mirror of https://github.com/roytam1/UXP.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
542 lines
19 KiB
542 lines
19 KiB
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- |
|
* 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/. */ |
|
|
|
this.EXPORTED_SYMBOLS = ["PlacesBackups"]; |
|
|
|
const Ci = Components.interfaces; |
|
const Cu = Components.utils; |
|
const Cc = Components.classes; |
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
Cu.import("resource://gre/modules/Services.jsm"); |
|
Cu.import("resource://gre/modules/PlacesUtils.jsm"); |
|
Cu.import("resource://gre/modules/Task.jsm"); |
|
Cu.import("resource://gre/modules/osfile.jsm"); |
|
Cu.import("resource://gre/modules/NetUtil.jsm"); |
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", |
|
"resource://gre/modules/BookmarkJSONUtils.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", |
|
"resource://gre/modules/Deprecated.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "OS", |
|
"resource://gre/modules/osfile.jsm"); |
|
|
|
XPCOMUtils.defineLazyGetter(this, "localFileCtor", |
|
() => Components.Constructor("@mozilla.org/file/local;1", |
|
"nsILocalFile", "initWithPath")); |
|
|
|
XPCOMUtils.defineLazyGetter(this, "filenamesRegex", |
|
() => /^bookmarks-([0-9-]+)(?:_([0-9]+)){0,1}(?:_([a-z0-9=+-]{24})){0,1}\.(json(lz4)?)$/i |
|
); |
|
|
|
/** |
|
* Appends meta-data information to a given filename. |
|
*/ |
|
function appendMetaDataToFilename(aFilename, aMetaData) { |
|
let matches = aFilename.match(filenamesRegex); |
|
return "bookmarks-" + matches[1] + |
|
"_" + aMetaData.count + |
|
"_" + aMetaData.hash + |
|
"." + matches[4]; |
|
} |
|
|
|
/** |
|
* Gets the hash from a backup filename. |
|
* |
|
* @return the extracted hash or null. |
|
*/ |
|
function getHashFromFilename(aFilename) { |
|
let matches = aFilename.match(filenamesRegex); |
|
if (matches && matches[3]) |
|
return matches[3]; |
|
return null; |
|
} |
|
|
|
/** |
|
* Given two filenames, checks if they contain the same date. |
|
*/ |
|
function isFilenameWithSameDate(aSourceName, aTargetName) { |
|
let sourceMatches = aSourceName.match(filenamesRegex); |
|
let targetMatches = aTargetName.match(filenamesRegex); |
|
|
|
return sourceMatches && targetMatches && |
|
sourceMatches[1] == targetMatches[1]; |
|
} |
|
|
|
/** |
|
* Given a filename, searches for another backup with the same date. |
|
* |
|
* @return OS.File path string or null. |
|
*/ |
|
function getBackupFileForSameDate(aFilename) { |
|
return Task.spawn(function* () { |
|
let backupFiles = yield PlacesBackups.getBackupFiles(); |
|
for (let backupFile of backupFiles) { |
|
if (isFilenameWithSameDate(OS.Path.basename(backupFile), aFilename)) |
|
return backupFile; |
|
} |
|
return null; |
|
}); |
|
} |
|
|
|
this.PlacesBackups = { |
|
/** |
|
* Matches the backup filename: |
|
* 0: file name |
|
* 1: date in form Y-m-d |
|
* 2: bookmarks count |
|
* 3: contents hash |
|
* 4: file extension |
|
*/ |
|
get filenamesRegex() { |
|
return filenamesRegex; |
|
}, |
|
|
|
get folder() { |
|
Deprecated.warning( |
|
"PlacesBackups.folder is deprecated and will be removed in a future version", |
|
"https://bugzilla.mozilla.org/show_bug.cgi?id=859695"); |
|
return this._folder; |
|
}, |
|
|
|
/** |
|
* This exists just to avoid spamming deprecate warnings from internal calls |
|
* needed to support deprecated methods themselves. |
|
*/ |
|
get _folder() { |
|
let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile); |
|
bookmarksBackupDir.append(this.profileRelativeFolderPath); |
|
if (!bookmarksBackupDir.exists()) { |
|
bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8)); |
|
if (!bookmarksBackupDir.exists()) |
|
throw ("Unable to create bookmarks backup folder"); |
|
} |
|
delete this._folder; |
|
return this._folder = bookmarksBackupDir; |
|
}, |
|
|
|
/** |
|
* Gets backup folder asynchronously. |
|
* @return {Promise} |
|
* @resolve the folder (the folder string path). |
|
*/ |
|
getBackupFolder: function PB_getBackupFolder() { |
|
return Task.spawn(function* () { |
|
if (this._backupFolder) { |
|
return this._backupFolder; |
|
} |
|
let profileDir = OS.Constants.Path.profileDir; |
|
let backupsDirPath = OS.Path.join(profileDir, this.profileRelativeFolderPath); |
|
yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true }); |
|
return this._backupFolder = backupsDirPath; |
|
}.bind(this)); |
|
}, |
|
|
|
get profileRelativeFolderPath() { |
|
return "bookmarkbackups"; |
|
}, |
|
|
|
/** |
|
* Cache current backups in a sorted (by date DESC) array. |
|
*/ |
|
get entries() { |
|
Deprecated.warning( |
|
"PlacesBackups.entries is deprecated and will be removed in a future version", |
|
"https://bugzilla.mozilla.org/show_bug.cgi?id=859695"); |
|
return this._entries; |
|
}, |
|
|
|
/** |
|
* This exists just to avoid spamming deprecate warnings from internal calls |
|
* needed to support deprecated methods themselves. |
|
*/ |
|
get _entries() { |
|
delete this._entries; |
|
this._entries = []; |
|
let files = this._folder.directoryEntries; |
|
while (files.hasMoreElements()) { |
|
let entry = files.getNext().QueryInterface(Ci.nsIFile); |
|
// A valid backup is any file that matches either the localized or |
|
// not-localized filename (bug 445704). |
|
if (!entry.isHidden() && filenamesRegex.test(entry.leafName)) { |
|
// Remove bogus backups in future dates. |
|
if (this.getDateForFile(entry) > new Date()) { |
|
entry.remove(false); |
|
continue; |
|
} |
|
this._entries.push(entry); |
|
} |
|
} |
|
this._entries.sort((a, b) => { |
|
let aDate = this.getDateForFile(a); |
|
let bDate = this.getDateForFile(b); |
|
return bDate - aDate; |
|
}); |
|
return this._entries; |
|
}, |
|
|
|
/** |
|
* Cache current backups in a sorted (by date DESC) array. |
|
* @return {Promise} |
|
* @resolve a sorted array of string paths. |
|
*/ |
|
getBackupFiles: function PB_getBackupFiles() { |
|
return Task.spawn(function* () { |
|
if (this._backupFiles) |
|
return this._backupFiles; |
|
|
|
this._backupFiles = []; |
|
|
|
let backupFolderPath = yield this.getBackupFolder(); |
|
let iterator = new OS.File.DirectoryIterator(backupFolderPath); |
|
yield iterator.forEach(function(aEntry) { |
|
// Since this is a lazy getter and OS.File I/O is serialized, we can |
|
// safely remove .tmp files without risking to remove ongoing backups. |
|
if (aEntry.name.endsWith(".tmp")) { |
|
OS.File.remove(aEntry.path); |
|
return undefined; |
|
} |
|
|
|
if (filenamesRegex.test(aEntry.name)) { |
|
// Remove bogus backups in future dates. |
|
let filePath = aEntry.path; |
|
if (this.getDateForFile(filePath) > new Date()) { |
|
return OS.File.remove(filePath); |
|
} |
|
this._backupFiles.push(filePath); |
|
} |
|
|
|
return undefined; |
|
}.bind(this)); |
|
iterator.close(); |
|
|
|
this._backupFiles.sort((a, b) => { |
|
let aDate = this.getDateForFile(a); |
|
let bDate = this.getDateForFile(b); |
|
return bDate - aDate; |
|
}); |
|
|
|
return this._backupFiles; |
|
}.bind(this)); |
|
}, |
|
|
|
/** |
|
* Generates a ISO date string (YYYY-MM-DD) from a Date object. |
|
* |
|
* @param dateObj |
|
* The date object to parse. |
|
* @return an ISO date string. |
|
*/ |
|
toISODateString: function toISODateString(dateObj) { |
|
if (!dateObj || dateObj.constructor.name != "Date" || !dateObj.getTime()) |
|
throw new Error("invalid date object"); |
|
let padDate = val => ("0" + val).substr(-2, 2); |
|
return [ |
|
dateObj.getFullYear(), |
|
padDate(dateObj.getMonth() + 1), |
|
padDate(dateObj.getDate()) |
|
].join("-"); |
|
}, |
|
|
|
/** |
|
* Creates a filename for bookmarks backup files. |
|
* |
|
* @param [optional] aDateObj |
|
* Date object used to build the filename. |
|
* Will use current date if empty. |
|
* @param [optional] bool - aCompress |
|
* Determines if file extension is json or jsonlz4 |
|
Default is json |
|
* @return A bookmarks backup filename. |
|
*/ |
|
getFilenameForDate: function PB_getFilenameForDate(aDateObj, aCompress) { |
|
let dateObj = aDateObj || new Date(); |
|
// Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters |
|
// and makes the alphabetical order of multiple backup files more useful. |
|
return "bookmarks-" + PlacesBackups.toISODateString(dateObj) + ".json" + |
|
(aCompress ? "lz4" : ""); |
|
}, |
|
|
|
/** |
|
* Creates a Date object from a backup file. The date is the backup |
|
* creation date. |
|
* |
|
* @param aBackupFile |
|
* nsIFile or string path of the backup. |
|
* @return A Date object for the backup's creation time. |
|
*/ |
|
getDateForFile: function PB_getDateForFile(aBackupFile) { |
|
let filename = (aBackupFile instanceof Ci.nsIFile) ? aBackupFile.leafName |
|
: OS.Path.basename(aBackupFile); |
|
let matches = filename.match(filenamesRegex); |
|
if (!matches) |
|
throw ("Invalid backup file name: " + filename); |
|
return new Date(matches[1].replace(/-/g, "/")); |
|
}, |
|
|
|
/** |
|
* Get the most recent backup file. |
|
* |
|
* @returns nsIFile backup file |
|
*/ |
|
getMostRecent: function PB_getMostRecent() { |
|
Deprecated.warning( |
|
"PlacesBackups.getMostRecent is deprecated and will be removed in a future version", |
|
"https://bugzilla.mozilla.org/show_bug.cgi?id=859695"); |
|
|
|
for (let i = 0; i < this._entries.length; i++) { |
|
let rx = /\.json(lz4)?$/; |
|
if (this._entries[i].leafName.match(rx)) |
|
return this._entries[i]; |
|
} |
|
return null; |
|
}, |
|
|
|
/** |
|
* Get the most recent backup file. |
|
* |
|
* @return {Promise} |
|
* @result the path to the file. |
|
*/ |
|
getMostRecentBackup: function PB_getMostRecentBackup() { |
|
return Task.spawn(function* () { |
|
let entries = yield this.getBackupFiles(); |
|
for (let entry of entries) { |
|
let rx = /\.json(lz4)?$/; |
|
if (OS.Path.basename(entry).match(rx)) { |
|
return entry; |
|
} |
|
} |
|
return null; |
|
}.bind(this)); |
|
}, |
|
|
|
/** |
|
* Serializes bookmarks using JSON, and writes to the supplied file. |
|
* Note: any item that should not be backed up must be annotated with |
|
* "places/excludeFromBackup". |
|
* |
|
* @param aFilePath |
|
* OS.File path for the "bookmarks.json" file to be created. |
|
* @return {Promise} |
|
* @resolves the number of serialized uri nodes. |
|
* @deprecated passing an nsIFile is deprecated |
|
*/ |
|
saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFilePath) { |
|
if (aFilePath instanceof Ci.nsIFile) { |
|
Deprecated.warning("Passing an nsIFile to PlacesBackups.saveBookmarksToJSONFile " + |
|
"is deprecated. Please use an OS.File path instead.", |
|
"https://developer.mozilla.org/docs/JavaScript_OS.File"); |
|
aFilePath = aFilePath.path; |
|
} |
|
return Task.spawn(function* () { |
|
let { count: nodeCount, hash: hash } = |
|
yield BookmarkJSONUtils.exportToFile(aFilePath); |
|
|
|
let backupFolderPath = yield this.getBackupFolder(); |
|
if (OS.Path.dirname(aFilePath) == backupFolderPath) { |
|
// We are creating a backup in the default backups folder, |
|
// so just update the internal cache. |
|
this._entries.unshift(new localFileCtor(aFilePath)); |
|
if (!this._backupFiles) { |
|
yield this.getBackupFiles(); |
|
} |
|
this._backupFiles.unshift(aFilePath); |
|
} else { |
|
// If we are saving to a folder different than our backups folder, then |
|
// we also want to create a new compressed version in it. |
|
// This way we ensure the latest valid backup is the same saved by the |
|
// user. See bug 424389. |
|
let mostRecentBackupFile = yield this.getMostRecentBackup(); |
|
if (!mostRecentBackupFile || |
|
hash != getHashFromFilename(OS.Path.basename(mostRecentBackupFile))) { |
|
let name = this.getFilenameForDate(undefined, true); |
|
let newFilename = appendMetaDataToFilename(name, |
|
{ count: nodeCount, |
|
hash: hash }); |
|
let newFilePath = OS.Path.join(backupFolderPath, newFilename); |
|
let backupFile = yield getBackupFileForSameDate(name); |
|
if (backupFile) { |
|
// There is already a backup for today, replace it. |
|
yield OS.File.remove(backupFile, { ignoreAbsent: true }); |
|
if (!this._backupFiles) |
|
yield this.getBackupFiles(); |
|
else |
|
this._backupFiles.shift(); |
|
this._backupFiles.unshift(newFilePath); |
|
} else { |
|
// There is no backup for today, add the new one. |
|
this._entries.unshift(new localFileCtor(newFilePath)); |
|
if (!this._backupFiles) |
|
yield this.getBackupFiles(); |
|
this._backupFiles.unshift(newFilePath); |
|
} |
|
let jsonString = yield OS.File.read(aFilePath); |
|
yield OS.File.writeAtomic(newFilePath, jsonString, { compression: "lz4" }); |
|
} |
|
} |
|
|
|
return nodeCount; |
|
}.bind(this)); |
|
}, |
|
|
|
/** |
|
* Creates a dated backup in <profile>/bookmarkbackups. |
|
* Stores the bookmarks using a lz4 compressed JSON file. |
|
* Note: any item that should not be backed up must be annotated with |
|
* "places/excludeFromBackup". |
|
* |
|
* @param [optional] int aMaxBackups |
|
* The maximum number of backups to keep. If set to 0 |
|
* all existing backups are removed and aForceBackup is |
|
* ignored, so a new one won't be created. |
|
* @param [optional] bool aForceBackup |
|
* Forces creating a backup even if one was already |
|
* created that day (overwrites). |
|
* @return {Promise} |
|
*/ |
|
create: function PB_create(aMaxBackups, aForceBackup) { |
|
let limitBackups = function* () { |
|
let backupFiles = yield this.getBackupFiles(); |
|
if (typeof aMaxBackups == "number" && aMaxBackups > -1 && |
|
backupFiles.length >= aMaxBackups) { |
|
let numberOfBackupsToDelete = backupFiles.length - aMaxBackups; |
|
while (numberOfBackupsToDelete--) { |
|
this._entries.pop(); |
|
let oldestBackup = this._backupFiles.pop(); |
|
yield OS.File.remove(oldestBackup); |
|
} |
|
} |
|
}.bind(this); |
|
|
|
return Task.spawn(function* () { |
|
if (aMaxBackups === 0) { |
|
// Backups are disabled, delete any existing one and bail out. |
|
yield limitBackups(0); |
|
return; |
|
} |
|
|
|
// Ensure to initialize _backupFiles |
|
if (!this._backupFiles) |
|
yield this.getBackupFiles(); |
|
let newBackupFilename = this.getFilenameForDate(undefined, true); |
|
// If we already have a backup for today we should do nothing, unless we |
|
// were required to enforce a new backup. |
|
let backupFile = yield getBackupFileForSameDate(newBackupFilename); |
|
if (backupFile && !aForceBackup) |
|
return; |
|
|
|
if (backupFile) { |
|
// In case there is a backup for today we should recreate it. |
|
this._backupFiles.shift(); |
|
this._entries.shift(); |
|
yield OS.File.remove(backupFile, { ignoreAbsent: true }); |
|
} |
|
|
|
// Now check the hash of the most recent backup, and try to create a new |
|
// backup, if that fails due to hash conflict, just rename the old backup. |
|
let mostRecentBackupFile = yield this.getMostRecentBackup(); |
|
let mostRecentHash = mostRecentBackupFile && |
|
getHashFromFilename(OS.Path.basename(mostRecentBackupFile)); |
|
|
|
// Save bookmarks to a backup file. |
|
let backupFolder = yield this.getBackupFolder(); |
|
let newBackupFile = OS.Path.join(backupFolder, newBackupFilename); |
|
let newFilenameWithMetaData; |
|
try { |
|
let { count: nodeCount, hash: hash } = |
|
yield BookmarkJSONUtils.exportToFile(newBackupFile, |
|
{ compress: true, |
|
failIfHashIs: mostRecentHash }); |
|
newFilenameWithMetaData = appendMetaDataToFilename(newBackupFilename, |
|
{ count: nodeCount, |
|
hash: hash }); |
|
} catch (ex) { |
|
if (!ex.becauseSameHash) { |
|
throw ex; |
|
} |
|
// The last backup already contained up-to-date information, just |
|
// rename it as if it was today's backup. |
|
this._backupFiles.shift(); |
|
this._entries.shift(); |
|
newBackupFile = mostRecentBackupFile; |
|
// Ensure we retain the proper extension when renaming |
|
// the most recent backup file. |
|
if (/\.json$/.test(OS.Path.basename(mostRecentBackupFile))) |
|
newBackupFilename = this.getFilenameForDate(); |
|
newFilenameWithMetaData = appendMetaDataToFilename( |
|
newBackupFilename, |
|
{ count: this.getBookmarkCountForFile(mostRecentBackupFile), |
|
hash: mostRecentHash }); |
|
} |
|
|
|
// Append metadata to the backup filename. |
|
let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData); |
|
yield OS.File.move(newBackupFile, newBackupFileWithMetadata); |
|
this._entries.unshift(new localFileCtor(newBackupFileWithMetadata)); |
|
this._backupFiles.unshift(newBackupFileWithMetadata); |
|
|
|
// Limit the number of backups. |
|
yield limitBackups(aMaxBackups); |
|
}.bind(this)); |
|
}, |
|
|
|
/** |
|
* Gets the bookmark count for backup file. |
|
* |
|
* @param aFilePath |
|
* File path The backup file. |
|
* |
|
* @return the bookmark count or null. |
|
*/ |
|
getBookmarkCountForFile: function PB_getBookmarkCountForFile(aFilePath) { |
|
let count = null; |
|
let filename = OS.Path.basename(aFilePath); |
|
let matches = filename.match(filenamesRegex); |
|
if (matches && matches[2]) |
|
count = matches[2]; |
|
return count; |
|
}, |
|
|
|
/** |
|
* Gets a bookmarks tree representation usable to create backups in different |
|
* file formats. The root or the tree is PlacesUtils.placesRootId. |
|
* Items annotated with PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO and all of their |
|
* descendants are excluded. |
|
* |
|
* @return an object representing a tree with the places root as its root. |
|
* Each bookmark is represented by an object having these properties: |
|
* * id: the item id (make this not enumerable after bug 824502) |
|
* * title: the title |
|
* * guid: unique id |
|
* * parent: item id of the parent folder, not enumerable |
|
* * index: the position in the parent |
|
* * dateAdded: microseconds from the epoch |
|
* * lastModified: microseconds from the epoch |
|
* * type: type of the originating node as defined in PlacesUtils |
|
* The following properties exist only for a subset of bookmarks: |
|
* * annos: array of annotations |
|
* * uri: url |
|
* * iconuri: favicon's url |
|
* * keyword: associated keyword |
|
* * charset: last known charset |
|
* * tags: csv string of tags |
|
* * root: string describing whether this represents a root |
|
* * children: array of child items in a folder |
|
*/ |
|
getBookmarksTree: Task.async(function* () { |
|
let startTime = Date.now(); |
|
let root = yield PlacesUtils.promiseBookmarksTree(PlacesUtils.bookmarks.rootGuid, { |
|
excludeItemsCallback: aItem => { |
|
return aItem.annos && |
|
aItem.annos.find(a => a.name == PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO); |
|
}, |
|
includeItemIds: true |
|
}); |
|
|
|
return [root, root.itemsCount]; |
|
}) |
|
} |
|
|
|
|