| Index: lib/filterStorage.js |
| =================================================================== |
| --- a/lib/filterStorage.js |
| +++ b/lib/filterStorage.js |
| @@ -391,183 +391,200 @@ let FilterStorage = exports.FilterStorag |
| } |
| for (let filter of filters) |
| { |
| filter.hitCount = 0; |
| filter.lastHit = 0; |
| } |
| }, |
| - _loading: false, |
| + /** |
| + * @callback TextSink |
| + * @param {string?} line |
| + */ |
| + |
| + /** |
| + * Allows importing previously serialized filter data. |
| + * @return {TextSink} |
| + * Function to be called for each line of data. Calling it with null as |
| + * parameter finalizes the import and replaces existing data. No changes |
| + * will be applied before finalization, so import can be "aborted" by |
| + * forgetting this callback. |
| + */ |
| + importData() |
| + { |
| + let parser = new INIParser(); |
| + return line => |
| + { |
| + parser.process(line); |
|
kzar
2017/03/30 12:58:48
I wonder why we parser.process the final null?
Wladimir Palant
2017/03/30 13:02:06
The parser uses it to flush out whatever object wa
kzar
2017/03/30 13:10:54
Acknowledged.
|
| + if (line === null) |
| + { |
| + // Old special groups might have been converted, remove them if |
| + // they are empty |
| + let specialMap = new Set(["~il~", "~wl~", "~fl~", "~eh~"]); |
| + let knownSubscriptions = Object.create(null); |
| + for (let i = 0; i < parser.subscriptions.length; i++) |
| + { |
| + let subscription = parser.subscriptions[i]; |
| + if (subscription instanceof SpecialSubscription && |
| + subscription.filters.length == 0 && |
| + specialMap.has(subscription.url)) |
| + { |
| + parser.subscriptions.splice(i--, 1); |
| + } |
| + else |
| + knownSubscriptions[subscription.url] = subscription; |
| + } |
| + |
| + this.fileProperties = parser.fileProperties; |
| + this.subscriptions = parser.subscriptions; |
| + this.knownSubscriptions = knownSubscriptions; |
| + Filter.knownFilters = parser.knownFilters; |
| + Subscription.knownSubscriptions = parser.knownSubscriptions; |
| + |
| + if (parser.userFilters) |
| + { |
| + for (let filter of parser.userFilters) |
| + this.addFilter(Filter.fromText(filter), null, undefined, true); |
| + } |
| + |
| + FilterNotifier.triggerListeners("load"); |
| + } |
| + }; |
| + }, |
| /** |
| * Loads all subscriptions from the disk |
| - * @param {nsIFile} [sourceFile] File to read from |
| */ |
| - loadFromDisk(sourceFile) |
| + loadFromDisk() |
| { |
| - if (this._loading) |
| - return; |
| - |
| - this._loading = true; |
| - |
| - let readFile = (currentSourceFile, backupIndex) => |
| + let readFile = () => |
| { |
| - let parser = new INIParser(); |
| - IO.readFromFile(currentSourceFile, parser, readFromFileException => |
| + let parser = { |
| + process: this.importData() |
| + }; |
| + IO.readFromFile(this.sourceFile, parser, readFromFileException => |
| { |
| - if (!readFromFileException && parser.subscriptions.length == 0) |
| + if (!readFromFileException && this.subscriptions.length == 0) |
| { |
| // No filter subscriptions in the file, this isn't right. |
| readFromFileException = new Error("No data in the file"); |
| } |
| if (readFromFileException) |
| Cu.reportError(readFromFileException); |
| - if (readFromFileException && !explicitFile) |
| - { |
| - // Attempt to load a backup |
| - currentSourceFile = this.sourcefile; |
| - if (currentSourceFile) |
| - { |
| - let [, part1, part2] = /^(.*)(\.\w+)$/.exec( |
| - currentSourceFile.leafName |
| - ) || [null, currentSourceFile.leafName, ""]; |
| - |
| - currentSourceFile = currentSourceFile.clone(); |
| - currentSourceFile.leafName = ( |
| - part1 + "-backup" + (++backupIndex) + part2 |
| - ); |
| - |
| - IO.statFile(currentSourceFile, (statFileException, statData) => |
| - { |
| - if (!statFileException && statData.exists) |
| - readFile(currentSourceFile, backupIndex); |
| - else |
| - doneReading(parser); |
| - }); |
| - return; |
| - } |
| - } |
| - doneReading(parser); |
| + if (readFromFileException) |
| + tryBackup(1); |
| }); |
| }; |
| - let doneReading = parser => |
| + let tryBackup = backupIndex => |
| { |
| - // Old special groups might have been converted, remove them if |
| - // they are empty |
| - let specialMap = {"~il~": true, "~wl~": true, "~fl~": true, "~eh~": true}; |
| - let knownSubscriptions = Object.create(null); |
| - for (let i = 0; i < parser.subscriptions.length; i++) |
| + this.restoreBackup(backupIndex).then(() => |
| { |
| - let subscription = parser.subscriptions[i]; |
| - if (subscription instanceof SpecialSubscription && |
| - subscription.filters.length == 0 && subscription.url in specialMap) |
| - { |
| - parser.subscriptions.splice(i--, 1); |
| - } |
| - else |
| - knownSubscriptions[subscription.url] = subscription; |
| - } |
| - |
| - this.fileProperties = parser.fileProperties; |
| - this.subscriptions = parser.subscriptions; |
| - this.knownSubscriptions = knownSubscriptions; |
| - Filter.knownFilters = parser.knownFilters; |
| - Subscription.knownSubscriptions = parser.knownSubscriptions; |
| - |
| - if (parser.userFilters) |
| + if (this.subscriptions.length == 0) |
| + tryBackup(backupIndex + 1); |
| + }).catch(error => |
| { |
| - for (let i = 0; i < parser.userFilters.length; i++) |
| - { |
| - let filter = Filter.fromText(parser.userFilters[i]); |
| - this.addFilter(filter, null, undefined, true); |
| - } |
| - } |
| - |
| - this._loading = false; |
| - FilterNotifier.triggerListeners("load"); |
| - |
| - if (sourceFile != this.sourceFile) |
| - this.saveToDisk(); |
| + // Give up |
| + }); |
| }; |
| - let explicitFile; |
| - if (sourceFile) |
| + IO.statFile(this.sourceFile, (statError, statData) => |
| { |
| - explicitFile = true; |
| - readFile(sourceFile, 0); |
| - } |
| - else |
| - { |
| - explicitFile = false; |
| - ({sourceFile} = FilterStorage); |
| - |
| - let callback = function(e, statData) |
| + if (statError || !statData.exists) |
| { |
| - if (e || !statData.exists) |
| - { |
| - this.firstRun = true; |
| - this._loading = false; |
| - FilterNotifier.triggerListeners("load"); |
| - } |
| - else |
| - readFile(sourceFile, 0); |
| - }.bind(this); |
| - |
| - if (sourceFile) |
| - IO.statFile(sourceFile, callback); |
| + this.firstRun = true; |
| + FilterNotifier.triggerListeners("load"); |
| + } |
| else |
| - callback(true); |
| - } |
| + readFile(); |
| + }); |
| }, |
| - *_generateFilterData(subscriptions) |
| + /** |
| + * Restores an automatically created backup. |
| + * @param {number} backupIndex |
| + * number of the backup to restore (1 being the most recent) |
| + * @return {Promise} promise resolved or rejected when restoring is complete |
| + */ |
| + restoreBackup(backupIndex) |
| { |
| + return new Promise((resolve, reject) => |
| + { |
| + // Attempt to load a backup |
| + let [, part1, part2] = /^(.*)(\.\w+)$/.exec( |
|
kzar
2017/03/30 12:58:48
Well I just learned something, I didn't realise va
|
| + this.sourceFile.leafName |
| + ) || [null, this.sourceFile.leafName, ""]; |
| + |
| + let backupFile = this.sourceFile.clone(); |
| + backupFile.leafName = (part1 + "-backup" + backupIndex + part2); |
| + |
| + let parser = { |
| + process: this.importData() |
| + }; |
| + IO.readFromFile(backupFile, parser, error => |
| + { |
| + if (error) |
| + reject(error); |
| + else |
| + { |
| + this.saveToDisk(); |
| + resolve(); |
| + } |
| + }); |
| + }); |
| + }, |
| + |
| + /** |
| + * Generator serializing filter data and yielding it line by line. |
| + */ |
| + *exportData() |
| + { |
| + // Do not persist external subscriptions |
|
kzar
2017/03/30 12:58:48
Nit: I guess we could avoid doing this filtering u
Wladimir Palant
2017/03/30 13:02:06
Better not. This is creating a copy of the list fo
kzar
2017/03/30 13:10:54
Ah OK, makes sense.
|
| + let subscriptions = this.subscriptions.filter( |
| + s => !(s instanceof ExternalSubscription) |
| + ); |
| + |
| yield "# Adblock Plus preferences"; |
| yield "version=" + formatVersion; |
| - let saved = Object.create(null); |
| + let saved = new Set(); |
| let buf = []; |
| // Save filter data |
| - for (let i = 0; i < subscriptions.length; i++) |
| + for (let subscription of subscriptions) |
| { |
| - let subscription = subscriptions[i]; |
| - for (let j = 0; j < subscription.filters.length; j++) |
| + for (let filter of subscription.filters) |
| { |
| - let filter = subscription.filters[j]; |
| - if (!(filter.text in saved)) |
| + if (!saved.has(filter.text)) |
| { |
| filter.serialize(buf); |
| - saved[filter.text] = filter; |
| - for (let k = 0; k < buf.length; k++) |
| - yield buf[k]; |
| + saved.add(filter.text); |
| + for (let line of buf) |
| + yield line; |
| buf.splice(0); |
| } |
| } |
| } |
| // Save subscriptions |
| - for (let i = 0; i < subscriptions.length; i++) |
| + for (let subscription of subscriptions) |
| { |
| - let subscription = subscriptions[i]; |
| - |
| yield ""; |
| subscription.serialize(buf); |
| if (subscription.filters.length) |
| { |
| buf.push("", "[Subscription filters]"); |
| subscription.serializeFilters(buf); |
| } |
| - for (let k = 0; k < buf.length; k++) |
| - yield buf[k]; |
| + for (let line of buf) |
| + yield line; |
| buf.splice(0); |
| } |
| }, |
| /** |
| * Will be set to true if saveToDisk() is running (reentrance protection). |
| * @type {boolean} |
| */ |
| @@ -577,66 +594,56 @@ let FilterStorage = exports.FilterStorag |
| * Will be set to true if a saveToDisk() call arrives while saveToDisk() is |
| * already running (delayed execution). |
| * @type {boolean} |
| */ |
| _needsSave: false, |
| /** |
| * Saves all subscriptions back to disk |
| - * @param {nsIFile} [targetFile] File to be written |
| */ |
| - saveToDisk(targetFile) |
| + saveToDisk() |
| { |
| - let explicitFile = true; |
| - if (!targetFile) |
| - { |
| - targetFile = FilterStorage.sourceFile; |
| - explicitFile = false; |
| - } |
| - if (!targetFile) |
| - return; |
| - |
| - if (!explicitFile && this._saving) |
| + if (this._saving) |
| { |
| this._needsSave = true; |
| return; |
| } |
| // Make sure the file's parent directory exists |
| + let targetFile = this.sourceFile; |
| try |
| { |
| targetFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, |
| FileUtils.PERMS_DIRECTORY); |
| } |
| catch (e) {} |
| let writeFilters = () => |
| { |
| - IO.writeToFile(targetFile, this._generateFilterData(subscriptions), e => |
| + IO.writeToFile(targetFile, this.exportData(), e => |
| { |
| - if (!explicitFile) |
| - this._saving = false; |
| + this._saving = false; |
| if (e) |
| Cu.reportError(e); |
| - if (!explicitFile && this._needsSave) |
| + if (this._needsSave) |
| { |
| this._needsSave = false; |
| this.saveToDisk(); |
| } |
| else |
| FilterNotifier.triggerListeners("save"); |
| }); |
| }; |
| let checkBackupRequired = (callbackNotRequired, callbackRequired) => |
| { |
| - if (explicitFile || Prefs.patternsbackups <= 0) |
| + if (Prefs.patternsbackups <= 0) |
| callbackNotRequired(); |
| else |
| { |
| IO.statFile(targetFile, (statFileException, statData) => |
| { |
| if (statFileException || !statData.exists) |
| callbackNotRequired(); |
| else |
| @@ -689,22 +696,17 @@ let FilterStorage = exports.FilterStorag |
| { |
| let toFile = targetFile.clone(); |
| toFile.leafName = part1 + "-backup" + (index + 1) + part2; |
| IO.copyFile(targetFile, toFile, writeFilters); |
| } |
| }; |
| - // Do not persist external subscriptions |
| - let subscriptions = this.subscriptions.filter( |
| - s => !(s instanceof ExternalSubscription) |
| - ); |
| - if (!explicitFile) |
| - this._saving = true; |
| + this._saving = true; |
| checkBackupRequired(writeFilters, removeLastBackup); |
| }, |
| /** |
| * @typedef FileInfo |
| * @type {object} |
| * @property {nsIFile} file |
| @@ -730,17 +732,17 @@ let FilterStorage = exports.FilterStorag |
| let file = FilterStorage.sourceFile.clone(); |
| file.leafName = part1 + "-backup" + index + part2; |
| IO.statFile(file, (error, result) => |
| { |
| if (!error && result.exists) |
| { |
| backups.push({ |
| - file, |
| + index, |
| lastModified: result.lastModified |
| }); |
| resolve(checkBackupFile(index + 1)); |
| } |
| else |
| resolve(backups); |
| }); |
| }); |