Bug 1757695 - Support exporting current profile on about:import page. r=benc
Differential Revision: https://phabricator.services.mozilla.com/D140704 --HG-- extra : amend_source : 50d3f66a39933ac3d2c0f29c7f172b2ad8da81eb
This commit is contained in:
Родитель
8a4ed15e83
Коммит
9707d6264b
|
@ -519,9 +519,24 @@ function showChatTab() {
|
|||
}
|
||||
}
|
||||
|
||||
function toImport() {
|
||||
/**
|
||||
* Open about:import or importDialog.xhtml.
|
||||
* @param {string} [tabId] - The tab to open in about:import, can be one of
|
||||
* ["app", "addressBook", "export"].
|
||||
*/
|
||||
function toImport(tabId = "app") {
|
||||
if (Services.prefs.getBoolPref("mail.import.in_new_tab")) {
|
||||
toMessengerWindow().openContentTab("about:import");
|
||||
let tab = toMessengerWindow().openTab("contentTab", {
|
||||
url: "about:import",
|
||||
onLoad(event, browser) {
|
||||
if (tabId) {
|
||||
browser.contentWindow.showTab(`tab-${tabId}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
// Somehow DOMContentLoaded is called even when about:import is already
|
||||
// open, which resets the active tab. Use setTimeout here as a workaround.
|
||||
setTimeout(() => tab.browser.contentWindow.showTab(`tab-${tabId}`), 100);
|
||||
return;
|
||||
}
|
||||
window.openDialog(
|
||||
|
@ -532,6 +547,10 @@ function toImport() {
|
|||
}
|
||||
|
||||
function toExport() {
|
||||
if (Services.prefs.getBoolPref("mail.import.in_new_tab")) {
|
||||
toImport("export");
|
||||
return;
|
||||
}
|
||||
window.openDialog(
|
||||
"chrome://messenger/content/exportDialog.xhtml",
|
||||
"exportDialog",
|
||||
|
|
|
@ -274,7 +274,7 @@ function importBook() {
|
|||
};
|
||||
|
||||
Services.obs.addObserver(observer, "addrbook-directory-created");
|
||||
window.browsingContext.topChromeWindow.toImport();
|
||||
window.browsingContext.topChromeWindow.toImport("addressBook");
|
||||
Services.obs.removeObserver(observer, "addrbook-directory-created");
|
||||
|
||||
// Select the directory after the import UI closes, so the user sees the change.
|
||||
|
|
|
@ -14,6 +14,8 @@ import-address-book = Import Address Book File
|
|||
|
||||
import-calendar = Import Calendar File
|
||||
|
||||
export-profile = Export
|
||||
|
||||
## Buttons
|
||||
|
||||
button-cancel = Cancel
|
||||
|
@ -22,6 +24,8 @@ button-back = Back
|
|||
|
||||
button-continue = Continue
|
||||
|
||||
button-export = Export
|
||||
|
||||
## Import from app steps
|
||||
|
||||
app-name-thunderbird = Thunderbird
|
||||
|
@ -84,7 +88,9 @@ addr-book-import-into-new-directory = Create a new directory
|
|||
|
||||
## Import dialog
|
||||
|
||||
progress-pane-title = Importing
|
||||
progress-pane-importing = Importing
|
||||
|
||||
progress-pane-exporting = Exporting
|
||||
|
||||
progress-pane-finished-desc = Finished.
|
||||
|
||||
|
@ -98,6 +104,8 @@ error-message-extract-zip-file-failed = Failed to extract the zip file. Please e
|
|||
|
||||
error-message-failed = Import failed unexpectedly, more information may be available in the Error Console.
|
||||
|
||||
error-export-failed = Export failed unexpectedly, more information may be available in the Error Console.
|
||||
|
||||
## <csv-field-map> element
|
||||
|
||||
csv-first-row-contains-headers = First row contains field names
|
||||
|
@ -109,3 +117,15 @@ csv-source-first-record = First record
|
|||
csv-source-second-record = Second record
|
||||
|
||||
csv-target-field = Address book field
|
||||
|
||||
## Export tab
|
||||
|
||||
export-profile-desc = Export mail accounts, mail messages, address books, settings to a zip file. When needed, you can import the zip file to restore your profile.
|
||||
|
||||
export-profile-desc2 = If your current profile is larger than 2GB, we suggest you back it up by yourself.
|
||||
|
||||
export-open-profile-folder = Open profile folder
|
||||
|
||||
export-file-picker = Export to a zip file
|
||||
|
||||
export-brand-name = { -brand-product-name }
|
||||
|
|
|
@ -116,6 +116,10 @@ td:last-child {
|
|||
color: var(--in-content-page-color);
|
||||
background-color: var(--in-content-categories-background);
|
||||
padding-top: 60px;
|
||||
padding-bottom: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#main {
|
||||
|
@ -169,3 +173,7 @@ td:last-child {
|
|||
"i text"
|
||||
". text";
|
||||
}
|
||||
|
||||
#tabPane-export p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const EXPORTED_SYMBOLS = ["ProfileExporter"];
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
Services: "resource://gre/modules/Services.jsm",
|
||||
setTimeout: "resource://gre/modules/Timer.jsm",
|
||||
});
|
||||
|
||||
// No need to backup those paths, they are not used when importing.
|
||||
const IGNORE_PATHS = [
|
||||
"cache2",
|
||||
"chrome_debugger_profile",
|
||||
"crashes",
|
||||
"datareporting",
|
||||
"extensions",
|
||||
"extension-store",
|
||||
"logs",
|
||||
"lock",
|
||||
"minidumps",
|
||||
"parent.lock",
|
||||
"shader-cache",
|
||||
"saved-telemetry-pings",
|
||||
"security_state",
|
||||
"storage",
|
||||
"xulstore",
|
||||
];
|
||||
|
||||
/**
|
||||
* A module to export the current profile to a zip file.
|
||||
*/
|
||||
class ProfileExporter {
|
||||
_logger = console.createInstance({
|
||||
prefix: "mail.export",
|
||||
maxLogLevel: "Warn",
|
||||
maxLogLevelPref: "mail.export.loglevel",
|
||||
});
|
||||
|
||||
/**
|
||||
* Callback for progress updates.
|
||||
* @param {number} current - Current imported items count.
|
||||
* @param {number} total - Total items count.
|
||||
*/
|
||||
onProgress = () => {};
|
||||
|
||||
/**
|
||||
* Export the current profile to the specified target zip file.
|
||||
* @param {nsIFile} targetFile - A target zip file to write to.
|
||||
*/
|
||||
async startExport(targetFile) {
|
||||
let zipW = Components.Constructor(
|
||||
"@mozilla.org/zipwriter;1",
|
||||
"nsIZipWriter"
|
||||
)();
|
||||
// MODE_WRONLY (0x02) and MODE_CREATE (0x08)
|
||||
zipW.open(targetFile, 0x02 | 0x08);
|
||||
let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
||||
let rootPathCount = PathUtils.split(profileDir.parent.path).length;
|
||||
let zipEntryMap = new Map();
|
||||
await this._collectFilesToZip(zipEntryMap, rootPathCount, profileDir);
|
||||
|
||||
let totalEntries = zipEntryMap.size;
|
||||
let i = 0;
|
||||
for (let [path, file] of zipEntryMap) {
|
||||
this._logger.debug("Adding entry file:", path);
|
||||
zipW.addEntryFile(
|
||||
path,
|
||||
0, // no compression, bigger file but much faster
|
||||
file,
|
||||
false
|
||||
);
|
||||
if (++i % 10 === 0) {
|
||||
this.onProgress(i, totalEntries);
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
}
|
||||
}
|
||||
this.onProgress(totalEntries, totalEntries);
|
||||
zipW.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect files to be zipped, save the entries into zipEntryMap.
|
||||
* @param {Map<string, nsIFile>} zipEntryMap - Collection of files to be zipped.
|
||||
* @param {number} rootPathCount - The count of rootPath parts.
|
||||
* @param {nsIFile} folder - The folder to search for files to zip.
|
||||
*/
|
||||
async _collectFilesToZip(zipEntryMap, rootPathCount, folder) {
|
||||
for (let file of folder.directoryEntries) {
|
||||
if (file.isDirectory()) {
|
||||
await this._collectFilesToZip(zipEntryMap, rootPathCount, file);
|
||||
} else {
|
||||
// We don't want to include the rootPath part in the zip file.
|
||||
let parts = PathUtils.split(file.path).slice(rootPathCount);
|
||||
// Parts look like this: ["profile-default", "lock"].
|
||||
if (IGNORE_PATHS.includes(parts[1])) {
|
||||
continue;
|
||||
}
|
||||
// Path separator inside a zip file is always "/".
|
||||
zipEntryMap.set(parts.join("/"), file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
"ProfileExporter.jsm",
|
||||
]
|
|
@ -11,6 +11,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
|||
MailServices: "resource:///modules/MailServices.jsm",
|
||||
MailUtils: "resource:///modules/MailUtils.jsm",
|
||||
AddrBookFileImporter: "resource:///modules/AddrBookFileImporter.jsm",
|
||||
ProfileExporter: "resource:///modules/ProfileExporter.jsm",
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -154,7 +155,7 @@ class ProfileImporterController extends ImporterController {
|
|||
// Let the user pick what to import.
|
||||
this._showItems(sourceProfiles[0]);
|
||||
} else {
|
||||
importDialog.showError("No profile found.");
|
||||
progressDialog.showError("No profile found.");
|
||||
}
|
||||
|
||||
document.getElementById(
|
||||
|
@ -259,7 +260,7 @@ class ProfileImporterController extends ImporterController {
|
|||
if (!selectedFile.isDirectory()) {
|
||||
if (selectedFile.fileSize > 2147483647) {
|
||||
// nsIZipReader only supports zip file less than 2GB.
|
||||
importDialog.showError(
|
||||
progressDialog.showError(
|
||||
await document.l10n.formatValue("error-message-zip-file-too-big")
|
||||
);
|
||||
return;
|
||||
|
@ -352,7 +353,7 @@ class ProfileImporterController extends ImporterController {
|
|||
this._extractedFileCount++;
|
||||
if (this._extractedFileCount % 10 == 0) {
|
||||
let progress = Math.min((this._extractedFileCount / 200) * 0.2, 0.2);
|
||||
importDialog.updateProgress(progress);
|
||||
progressDialog.updateProgress(progress);
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -361,20 +362,20 @@ class ProfileImporterController extends ImporterController {
|
|||
}
|
||||
// Use the tmp dir as source profile dir.
|
||||
this._sourceProfile = { dir: targetDir };
|
||||
importDialog.updateProgress(0.2);
|
||||
progressDialog.updateProgress(0.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the Continue button on the items pane.
|
||||
*/
|
||||
async _onSelectItems() {
|
||||
importDialog.showProgress(this);
|
||||
progressDialog.showProgress(this);
|
||||
if (this._importingFromZip) {
|
||||
this._extractedFileCount = 0;
|
||||
try {
|
||||
await this._extractZipFile();
|
||||
} catch (e) {
|
||||
importDialog.showError(
|
||||
progressDialog.showError(
|
||||
await document.l10n.formatValue(
|
||||
"error-message-extract-zip-file-failed"
|
||||
)
|
||||
|
@ -383,19 +384,19 @@ class ProfileImporterController extends ImporterController {
|
|||
}
|
||||
}
|
||||
this._importer.onProgress = (current, total) => {
|
||||
importDialog.updateProgress(
|
||||
progressDialog.updateProgress(
|
||||
this._importingFromZip ? 0.2 + (0.8 * current) / total : current / total
|
||||
);
|
||||
};
|
||||
try {
|
||||
importDialog.finish(
|
||||
progressDialog.finish(
|
||||
await this._importer.startImport(
|
||||
this._sourceProfile.dir,
|
||||
this._getItemsChecked()
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
importDialog.showError(
|
||||
progressDialog.showError(
|
||||
await document.l10n.formatValue("error-message-failed")
|
||||
);
|
||||
throw e;
|
||||
|
@ -594,16 +595,16 @@ class AddrBookImporterController extends ImporterController {
|
|||
targetDirectory = MailServices.ab.getDirectoryFromId(dirId);
|
||||
}
|
||||
|
||||
importDialog.showProgress(this);
|
||||
progressDialog.showProgress(this);
|
||||
this._importer.onProgress = (current, total) => {
|
||||
importDialog.updateProgress(current / total);
|
||||
progressDialog.updateProgress(current / total);
|
||||
};
|
||||
try {
|
||||
importDialog.finish(
|
||||
progressDialog.finish(
|
||||
await this._importer.startImport(this._sourceFile, targetDirectory)
|
||||
);
|
||||
} catch (e) {
|
||||
importDialog.showError(
|
||||
progressDialog.showError(
|
||||
await document.l10n.formatValue("error-message-failed")
|
||||
);
|
||||
throw e;
|
||||
|
@ -612,17 +613,71 @@ class AddrBookImporterController extends ImporterController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Control the #importDialog element, to show importing progress and result.
|
||||
* Control the #tabPane-export element, to support exporting the current profile
|
||||
* to a zip file.
|
||||
*/
|
||||
let importDialog = {
|
||||
class ExportController {
|
||||
back() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
async next() {
|
||||
let [
|
||||
filePickerTitle,
|
||||
brandName,
|
||||
progressPaneTitle,
|
||||
errorMsg,
|
||||
] = await document.l10n.formatValues([
|
||||
"export-file-picker",
|
||||
"export-brand-name",
|
||||
"progress-pane-exporting",
|
||||
"error-export-failed",
|
||||
]);
|
||||
let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(
|
||||
Ci.nsIFilePicker
|
||||
);
|
||||
filePicker.init(window, filePickerTitle, Ci.nsIFilePicker.modeSave);
|
||||
filePicker.defaultString = `${brandName}_profile_backup.zip`;
|
||||
filePicker.defaultExtension = "zip";
|
||||
filePicker.appendFilter("", "*.zip");
|
||||
let rv = await new Promise(resolve => filePicker.open(resolve));
|
||||
if (
|
||||
![Ci.nsIFilePicker.returnOK, Ci.nsIFilePicker.returnReplace].includes(rv)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let exporter = new ProfileExporter();
|
||||
progressDialog.showProgress(null, progressPaneTitle);
|
||||
exporter.onProgress = (current, total) => {
|
||||
progressDialog.updateProgress(current / total);
|
||||
};
|
||||
try {
|
||||
await exporter.startExport(filePicker.file);
|
||||
progressDialog.finish();
|
||||
} catch (e) {
|
||||
progressDialog.showError(errorMsg);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
openProfileFolder() {
|
||||
Services.dirsvc.get("ProfD", Ci.nsIFile).reveal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Control the #progressDialog element, to show importing progress and result.
|
||||
*/
|
||||
let progressDialog = {
|
||||
/**
|
||||
* Init internal variables and event bindings.
|
||||
*/
|
||||
init() {
|
||||
this._el = document.getElementById("importDialog");
|
||||
this._el = document.getElementById("progressDialog");
|
||||
this._elFooter = this._el.querySelector("footer");
|
||||
this._btnCancel = this._el.querySelector("#importDialogCancel");
|
||||
this._btnAccept = this._el.querySelector("#importDialogAccept");
|
||||
this._btnCancel = this._el.querySelector("#progressDialogCancel");
|
||||
this._btnAccept = this._el.querySelector("#progressDialogAccept");
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -660,8 +715,12 @@ let importDialog = {
|
|||
* Show the progress pane.
|
||||
* @param {ImporterController} importerController - An instance of the
|
||||
* controller.
|
||||
* @param {string} [title] - Pane title.
|
||||
*/
|
||||
showProgress(importerController) {
|
||||
async showProgress(importerController, title) {
|
||||
document.getElementById("progressPaneTitle").textContent = title
|
||||
? title
|
||||
: await document.l10n.formatValue("progress-pane-importing");
|
||||
this._showPane("progress");
|
||||
this._importerController = importerController;
|
||||
this._disableCancel(true);
|
||||
|
@ -673,7 +732,7 @@ let importDialog = {
|
|||
* @param {number} value - A number between 0 and 1 to represent the progress.
|
||||
*/
|
||||
updateProgress(value) {
|
||||
document.getElementById("importDialogProgressBar").value = value;
|
||||
document.getElementById("progressDialogProgressBar").value = value;
|
||||
if (value >= 1) {
|
||||
this._disableAccept(false);
|
||||
}
|
||||
|
@ -729,7 +788,7 @@ let importDialog = {
|
|||
|
||||
/**
|
||||
* Show a specific importing tab.
|
||||
* @param {string} tabId - One of ["tab-app", "tab-addressBook"].
|
||||
* @param {string} tabId - One of ["tab-app", "tab-addressBook", "tab-export"].
|
||||
*/
|
||||
function showTab(tabId) {
|
||||
let selectedPaneId = `tabPane-${tabId.split("-")[1]}`;
|
||||
|
@ -747,11 +806,13 @@ function showTab(tabId) {
|
|||
|
||||
let profileController;
|
||||
let addrBookController;
|
||||
let exportController;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
profileController = new ProfileImporterController();
|
||||
addrBookController = new AddrBookImporterController();
|
||||
importDialog.init();
|
||||
exportController = new ExportController();
|
||||
progressDialog.init();
|
||||
|
||||
for (let tab of document.querySelectorAll("[id^=tab-]")) {
|
||||
tab.onclick = () => showTab(tab.id);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<link rel="stylesheet" href="chrome://messenger/skin/accountSetup.css"/>
|
||||
<link rel="stylesheet" href="chrome://messenger/skin/aboutImport.css"/>
|
||||
|
||||
<link rel="localization" href="branding/brand.ftl" />
|
||||
<link rel="localization" href="messenger/aboutImport.ftl"/>
|
||||
|
||||
<script defer="" src="chrome://messenger/content/aboutImport.js"></script>
|
||||
|
@ -22,10 +23,15 @@
|
|||
<ul>
|
||||
<li id="tab-app"
|
||||
class="tab"
|
||||
data-l10n-id="import-from-app"></li>
|
||||
data-l10n-id="import-from-app"/>
|
||||
<li id="tab-addressBook"
|
||||
class="tab"
|
||||
data-l10n-id="import-address-book"></li>
|
||||
data-l10n-id="import-address-book"/>
|
||||
</ul>
|
||||
<ul>
|
||||
<li id="tab-export"
|
||||
class="tab"
|
||||
data-l10n-id="export-profile"/>
|
||||
</ul>
|
||||
</nav>
|
||||
<main id="main">
|
||||
|
@ -197,11 +203,27 @@
|
|||
data-l10n-id="button-continue"/>
|
||||
</footer>
|
||||
</div>
|
||||
<div id="tabPane-export" class="tabPane">
|
||||
<h1 data-l10n-id="export-profile"/>
|
||||
<p data-l10n-id="export-profile-desc"/>
|
||||
<p>
|
||||
<span data-l10n-id="export-profile-desc2"/>
|
||||
<a data-l10n-id="export-open-profile-folder"
|
||||
onclick="exportController.openProfileFolder()"/>
|
||||
</p>
|
||||
<footer class="buttons-container">
|
||||
<button onclick="exportController.back()"
|
||||
data-l10n-id="button-cancel"/>
|
||||
<button class="primary"
|
||||
onclick="exportController.next()"
|
||||
data-l10n-id="button-export"/>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
<dialog id="importDialog">
|
||||
<dialog id="progressDialog">
|
||||
<section id="dialogPane-progress">
|
||||
<h3 data-l10n-id="progress-pane-title"></h3>
|
||||
<progress id="importDialogProgressBar"></progress>
|
||||
<h3 id="progressPaneTitle"></h3>
|
||||
<progress id="progressDialogProgressBar"></progress>
|
||||
<p id="progressPaneDesc"/>
|
||||
</section>
|
||||
<section id="dialogPane-error">
|
||||
|
@ -209,10 +231,10 @@
|
|||
<p id="dialogError"/>
|
||||
</section>
|
||||
<footer>
|
||||
<button id="importDialogCancel"
|
||||
onclick="importDialog.onCancel()">Cancel</button>
|
||||
<button id="importDialogAccept"
|
||||
onclick="importDialog.onAccept()">OK</button>
|
||||
<button id="progressDialogCancel"
|
||||
onclick="progressDialog.onCancel()">Cancel</button>
|
||||
<button id="progressDialogAccept"
|
||||
onclick="progressDialog.onAccept()">OK</button>
|
||||
</footer>
|
||||
</dialog>
|
||||
</body>
|
||||
|
|
|
@ -10,6 +10,7 @@ DIRS += [
|
|||
"db/gloda",
|
||||
"db/mork",
|
||||
"db/msgdb",
|
||||
"export/modules",
|
||||
"extensions",
|
||||
"imap/public",
|
||||
"imap/src",
|
||||
|
|
Загрузка…
Ссылка в новой задаче