Skip to content

Commit 8b11a60

Browse files
committed
Added backup to/restore from file feature
1 parent cb31410 commit 8b11a60

File tree

4 files changed

+144
-1
lines changed

4 files changed

+144
-1
lines changed

src/js/background/backgroundLogic.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,70 @@ const backgroundLogic = {
267267
return browser.tabs.remove(tabIds);
268268
},
269269

270+
async backupIdentitiesState() {
271+
const identities = await browser.contextualIdentities.query({});
272+
return Promise.all(
273+
identities.map(async ({ cookieStoreId, color, icon, name }) => {
274+
const userContextId = this.getUserContextIdFromCookieStoreId(cookieStoreId);
275+
const sitesByContainer = await assignManager.storageArea.getAssignedSites(userContextId);
276+
const sites = Object.values(sitesByContainer).map(({ neverAsk, hostname }) => ({ neverAsk, hostname }));
277+
return ({ color, icon, name, sites });
278+
})
279+
);
280+
},
281+
282+
async restoreIdentitiesState(identities) {
283+
const backup = await browser.contextualIdentities.query({});
284+
const incomplete = [];
285+
let allSucceed = true;
286+
const identitiesPromise = identities.map(async ({ color, icon, name, sites }) => {
287+
try {
288+
if (typeof color !== "string" || typeof icon !== "string" || typeof name !== "string" || !Array.isArray((sites)))
289+
throw new Error("Corrupted container backup");
290+
const identity = await browser.contextualIdentities.create({ color, icon, name });
291+
try {
292+
await identityState.storageArea.get(identity.cookieStoreId);
293+
const userContextId = this.getUserContextIdFromCookieStoreId(identity.cookieStoreId);
294+
for (const { neverAsk, hostname } of sites) {
295+
if (typeof neverAsk !== "boolean" || typeof hostname !== "string" || hostname === "")
296+
throw new Error("Corrupted site association");
297+
const pageUrl = `http://${hostname}`; // protocol doesn't really matter here
298+
await assignManager.storageArea.set(pageUrl, {
299+
neverAsk,
300+
userContextId
301+
});
302+
}
303+
} catch (err) {
304+
incomplete.push(name); // site association damaged
305+
}
306+
return identity;
307+
} catch (err) {
308+
allSucceed = false;
309+
return null;
310+
}
311+
});
312+
const created = await Promise.all(identitiesPromise);
313+
if (!allSucceed) { // Importation failed, restore previous state
314+
await Promise.all(
315+
created.map(async (identityOrNull) => {
316+
if (identityOrNull) {
317+
await identityState.storageArea.remove(identityOrNull.cookieStoreId);
318+
await browser.contextualIdentities.remove(identityOrNull.cookieStoreId);
319+
}
320+
})
321+
);
322+
throw new Error("Some containers couldn't be created");
323+
} else { // Importation succeed, remove old identities
324+
await Promise.all(
325+
backup.map(async (identity) => {
326+
await identityState.storageArea.remove(identity.cookieStoreId);
327+
await browser.contextualIdentities.remove(identity.cookieStoreId);
328+
})
329+
);
330+
}
331+
return { created: created.length, incomplete };
332+
},
333+
270334
async queryIdentitiesState(windowId) {
271335
const identities = await browser.contextualIdentities.query({});
272336
const identitiesOutput = {};

src/js/background/messageHandler.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ const messageHandler = {
7272
windowId: m.windowId
7373
});
7474
break;
75+
case "backupIdentitiesState":
76+
response = backgroundLogic.backupIdentitiesState();
77+
break;
78+
case "restoreIdentitiesState":
79+
response = backgroundLogic.restoreIdentitiesState(m.identities);
80+
break;
7581
case "queryIdentitiesState":
7682
response = backgroundLogic.queryIdentitiesState(m.message.windowId);
7783
break;

src/js/options.js

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,56 @@ async function enableDisableReplaceTab() {
5353
await browser.storage.local.set({replaceTabEnabled: !!checkbox.checked});
5454
}
5555

56+
async function backupContainers() {
57+
const backupLink = document.getElementById("containers-save-link");
58+
const backupResult = document.getElementById("containers-save-result");
59+
try {
60+
const content = JSON.stringify(
61+
await browser.runtime.sendMessage({
62+
method: "backupIdentitiesState"
63+
})
64+
);
65+
backupLink.href = `data:application/json;base64,${btoa(content)}`;
66+
backupLink.download = `containers-backup-${(new Date()).toISOString()}.json`;
67+
backupLink.click();
68+
backupResult.textContent = "";
69+
} catch (err) {
70+
backupResult.textContent = browser.i18n.getMessage("backupFailure", [String(err.message || err)]);
71+
backupResult.style.color = "red";
72+
}
73+
}
74+
75+
async function restoreContainers(event) {
76+
const restoreInput = event.currentTarget;
77+
const restoreResult = document.getElementById("containers-restore-result");
78+
event.preventDefault();
79+
if (restoreInput.files.length) {
80+
const reader = new FileReader();
81+
reader.onloadend = async () => {
82+
try {
83+
const identitiesState = JSON.parse(reader.result);
84+
const { created: restoredCount, incomplete } = await browser.runtime.sendMessage({
85+
method: "restoreIdentitiesState",
86+
identities: identitiesState
87+
});
88+
if (incomplete.length === 0) {
89+
restoreResult.textContent = browser.i18n.getMessage("containersRestored", [String(restoredCount)]);
90+
restoreResult.style.color = "green";
91+
} else {
92+
restoreResult.textContent = browser.i18n.getMessage("containersPartiallyRestored", [String(restoredCount), String(incomplete.join(", "))]);
93+
restoreResult.style.color = "orange";
94+
}
95+
} catch (err) {
96+
console.error("Cannot restore containers list: %s", err.message || err);
97+
restoreResult.textContent = browser.i18n.getMessage("containersRestorationFailed");
98+
restoreResult.style.color = "red";
99+
}
100+
};
101+
reader.readAsText(restoreInput.files.item(0));
102+
}
103+
restoreInput.value = "";
104+
}
105+
56106
async function setupOptions() {
57107
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
58108
const { replaceTabEnabled } = await browser.storage.local.get("replaceTabEnabled");
@@ -114,15 +164,20 @@ browser.permissions.onRemoved.addListener(resetPermissionsUi);
114164
document.addEventListener("DOMContentLoaded", setupOptions);
115165
document.querySelector("#syncCheck").addEventListener( "change", enableDisableSync);
116166
document.querySelector("#replaceTabCheck").addEventListener( "change", enableDisableReplaceTab);
167+
document.querySelector("#containersRestoreInput").addEventListener( "change", restoreContainers);
117168
maybeShowPermissionsWarningIcon();
118169
for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
119170
document.querySelector("#open_container_"+i)
120171
.addEventListener("change", storeShortcutChoice);
121172
}
122173

123174
document.querySelectorAll("[data-btn-id]").forEach(btn => {
124-
btn.addEventListener("click", () => {
175+
btn.addEventListener("click", e => {
125176
switch (btn.dataset.btnId) {
177+
case "containers-save-button":
178+
e.preventDefault();
179+
backupContainers();
180+
break;
126181
case "reset-onboarding":
127182
resetOnboarding();
128183
break;

src/options.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,24 @@ <h3 data-i18n-message-id="tabBehavior"></h3>
5959
<p><em data-i18n-message-id="replaceTabDescription"></em></p>
6060
</div>
6161

62+
<h3 data-i18n-message-id="backup"></h3>
63+
64+
<div class="settings-group">
65+
<fieldset>
66+
<legend data-i18n-message-id="restoreLegend"></legend>
67+
<input id="containersRestoreInput" type="file">
68+
<p><em id="containers-restore-result"></em></p>
69+
<p data-i18n-message-id="warningConfigOverride"></p>
70+
</fieldset>
71+
<fieldset>
72+
<legend data-i18n-message-id="saveLegend"></legend>
73+
<a id="containers-save-link" href="#" style="display: none;"></a>
74+
<button data-i18n-message-id="saveButton" data-btn-id="containers-save-button"></button>
75+
<p><em id="containers-save-result"></em></p>
76+
<p data-i18n-message-id="noteWontBackupCookies"></p>
77+
</fieldset>
78+
</div>
79+
6280
<h3 data-i18n-message-id="keyboardShortCuts"></h3>
6381
<p><em data-i18n-message-id="editWhichContainer"></em></p>
6482

0 commit comments

Comments
 (0)