Index: lib/filterStorage.js |
=================================================================== |
--- a/lib/filterStorage.js |
+++ b/lib/filterStorage.js |
@@ -17,26 +17,22 @@ |
"use strict"; |
/** |
* @fileOverview FilterStorage class responsible for managing user's |
* subscriptions and filters. |
*/ |
-const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {}); |
-const {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); |
- |
const {IO} = require("io"); |
const {Prefs} = require("prefs"); |
const {Filter, ActiveFilter} = require("filterClasses"); |
const {Subscription, SpecialSubscription, |
ExternalSubscription} = require("subscriptionClasses"); |
const {FilterNotifier} = require("filterNotifier"); |
-const {Utils} = require("utils"); |
/** |
* Version number of the filter storage file format. |
* @type {number} |
*/ |
let formatVersion = 4; |
/** |
@@ -57,59 +53,22 @@ let FilterStorage = exports.FilterStorag |
* @type {number} |
*/ |
get formatVersion() |
{ |
return formatVersion; |
}, |
/** |
- * File that the filter list has been loaded from and should be saved to |
- * @type {nsIFile} |
+ * File containing the filter list |
+ * @type {string} |
*/ |
get sourceFile() |
{ |
- let file = null; |
- if (Prefs.patternsfile) |
- { |
- // Override in place, use it instead of placing the file in the |
- // regular data dir |
- file = IO.resolveFilePath(Prefs.patternsfile); |
- } |
- if (!file) |
- { |
- // Place the file in the data dir |
- file = IO.resolveFilePath(Prefs.data_directory); |
- if (file) |
- file.append("patterns.ini"); |
- } |
- if (!file) |
- { |
- // Data directory pref misconfigured? Try the default value |
- try |
- { |
- let dir = Services.prefs.getDefaultBranch("extensions.adblockplus.") |
- .getCharPref("data_directory"); |
- file = IO.resolveFilePath(dir); |
- if (file) |
- file.append("patterns.ini"); |
- } |
- catch (e) {} |
- } |
- |
- if (!file) |
- { |
- Cu.reportError("Adblock Plus: Failed to resolve filter file location " + |
- "from extensions.adblockplus.patternsfile preference"); |
- } |
- |
- // Property is configurable because of the test suite. |
- Object.defineProperty(this, "sourceFile", |
- {value: file, configurable: true}); |
- return file; |
+ return "patterns.ini"; |
}, |
/** |
* Will be set to true if no patterns.ini file exists. |
* @type {boolean} |
*/ |
firstRun: false, |
@@ -404,23 +363,25 @@ let FilterStorage = exports.FilterStorag |
/** |
* @callback TextSink |
* @param {string?} line |
*/ |
/** |
* Allows importing previously serialized filter data. |
+ * @param {boolean} silent |
+ * If true, no "load" notification will be sent out. |
* @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() |
+ importData(silent) |
{ |
let parser = new INIParser(); |
return line => |
{ |
parser.process(line); |
if (line === null) |
{ |
// Old special groups might have been converted, remove them if |
@@ -447,105 +408,97 @@ let FilterStorage = exports.FilterStorag |
Subscription.knownSubscriptions = parser.knownSubscriptions; |
if (parser.userFilters) |
{ |
for (let filter of parser.userFilters) |
this.addFilter(Filter.fromText(filter), null, undefined, true); |
} |
- FilterNotifier.triggerListeners("load"); |
+ if (!silent) |
+ FilterNotifier.triggerListeners("load"); |
} |
}; |
}, |
/** |
- * Loads all subscriptions from the disk |
+ * Loads all subscriptions from the disk. |
+ * @return {Promise} promise resolved or rejected when loading is complete |
*/ |
loadFromDisk() |
{ |
- let readFile = () => |
- { |
- let parser = { |
- process: this.importData() |
- }; |
- IO.readFromFile(this.sourceFile, parser, readFromFileException => |
- { |
- this.initialized = true; |
- |
- 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) |
- tryBackup(1); |
- }); |
- }; |
- |
let tryBackup = backupIndex => |
{ |
- this.restoreBackup(backupIndex).then(() => |
+ return this.restoreBackup(backupIndex, true).then(() => |
{ |
if (this.subscriptions.length == 0) |
- tryBackup(backupIndex + 1); |
+ return tryBackup(backupIndex + 1); |
}).catch(error => |
{ |
// Give up |
}); |
}; |
- IO.statFile(this.sourceFile, (statError, statData) => |
+ return IO.statFile(this.sourceFile).then(statData => |
{ |
- if (statError || !statData.exists) |
+ if (!statData.exists) |
{ |
this.firstRun = true; |
- this.initialized = true; |
- FilterNotifier.triggerListeners("load"); |
+ return; |
} |
- else |
- readFile(); |
+ |
+ let parser = this.importData(true); |
+ return IO.readFromFile(this.sourceFile, parser).then(() => |
+ { |
+ parser(null); |
+ if (this.subscriptions.length == 0) |
+ { |
+ // No filter subscriptions in the file, this isn't right. |
+ throw new Error("No data in the file"); |
+ } |
+ }); |
+ }).catch(error => |
+ { |
+ Cu.reportError(error); |
+ return tryBackup(1); |
kzar
2017/04/20 06:49:25
So we return a promise here, which might recursive
Wladimir Palant
2017/04/20 07:17:06
Yes, this is the intended behavior - we already re
|
+ }).then(() => |
+ { |
+ this.initialized = true; |
+ FilterNotifier.triggerListeners("load"); |
}); |
}, |
/** |
+ * Constructs the file name for a patterns.ini backup. |
+ * @param {number} backupIndex |
+ * number of the backup file (1 being the most recent) |
+ * @return {string} backup file name |
+ */ |
+ getBackupName(backupIndex) |
+ { |
+ let [name, extension] = this.sourceFile.split(".", 2); |
+ return (name + "-backup" + backupIndex + "." + extension); |
+ }, |
+ |
+ /** |
* Restores an automatically created backup. |
* @param {number} backupIndex |
* number of the backup to restore (1 being the most recent) |
+ * @param {boolean} silent |
+ * If true, no "load" notification will be sent out. |
* @return {Promise} promise resolved or rejected when restoring is complete |
*/ |
- restoreBackup(backupIndex) |
+ restoreBackup(backupIndex, silent) |
{ |
- return new Promise((resolve, reject) => |
+ let backupFile = this.getBackupName(backupIndex); |
+ let parser = this.importData(silent); |
+ return IO.readFromFile(backupFile, parser).then(() => |
{ |
- // Attempt to load a backup |
- let [, part1, part2] = /^(.*)(\.\w+)$/.exec( |
- 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(); |
- } |
- }); |
+ parser(null); |
+ return this.saveToDisk(); |
}); |
}, |
/** |
* Generator serializing filter data and yielding it line by line. |
*/ |
*exportData() |
{ |
@@ -603,121 +556,100 @@ 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 |
+ * @return {Promise} promise resolved or rejected when saving is complete |
*/ |
saveToDisk() |
{ |
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.exportData(), e => |
- { |
- this._saving = false; |
- |
- if (e) |
- Cu.reportError(e); |
- |
- if (this._needsSave) |
- { |
- this._needsSave = false; |
- this.saveToDisk(); |
- } |
- else |
- FilterNotifier.triggerListeners("save"); |
- }); |
- }; |
- |
- let checkBackupRequired = (callbackNotRequired, callbackRequired) => |
- { |
- if (Prefs.patternsbackups <= 0) |
- callbackNotRequired(); |
- else |
- { |
- IO.statFile(targetFile, (statFileException, statData) => |
- { |
- if (statFileException || !statData.exists) |
- callbackNotRequired(); |
- else |
- { |
- let [, part1, part2] = /^(.*)(\.\w+)$/.exec(targetFile.leafName) || |
- [null, targetFile.leafName, ""]; |
- let newestBackup = targetFile.clone(); |
- newestBackup.leafName = part1 + "-backup1" + part2; |
- IO.statFile( |
- newestBackup, |
- (statBackupFileException, statBackupData) => |
- { |
- if (!statBackupFileException && (!statBackupData.exists || |
- (Date.now() - statBackupData.lastModified) / |
- 3600000 >= Prefs.patternsbackupinterval)) |
- { |
- callbackRequired(part1, part2); |
- } |
- else |
- callbackNotRequired(); |
- } |
- ); |
- } |
- }); |
- } |
- }; |
- |
- let removeLastBackup = (part1, part2) => |
- { |
- let file = targetFile.clone(); |
- file.leafName = part1 + "-backup" + Prefs.patternsbackups + part2; |
- IO.removeFile( |
- file, e => renameBackup(part1, part2, Prefs.patternsbackups - 1) |
- ); |
- }; |
- |
- let renameBackup = (part1, part2, index) => |
- { |
- if (index > 0) |
- { |
- let fromFile = targetFile.clone(); |
- fromFile.leafName = part1 + "-backup" + index + part2; |
- |
- let toName = part1 + "-backup" + (index + 1) + part2; |
- |
- IO.renameFile(fromFile, toName, e => renameBackup(part1, part2, |
- index - 1)); |
- } |
- else |
- { |
- let toFile = targetFile.clone(); |
- toFile.leafName = part1 + "-backup" + (index + 1) + part2; |
- |
- IO.copyFile(targetFile, toFile, writeFilters); |
- } |
- }; |
- |
this._saving = true; |
- checkBackupRequired(writeFilters, removeLastBackup); |
+ return Promise.resolve().then(() => |
+ { |
+ // First check whether we need to create a backup |
+ if (Prefs.patternsbackups <= 0) |
+ return false; |
+ |
+ return IO.statFile(this.sourceFile).then(statData => |
+ { |
+ if (!statData.exists) |
+ return false; |
+ |
+ return IO.statFile(this.getBackupName(1)).then(backupStatData => |
+ { |
+ if (backupStatData.exists && |
+ (Date.now() - backupStatData.lastModified) / 3600000 < |
+ Prefs.patternsbackupinterval) |
+ { |
+ return false; |
+ } |
+ return true; |
+ }); |
+ }); |
+ }).then(backupRequired => |
+ { |
+ if (!backupRequired) |
+ return; |
+ |
+ let ignoreErrors = error => |
+ { |
+ // Expected error, backup file doesn't exist. |
+ }; |
+ |
+ let renameBackup = index => |
+ { |
+ if (index > 0) |
+ { |
+ return IO.renameFile(this.getBackupName(index), |
+ this.getBackupName(index + 1)) |
+ .catch(ignoreErrors) |
+ .then(() => renameBackup(index - 1)); |
+ } |
+ |
+ return IO.renameFile(this.sourceFile, this.getBackupName(1)) |
+ .catch(ignoreErrors); |
+ }; |
+ |
+ // Rename existing files |
+ return renameBackup(Prefs.patternsbackups - 1); |
+ }).catch(error => |
+ { |
+ // Errors during backup creation shouldn't prevent writing filters. |
+ Cu.reportError(error); |
+ }).then(() => |
+ { |
+ return IO.writeToFile(this.sourceFile, this.exportData()); |
+ }).then(() => |
+ { |
+ FilterNotifier.triggerListeners("save"); |
+ }).catch(error => |
+ { |
+ // If saving failed, report error but continue - we still have to process |
+ // flags. |
+ Cu.reportError(error); |
+ }).then(() => |
+ { |
+ this._saving = false; |
+ if (this._needsSave) |
+ { |
+ this._needsSave = false; |
+ this.saveToDisk(); |
+ } |
+ }); |
}, |
/** |
* @typedef FileInfo |
* @type {object} |
* @property {nsIFile} file |
* @property {number} lastModified |
*/ |
@@ -725,42 +657,35 @@ let FilterStorage = exports.FilterStorag |
/** |
* Returns a promise resolving in a list of existing backup files. |
* @return {Promise.<FileInfo[]>} |
*/ |
getBackupFiles() |
{ |
let backups = []; |
- let [, part1, part2] = /^(.*)(\.\w+)$/.exec( |
- FilterStorage.sourceFile.leafName |
- ) || [null, FilterStorage.sourceFile.leafName, ""]; |
- |
- function checkBackupFile(index) |
+ let checkBackupFile = index => |
{ |
- return new Promise((resolve, reject) => |
+ return IO.statFile(this.getBackupName(index)).then(statData => |
{ |
- let file = FilterStorage.sourceFile.clone(); |
- file.leafName = part1 + "-backup" + index + part2; |
+ if (!statData.exists) |
+ return backups; |
- IO.statFile(file, (error, result) => |
- { |
- if (!error && result.exists) |
- { |
- backups.push({ |
- index, |
- lastModified: result.lastModified |
- }); |
- resolve(checkBackupFile(index + 1)); |
- } |
- else |
- resolve(backups); |
+ backups.push({ |
+ index, |
+ lastModified: statData.lastModified |
}); |
+ return checkBackupFile(index + 1); |
+ }).catch(error => |
+ { |
+ // Something went wrong, return whatever data we got so far. |
+ Cu.reportError(error); |
+ return backups; |
}); |
- } |
+ }; |
return checkBackupFile(1); |
} |
}; |
/** |
* Joins subscription's filters to the subscription without any notifications. |
* @param {Subscription} subscription |
@@ -789,17 +714,17 @@ function removeSubscriptionFilters(subsc |
{ |
let i = filter.subscriptions.indexOf(subscription); |
if (i >= 0) |
filter.subscriptions.splice(i, 1); |
} |
} |
/** |
- * IO.readFromFile() listener to parse filter data. |
+ * Listener returned by FilterStorage.importData(), parses filter data. |
* @constructor |
*/ |
function INIParser() |
{ |
this.fileProperties = this.curObj = {}; |
this.subscriptions = []; |
this.knownFilters = Object.create(null); |
this.knownSubscriptions = Object.create(null); |
@@ -892,16 +817,10 @@ INIParser.prototype = |
else if (this.wantObj === false && val) |
this.curObj.push(val.replace(/\\\[/g, "[")); |
} |
finally |
{ |
Filter.knownFilters = origKnownFilters; |
Subscription.knownSubscriptions = origKnownSubscriptions; |
} |
- |
- // Allow events to be processed every now and then. |
- // Note: IO.readFromFile() will deal with the potential reentrance here. |
- this.linesProcessed++; |
- if (this.linesProcessed % 1000 == 0) |
- return Utils.yield(); |
} |
}; |