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:
Ping Chen 2022-03-11 12:06:04 +02:00
Родитель 8a4ed15e83
Коммит 9707d6264b
9 изменённых файлов: 283 добавлений и 35 удалений

Просмотреть файл

@ -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",