211 lines
6.5 KiB
JavaScript
211 lines
6.5 KiB
JavaScript
/* 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/. */
|
|
|
|
/**
|
|
* Handles serialization of Download objects and persistence into a file, so
|
|
* that the state of downloads can be restored across sessions.
|
|
*
|
|
* The file is stored in JSON format, without indentation. With indentation
|
|
* applied, the file would look like this:
|
|
*
|
|
* {
|
|
* "list": [
|
|
* {
|
|
* "source": "http://www.example.com/download.txt",
|
|
* "target": "/home/user/Downloads/download.txt"
|
|
* },
|
|
* {
|
|
* "source": {
|
|
* "url": "http://www.example.com/download.txt",
|
|
* "referrerInfo": serialized string represents referrerInfo object
|
|
* },
|
|
* "target": "/home/user/Downloads/download-2.txt"
|
|
* }
|
|
* ]
|
|
* }
|
|
*/
|
|
|
|
// Time after which insecure downloads that have not been dealt with on shutdown
|
|
// get removed (5 minutes).
|
|
const MAX_INSECURE_DOWNLOAD_AGE_MS = 5 * 60 * 1000;
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
Downloads: "resource://gre/modules/Downloads.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "gTextDecoder", function () {
|
|
return new TextDecoder();
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "gTextEncoder", function () {
|
|
return new TextEncoder();
|
|
});
|
|
|
|
/**
|
|
* Handles serialization of Download objects and persistence into a file, so
|
|
* that the state of downloads can be restored across sessions.
|
|
*
|
|
* @param aList
|
|
* DownloadList object to be populated or serialized.
|
|
* @param aPath
|
|
* String containing the file path where data should be saved.
|
|
*/
|
|
export var DownloadStore = function (aList, aPath) {
|
|
this.list = aList;
|
|
this.path = aPath;
|
|
};
|
|
|
|
DownloadStore.prototype = {
|
|
/**
|
|
* DownloadList object to be populated or serialized.
|
|
*/
|
|
list: null,
|
|
|
|
/**
|
|
* String containing the file path where data should be saved.
|
|
*/
|
|
path: "",
|
|
|
|
/**
|
|
* This function is called with a Download object as its first argument, and
|
|
* should return true if the item should be saved.
|
|
*/
|
|
onsaveitem: () => true,
|
|
|
|
/**
|
|
* Loads persistent downloads from the file to the list.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the operation finished successfully.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
load: function DS_load() {
|
|
return (async () => {
|
|
let bytes;
|
|
try {
|
|
bytes = await IOUtils.read(this.path);
|
|
} catch (ex) {
|
|
if (!(ex.name == "NotFoundError")) {
|
|
throw ex;
|
|
}
|
|
// If the file does not exist, there are no downloads to load.
|
|
return;
|
|
}
|
|
|
|
// Set this to true when we make changes to the download list that should
|
|
// be reflected in the file again.
|
|
let storeChanges = false;
|
|
let removePromises = [];
|
|
let storeData = JSON.parse(lazy.gTextDecoder.decode(bytes));
|
|
|
|
// Create live downloads based on the static snapshot.
|
|
for (let downloadData of storeData.list) {
|
|
try {
|
|
let download = await lazy.Downloads.createDownload(downloadData);
|
|
|
|
// Insecure downloads that have not been dealt with on shutdown should
|
|
// get cleaned up and removed from the download list on restart unless
|
|
// they are very new
|
|
if (
|
|
download.error?.becauseBlockedByReputationCheck &&
|
|
download.error.reputationCheckVerdict == "Insecure" &&
|
|
Date.now() - download.startTime > MAX_INSECURE_DOWNLOAD_AGE_MS
|
|
) {
|
|
removePromises.push(download.removePartialData());
|
|
storeChanges = true;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
if (!download.succeeded && !download.canceled && !download.error) {
|
|
// Try to restart the download if it was in progress during the
|
|
// previous session. Ignore errors.
|
|
download.start().catch(() => {});
|
|
} else {
|
|
// If the download was not in progress, try to update the current
|
|
// progress from disk. This is relevant in case we retained
|
|
// partially downloaded data.
|
|
await download.refresh();
|
|
}
|
|
} finally {
|
|
// Add the download to the list if we succeeded in creating it,
|
|
// after we have updated its initial state.
|
|
await this.list.add(download);
|
|
}
|
|
} catch (ex) {
|
|
// If an item is unrecognized, don't prevent others from being loaded.
|
|
console.error(ex);
|
|
}
|
|
}
|
|
|
|
if (storeChanges) {
|
|
try {
|
|
await Promise.all(removePromises);
|
|
await this.save();
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
})();
|
|
},
|
|
|
|
/**
|
|
* Saves persistent downloads from the list to the file.
|
|
*
|
|
* If an error occurs, the previous file is not deleted.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves When the operation finished successfully.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
save: function DS_save() {
|
|
return (async () => {
|
|
let downloads = await this.list.getAll();
|
|
|
|
// Take a static snapshot of the current state of all the downloads.
|
|
let storeData = { list: [] };
|
|
let atLeastOneDownload = false;
|
|
for (let download of downloads) {
|
|
try {
|
|
if (!this.onsaveitem(download)) {
|
|
continue;
|
|
}
|
|
|
|
let serializable = download.toSerializable();
|
|
if (!serializable) {
|
|
// This item cannot be persisted across sessions.
|
|
continue;
|
|
}
|
|
storeData.list.push(serializable);
|
|
atLeastOneDownload = true;
|
|
} catch (ex) {
|
|
// If an item cannot be converted to a serializable form, don't
|
|
// prevent others from being saved.
|
|
console.error(ex);
|
|
}
|
|
}
|
|
|
|
if (atLeastOneDownload) {
|
|
// Create or overwrite the file if there are downloads to save.
|
|
let bytes = lazy.gTextEncoder.encode(JSON.stringify(storeData));
|
|
await IOUtils.write(this.path, bytes, {
|
|
tmpPath: this.path + ".tmp",
|
|
});
|
|
} else {
|
|
// Remove the file if there are no downloads to save at all.
|
|
try {
|
|
await IOUtils.remove(this.path);
|
|
} catch (ex) {
|
|
if (!(ex.name == "NotFoundError" || ex.name == "NotAllowedError")) {
|
|
throw ex;
|
|
}
|
|
// On Windows, we may get an access denied error instead of a no such
|
|
// file error if the file existed before, and was recently deleted.
|
|
}
|
|
}
|
|
})();
|
|
},
|
|
};
|