diff --git a/docs/API-Reference/features/TaskManager.md b/docs/API-Reference/features/TaskManager.md index fdb774bd3c..d07490a63a 100644 --- a/docs/API-Reference/features/TaskManager.md +++ b/docs/API-Reference/features/TaskManager.md @@ -87,7 +87,7 @@ Methods for managing the task's state and UI representation in the TaskManager. | getProgressPercent | function | Returns the task's current progress percentage. | | setFailed | function | Marks the task as failed. | | isFailed | function | Returns true if the task is marked as failed. | -| setSucceded | function | Marks the task as succeeded. | +| setSucceeded | function | Marks the task as succeeded. | | isSucceded | function | Returns true if the task is marked as succeeded. | | showStopIcon | function | Shows the stop icon with an optional tooltip message. | | hideStopIcon | function | Hides the stop icon. | diff --git a/src/extensions/default/PHPSupport/ServerInstaller.js b/src/extensions/default/PHPSupport/ServerInstaller.js index 83f20f001d..1414d1f707 100644 --- a/src/extensions/default/PHPSupport/ServerInstaller.js +++ b/src/extensions/default/PHPSupport/ServerInstaller.js @@ -241,7 +241,7 @@ define(function (require, exports, module) { } task.setProgressPercent(100); task.setMessage(Strings.PHP_INSTALL_DONE); - task.setSucceded(); // (sic - TaskManager's exported name) + task.setSucceeded(); setTimeout(task.close, 4000); Metrics.countEvent("lsp", "phpInst", upgrading ? "upOk" : "ok"); return { entryPath: getEntryPlatformPath(), upgraded: upgrading }; diff --git a/src/extensions/default/PythonSupport/ServerInstaller.js b/src/extensions/default/PythonSupport/ServerInstaller.js index 792ec6bfec..1c603a2edf 100644 --- a/src/extensions/default/PythonSupport/ServerInstaller.js +++ b/src/extensions/default/PythonSupport/ServerInstaller.js @@ -339,7 +339,7 @@ define(function (require, exports, module) { currentUnit = null; task.setProgressPercent(100); task.setMessage(Strings.PYTHON_INSTALL_DONE); - task.setSucceded(); // (sic - TaskManager's exported name) + task.setSucceeded(); setTimeout(task.close, 4000); Metrics.countEvent("lsp", "pyInst", upgrading ? "upOk" : "ok"); return { binaryPath: getBinaryPlatformPath(), upgraded: upgrading }; diff --git a/src/extensionsIntegrated/MigrateAssist/constants.js b/src/extensionsIntegrated/MigrateAssist/constants.js index ff41ed689c..1db7f0e090 100644 --- a/src/extensionsIntegrated/MigrateAssist/constants.js +++ b/src/extensionsIntegrated/MigrateAssist/constants.js @@ -46,16 +46,16 @@ define(function (require, exports, module) { const NEW_ORIGIN = "https://web.phcode.dev"; /** - * Human readable form of the above, used inside translated sentences via StringUtils.format. + * Human readable form of the new origin, used inside translated sentences via StringUtils.format. + * The legacy equivalent is derived from the origin instead, see getLegacyDomainName. */ - const LEGACY_DOMAIN_NAME = "phcode.dev"; const NEW_DOMAIN_NAME = "web.phcode.dev"; /** * The day the legacy origin stops serving. Month is 0 based, so 8 is September. * @type {number} */ - const SUNSET_DATE = Date.UTC(2026, 8, 1); + const SUNSET_DATE = Date.UTC(2026, 8, 10); /** * Android/ChromeOS Trusted Web Activity that wraps the legacy origin. Users launched from this @@ -135,6 +135,16 @@ define(function (require, exports, module) { return !Phoenix.isNativeApp && location.origin === getNewOrigin(); } + /** + * Hostname of the origin being retired, for use inside user facing sentences. Derived from the + * origin rather than written out separately so the two can never disagree, which matters while + * the legacy origin still points at staging. + * @return {string} + */ + function getLegacyDomainName() { + return new URL(getLegacyOrigin()).hostname; + } + /** * URL of the helper page on the legacy origin. * @@ -189,13 +199,13 @@ define(function (require, exports, module) { return (typeof now === "number" ? now : Date.now()) >= SUNSET_DATE; } - exports.LEGACY_DOMAIN_NAME = LEGACY_DOMAIN_NAME; exports.NEW_DOMAIN_NAME = NEW_DOMAIN_NAME; exports.SUNSET_DATE = SUNSET_DATE; exports.TWA_STORE_URL = TWA_STORE_URL; exports.MIGRATION_DONE_KEY = MIGRATION_DONE_KEY; exports.getLegacyOrigin = getLegacyOrigin; exports.getMigrateAssistURL = getMigrateAssistURL; + exports.getLegacyDomainName = getLegacyDomainName; exports.getNewOrigin = getNewOrigin; exports.isLegacyOrigin = isLegacyOrigin; exports.isNewOrigin = isNewOrigin; diff --git a/src/extensionsIntegrated/MigrateAssist/html/migrate-progress.html b/src/extensionsIntegrated/MigrateAssist/html/migrate-progress.html deleted file mode 100644 index 1b3f051cc2..0000000000 --- a/src/extensionsIntegrated/MigrateAssist/html/migrate-progress.html +++ /dev/null @@ -1,25 +0,0 @@ - diff --git a/src/extensionsIntegrated/MigrateAssist/main.js b/src/extensionsIntegrated/MigrateAssist/main.js index 7ff9caa436..76969e5363 100644 --- a/src/extensionsIntegrated/MigrateAssist/main.js +++ b/src/extensionsIntegrated/MigrateAssist/main.js @@ -38,6 +38,7 @@ define(function (require, exports, module) { Commands = require("command/Commands"), Menus = require("command/Menus"), Strings = require("strings"), + StringUtils = require("utils/StringUtils"), Constants = require("./constants"), SunsetDialog = require("./sunset-dialog"), Migrator = require("./migrator"); @@ -54,7 +55,9 @@ define(function (require, exports, module) { // desktop. It is also skipped on Safari/iOS, where the migration is deliberately not // implemented: offering an action we do not honour would be worse than not offering it. if (!Phoenix.isTestWindow && Constants.isMigrationSupportedBrowser()) { - CommandManager.register(Strings.CMD_MIGRATE_DATA, Commands.HELP_MIGRATE_DATA, function () { + CommandManager.register( + StringUtils.format(Strings.CMD_MIGRATE_DATA, Constants.getLegacyDomainName()), + Commands.HELP_MIGRATE_DATA, function () { Migrator.runManually(); }); // Anchored to About rather than to Check for Updates: the updater only registers its diff --git a/src/extensionsIntegrated/MigrateAssist/migrator.js b/src/extensionsIntegrated/MigrateAssist/migrator.js index 61d76dd1b0..b7276146bd 100644 --- a/src/extensionsIntegrated/MigrateAssist/migrator.js +++ b/src/extensionsIntegrated/MigrateAssist/migrator.js @@ -35,22 +35,22 @@ define(function (require, exports, module) { const Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), - Mustache = require("thirdparty/mustache/mustache"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), Metrics = require("utils/Metrics"), - ZipUtils = require("utils/ZipUtils"), + TaskManager = require("features/TaskManager"), PreferencesManager = require("preferences/PreferencesManager"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), - Constants = require("./constants"), - progressTemplate = require("text!./html/migrate-progress.html"); + Constants = require("./constants"); const HANDSHAKE_TIMEOUT_MS = 15000, - BUNDLE_TIMEOUT_MS = 120000, + FILE_TIMEOUT_MS = 60000, + CHUNK_SIZE = 8 * 1024 * 1024, IFRAME_ID = "migrate-assist-frame"; const RESULT_MIGRATED = "migrated", + RESULT_DECLINED = "declined", RESULT_NOTHING = "nothing", RESULT_UNREACHABLE = "unreachable"; @@ -68,7 +68,7 @@ define(function (require, exports, module) { iframe.style.display = "none"; let pendingScan = null, - bundleHandler = null, + fileHandler = null, destroyed = false; function _onMessage(event) { @@ -87,8 +87,8 @@ define(function (require, exports, module) { const resolve = pendingScan; pendingScan = null; resolve(data); - } else if (bundleHandler) { - bundleHandler(data); + } else if (fileHandler) { + fileHandler(data); } } @@ -126,100 +126,158 @@ define(function (require, exports, module) { } /** - * Requests one bundle and reassembles its chunks into a single ArrayBuffer. + * Requests one file, reassembling it from chunks if it is larger than the chunk size. */ - function fetchBundle(id) { + function fetchFile(path) { return new Promise((resolve, reject) => { - const chunks = []; - let expected = -1, - received = 0; + const parts = []; + let received = 0; const timer = setTimeout(() => { - bundleHandler = null; - reject(new Error("timed out receiving " + id)); - }, BUNDLE_TIMEOUT_MS); + fileHandler = null; + reject(new Error("timed out receiving " + path)); + }, FILE_TIMEOUT_MS); - bundleHandler = function (data) { - if (data.id !== id) { + function requestFrom(offset) { + iframe.contentWindow.postMessage( + { type: "MIGRATE_READ", path: path, offset: offset, length: CHUNK_SIZE }, + legacyOrigin); + } + + fileHandler = function (data) { + if (data.path !== path) { return; } if (data.type === "MIGRATE_ERROR") { clearTimeout(timer); - bundleHandler = null; + fileHandler = null; reject(new Error(data.message)); - } else if (data.type === "MIGRATE_BUNDLE_META") { - expected = data.chunkCount; - } else if (data.type === "MIGRATE_CHUNK") { - chunks[data.index] = data.chunk; - received = received + 1; - if (data.last || (expected > 0 && received === expected)) { - clearTimeout(timer); - bundleHandler = null; - const total = chunks.reduce((sum, c) => sum + c.byteLength, 0); - const merged = new Uint8Array(total); - let offset = 0; - for (const chunk of chunks) { - merged.set(new Uint8Array(chunk), offset); - offset = offset + chunk.byteLength; - } - resolve(merged.buffer); - } + return; + } + if (data.type !== "MIGRATE_DATA") { + return; } + parts.push(new Uint8Array(data.chunk)); + received = received + data.chunk.byteLength; + if (!data.eof) { + requestFrom(received); + return; + } + clearTimeout(timer); + fileHandler = null; + if (parts.length === 1) { + resolve(parts[0]); + return; + } + const merged = new Uint8Array(received); + let offset = 0; + for (const part of parts) { + merged.set(part, offset); + offset = offset + part.byteLength; + } + resolve(merged); }; - iframe.contentWindow.postMessage({ type: "MIGRATE_BUNDLE", id: id }, legacyOrigin); + requestFrom(0); }); } - return { scan, fetchBundle, destroy }; + return { scan, fetchFile, destroy }; } - function _showProgressDialog(bundleCount) { - const dialog = Dialogs.showModalDialogUsingTemplate( - Mustache.render(progressTemplate, { - Strings: Strings, - introMessage: StringUtils.format(Strings.MIGRATE_PROGRESS_INTRO, - Constants.LEGACY_DOMAIN_NAME) - }), - false // no auto dismiss, the transfer must not be interrupted half way - ); - const $dlg = dialog.getElement(); - let bundlesDone = 0; - + /** + * Progress lives in the status bar rather than a modal. The transfer is dominated by IndexedDB + * writes at roughly 10ms per file, so a few thousand files is minutes long, and blocking the + * whole editor behind an undismissable dialog for that is not acceptable. The user is told once + * up front, works normally while it runs, and gets a dialog again only when it is done. + */ + function _startProgressTask(totalFiles) { + const task = TaskManager.addNewTask( + Strings.MIGRATE_PROGRESS_TITLE, + StringUtils.format(Strings.MIGRATE_PROGRESS_STATUS, 0, totalFiles), + ``); return { - dialog: dialog, - setWaiting: function (index, name) { - // The remote side is zipping. Nothing to count yet, so pulse rather than sit at 0%. - $dlg.find(".migrate-assist-bar").addClass("migrate-assist-bar-indeterminate"); - $dlg.find(".migrate-assist-status") - .text(StringUtils.format(Strings.MIGRATE_PROGRESS_PREPARING, name, index + 1, bundleCount)); + update: function (done) { + task.setProgressPercent(Math.round((done / totalFiles) * 100)); + task.setMessage(StringUtils.format(Strings.MIGRATE_PROGRESS_STATUS, done, totalFiles)); }, - setBundleProgress: function (index, name, doneFiles, totalFiles) { - $dlg.find(".migrate-assist-bar").removeClass("migrate-assist-bar-indeterminate"); - bundlesDone = index; - const withinBundle = totalFiles ? (doneFiles / totalFiles) : 0; - const overall = Math.min(100, Math.round(((bundlesDone + withinBundle) / bundleCount) * 100)); - $dlg.find(".migrate-assist-bar").css("width", `${overall}%`); - $dlg.find(".migrate-assist-status") - .text(StringUtils.format(Strings.MIGRATE_PROGRESS_STATUS, name, index + 1, bundleCount)); + succeed: function (done) { + task.setProgressPercent(100); + task.setMessage(StringUtils.format(Strings.MIGRATE_PROGRESS_STATUS, done, totalFiles)); + task.setSucceeded(); + task.close(); }, - finish: function (summary) { - $dlg.find(".migrate-assist-bar") - .removeClass("migrate-assist-bar-indeterminate") - .css("width", "100%"); - $dlg.find(".migrate-assist-intro").text(Strings.MIGRATE_DONE_TITLE); - $dlg.find(".migrate-assist-status").text(summary.message); - if (summary.detail) { - $dlg.find(".migrate-assist-detail").removeClass("forced-hidden").text(summary.detail); - } - $dlg.find(".migrate-assist-reload").removeClass("forced-hidden").on("click", function () { - CommandManager.execute(Commands.APP_RELOAD); - }); - $dlg.find(".migrate-assist-close").removeClass("forced-hidden").on("click", function () { - dialog.close(); - }); + fail: function () { + task.setFailed(); + task.close(); } }; } + /** + * Shown once, before anything is copied, so the user knows why their machine is busy. + */ + function _confirmStart(fileCount) { + return Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_INFO, + Strings.MIGRATE_PROGRESS_TITLE, + StringUtils.format(Strings.MIGRATE_START_MESSAGE, fileCount, Constants.getLegacyDomainName()), + [ + { + className: Dialogs.DIALOG_BTN_CLASS_NORMAL, + id: Dialogs.DIALOG_BTN_CANCEL, + text: Strings.CANCEL + }, + { + className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, + id: Dialogs.DIALOG_BTN_OK, + text: Strings.MIGRATE_START_CONFIRM + } + ] + ).getPromise(); + } + + function _showCompletion(migratedFiles, failed) { + const message = failed.length + ? StringUtils.format(Strings.MIGRATE_DONE_MESSAGE, migratedFiles) + "

" + + StringUtils.format(Strings.MIGRATE_DONE_PARTIAL, failed.length) + : StringUtils.format(Strings.MIGRATE_DONE_MESSAGE, migratedFiles); + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_INFO, + Strings.MIGRATE_DONE_TITLE, + message, + [ + { + className: Dialogs.DIALOG_BTN_CLASS_NORMAL, + id: Dialogs.DIALOG_BTN_CANCEL, + text: Strings.MIGRATE_RELOAD_LATER + }, + { + className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, + id: Dialogs.DIALOG_BTN_OK, + text: Strings.MIGRATE_RELOAD_NOW + } + ] + ).done(function (buttonId) { + if (buttonId === Dialogs.DIALOG_BTN_OK) { + CommandManager.execute(Commands.APP_RELOAD); + } + }); + } + + /** + * Filer needs the parent directory to exist before a write, and mkdirs is recursive. + */ + function _ensureParentDir(filePath) { + return new Promise((resolve, reject) => { + window.fs.mkdirs(window.path.dirname(filePath), 0o755, true, (err) => { + if (err && err.code !== "EEXIST") { + reject(err); + return; + } + resolve(); + }); + }); + } + async function _applyPreferences(prefsText) { // Flush first. The in memory user scope is authoritative, so overwriting the file underneath // it would just get clobbered by the next save. @@ -250,35 +308,39 @@ define(function (require, exports, module) { let progress = null; try { const scan = await bridge.scan(); - if (!scan.hasData || !scan.bundles.length) { + if (!scan.hasData || !scan.files.length) { Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist", manual ? "manualNothing" : "autoNothing"); return RESULT_NOTHING; } + // Asked once, before anything is copied. After this the user is left alone. + const choice = await _confirmStart(scan.files.length); + if (choice !== Dialogs.DIALOG_BTN_OK) { + Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist", "declined"); + return RESULT_DECLINED; + } + Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist", manual ? "manualStart" : "autoStart"); - progress = _showProgressDialog(scan.bundles.length); + progress = _startProgressTask(scan.files.length); const failed = []; let migratedFiles = 0; - for (let i = 0; i < scan.bundles.length; i++) { - const bundle = scan.bundles[i]; - const name = bundle.dest.substring(bundle.dest.lastIndexOf("/") + 1); - progress.setWaiting(i, name); + for (const file of scan.files) { try { - const buffer = await bridge.fetchBundle(bundle.id); - await ZipUtils.unzipBinDataToLocation(buffer, bundle.dest, false, - function (doneCount, totalCount) { - progress.setBundleProgress(i, name, doneCount, totalCount); - return true; // must be explicit, see unzipBinDataToLocation - }); - migratedFiles = migratedFiles + bundle.fileCount; + const bytes = await bridge.fetchFile(file.path); + // Paths are identical on both origins, so the destination is the source path. + await _ensureParentDir(file.path); + await Phoenix.VFS.writeFileAsync(file.path, window.Filer.Buffer.from(bytes), + window.fs.BYTE_ARRAY_ENCODING); + migratedFiles = migratedFiles + 1; } catch (err) { - // One bad folder should not cost the user everything else. - console.error("MigrateAssist: bundle failed", bundle.id, err); - failed.push(name); + // One unreadable file should not cost the user everything else. + console.error("MigrateAssist: could not copy", file.path, err); + failed.push(file.path); } + progress.update(migratedFiles + failed.length); } if (scan.prefs) { @@ -299,19 +361,15 @@ define(function (require, exports, module) { Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist", failed.length ? "completedWithErrors" : "completed"); - progress.finish({ - message: StringUtils.format(Strings.MIGRATE_DONE_MESSAGE, migratedFiles), - detail: failed.length - ? StringUtils.format(Strings.MIGRATE_DONE_PARTIAL, failed.join(", ")) - : null - }); + progress.succeed(migratedFiles); + _showCompletion(migratedFiles, failed); return RESULT_MIGRATED; } catch (err) { console.error("MigrateAssist: migration could not run", err); Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist", manual ? "manualUnreachable" : "autoUnreachable"); if (progress) { - progress.dialog.close(); + progress.fail(); } return RESULT_UNREACHABLE; } finally { @@ -348,14 +406,17 @@ define(function (require, exports, module) { */ async function runManually() { const result = await run(true); + if (result === RESULT_DECLINED) { + return; // the user said no, they do not need to be told what they just chose + } if (result === RESULT_NOTHING) { Dialogs.showModalDialog(DefaultDialogs.DIALOG_ID_INFO, Strings.MIGRATE_NOTHING_TITLE, - StringUtils.format(Strings.MIGRATE_NOTHING_MESSAGE, Constants.LEGACY_DOMAIN_NAME)); + StringUtils.format(Strings.MIGRATE_NOTHING_MESSAGE, Constants.getLegacyDomainName())); } else if (result === RESULT_UNREACHABLE) { Dialogs.showModalDialog(DefaultDialogs.DIALOG_ID_ERROR, Strings.MIGRATE_UNREACHABLE_TITLE, - StringUtils.format(Strings.MIGRATE_UNREACHABLE_MESSAGE, Constants.LEGACY_DOMAIN_NAME)); + StringUtils.format(Strings.MIGRATE_UNREACHABLE_MESSAGE, Constants.getLegacyDomainName())); } } diff --git a/src/extensionsIntegrated/MigrateAssist/sunset-dialog.js b/src/extensionsIntegrated/MigrateAssist/sunset-dialog.js index 85ec1ff579..7baf156d7a 100644 --- a/src/extensionsIntegrated/MigrateAssist/sunset-dialog.js +++ b/src/extensionsIntegrated/MigrateAssist/sunset-dialog.js @@ -51,13 +51,13 @@ define(function (require, exports, module) { const paragraphs = []; paragraphs.push(StringUtils.format(Strings.MIGRATE_MOVING_MESSAGE, - Constants.LEGACY_DOMAIN_NAME, Constants.NEW_DOMAIN_NAME)); + Constants.getLegacyDomainName(), Constants.NEW_DOMAIN_NAME)); if (!Constants.isPastSunset()) { const days = Constants.daysToSunset(); paragraphs.push(StringUtils.format( days === 1 ? Strings.MIGRATE_SUNSET_COUNTDOWN_ONE : Strings.MIGRATE_SUNSET_COUNTDOWN, - days, Constants.LEGACY_DOMAIN_NAME)); + days, Constants.getLegacyDomainName())); } if (!Constants.isMigrationSupportedBrowser()) { @@ -112,7 +112,7 @@ define(function (require, exports, module) { Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_INFO, - StringUtils.format(Strings.MIGRATE_MOVING_TITLE, Constants.LEGACY_DOMAIN_NAME), + StringUtils.format(Strings.MIGRATE_MOVING_TITLE, Constants.getLegacyDomainName()), _buildMessage(), _buildButtons() ).done(function (buttonId) { diff --git a/src/extensionsIntegrated/appUpdater/main.js b/src/extensionsIntegrated/appUpdater/main.js index f2ea906989..fbc45a686a 100644 --- a/src/extensionsIntegrated/appUpdater/main.js +++ b/src/extensionsIntegrated/appUpdater/main.js @@ -582,7 +582,7 @@ define(function (require, exports, module) { } else if(data === UPDATE_STATUS.INSTALLER_DOWNLOADED){ Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'downloaded', Phoenix.platform); updatePendingRestart = true; - updateTask.setSucceded(); + updateTask.setSucceeded(); updateTask.setTitle(Strings.UPDATE_DONE); updateTask.setMessage(Strings.UPDATE_RESTART_INSTALL); if(!updateInstalledDialogShown){ diff --git a/src/extensionsIntegrated/appUpdater/update-electron.js b/src/extensionsIntegrated/appUpdater/update-electron.js index 9629b88d2b..0c57fdf89c 100644 --- a/src/extensionsIntegrated/appUpdater/update-electron.js +++ b/src/extensionsIntegrated/appUpdater/update-electron.js @@ -194,7 +194,7 @@ define(function (require, exports, module) { await window.electronAPI.setUpdateScheduled(true); showOrHideUpdateIcon(); Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'scheduled', Phoenix.platform); - updateTask.setSucceded(); + updateTask.setSucceeded(); updateTask.setTitle(Strings.UPDATE_DONE); updateTask.setMessage(Strings.UPDATE_RESTART_INSTALL); NotificationUI.createToastFromTemplate(Strings.UPDATE_READY_RESTART_TITLE, @@ -406,7 +406,7 @@ define(function (require, exports, module) { Strings.UPDATE_READY_RESTART_INSTALL_MESSAGE); } }); - updateTask.setSucceded(); + updateTask.setSucceeded(); Phoenix.app.registerQuitTimeAppUpdateHandler(quitTimeAppUpdateHandler); console.log("Update was scheduled in another window, registering quit handler"); } diff --git a/src/features/TaskManager.js b/src/features/TaskManager.js index c1450e2c0c..31b558f8d5 100644 --- a/src/features/TaskManager.js +++ b/src/features/TaskManager.js @@ -318,7 +318,7 @@ define(function (require, exports, module) { * @property {function(): number} getProgressPercent - Returns the task's current progress percentage. * @property {function(): void} setFailed - Marks the task as failed. * @property {function(): boolean} isFailed - Returns true if the task is marked as failed. - * @property {function(): void} setSucceded - Marks the task as succeeded. + * @property {function(): void} setSucceeded - Marks the task as succeeded. * @property {function(): boolean} isSucceded - Returns true if the task is marked as succeeded. * @property {function(string): void} showStopIcon - Shows the stop icon with an optional tooltip message. * @property {function(): void} hideStopIcon - Hides the stop icon. @@ -521,7 +521,7 @@ define(function (require, exports, module) { task.getTitle = getTitle; task.setMessage = setMessage; task.getMessage = getMessage; - task.setSucceded = setSucceeded; + task.setSucceeded = setSucceeded; task.isSucceeded = isSucceeded; task.setFailed = setFailed; task.isFailed = isFailed; diff --git a/src/migrateAssist.html b/src/migrateAssist.html index be36c09ca0..ffb4bb9b24 100644 --- a/src/migrateAssist.html +++ b/src/migrateAssist.html @@ -38,9 +38,14 @@ It deliberately does not boot Phoenix. virtualfs.js is a self contained IIFE that gives us window.fs over the same Filer IndexedDB, which is all we need. + + Files are streamed one at a time rather than zipped per folder. IndexedDB costs roughly 10ms per + file whatever we do, so an intermediate zip only adds a read/encode/decode pass on top of that + floor, and measured on 300 files it produced an archive slightly LARGER than the input because + JSZip stores uncompressed. Streaming also gives the receiver an exact per file progress count and + keeps peak memory at one file instead of one whole folder. --> -