2014-06-25 09:12:07 +04:00
|
|
|
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
2013-04-10 13:10:33 +04:00
|
|
|
* vim: sw=2 ts=2 sts=2 expandtab filetype=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/. */
|
|
|
|
|
|
|
|
this.EXPORTED_SYMBOLS = ["PlacesBackups"];
|
|
|
|
|
|
|
|
const Ci = Components.interfaces;
|
|
|
|
const Cu = Components.utils;
|
2013-10-07 10:52:08 +04:00
|
|
|
const Cc = Components.classes;
|
2013-04-10 13:10:33 +04:00
|
|
|
|
2013-07-31 18:51:09 +04:00
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
2013-04-10 13:10:33 +04:00
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
Cu.import("resource://gre/modules/PlacesUtils.jsm");
|
2013-10-07 10:52:08 +04:00
|
|
|
Cu.import("resource://gre/modules/osfile.jsm");
|
2014-02-04 17:43:20 +04:00
|
|
|
Cu.import("resource://gre/modules/NetUtil.jsm");
|
2013-04-10 13:10:33 +04:00
|
|
|
|
2014-04-16 05:20:28 +04:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
|
|
|
|
"resource://gre/modules/BookmarkJSONUtils.jsm");
|
2014-03-28 18:36:48 +04:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
|
|
|
|
"resource://gre/modules/Deprecated.jsm");
|
2013-07-31 18:51:09 +04:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
|
|
|
"resource://gre/modules/osfile.jsm");
|
2014-02-04 17:43:20 +04:00
|
|
|
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "localFileCtor",
|
|
|
|
() => Components.Constructor("@mozilla.org/file/local;1",
|
2017-08-04 11:49:22 +03:00
|
|
|
"nsIFile", "initWithPath"));
|
2013-07-31 18:51:09 +04:00
|
|
|
|
2014-03-19 17:33:02 +04:00
|
|
|
XPCOMUtils.defineLazyGetter(this, "filenamesRegex",
|
2015-01-15 14:25:00 +03:00
|
|
|
() => /^bookmarks-([0-9-]+)(?:_([0-9]+)){0,1}(?:_([a-z0-9=+-]{24})){0,1}\.(json(lz4)?)$/i
|
2014-03-19 17:33:02 +04:00
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 &&
|
2014-06-06 18:09:00 +04:00
|
|
|
sourceMatches[1] == targetMatches[1];
|
2014-03-19 17:33:02 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a filename, searches for another backup with the same date.
|
|
|
|
*
|
|
|
|
* @return OS.File path string or null.
|
|
|
|
*/
|
|
|
|
function getBackupFileForSameDate(aFilename) {
|
2017-05-12 15:42:39 +03:00
|
|
|
return (async function() {
|
|
|
|
let backupFiles = await PlacesBackups.getBackupFiles();
|
2014-03-19 17:33:02 +04:00
|
|
|
for (let backupFile of backupFiles) {
|
|
|
|
if (isFilenameWithSameDate(OS.Path.basename(backupFile), aFilename))
|
|
|
|
return backupFile;
|
|
|
|
}
|
|
|
|
return null;
|
2017-05-12 15:42:39 +03:00
|
|
|
})();
|
2014-03-19 17:33:02 +04:00
|
|
|
}
|
|
|
|
|
2017-05-12 10:04:51 +03:00
|
|
|
/**
|
|
|
|
* Returns the top-level bookmark folders ids and guids.
|
|
|
|
*
|
|
|
|
* @return {Promise} Resolve with an array of objects containing id and guid
|
|
|
|
* when the query is complete.
|
|
|
|
*/
|
|
|
|
async function getTopLevelFolderIds() {
|
|
|
|
let db = await PlacesUtils.promiseDBConnection();
|
|
|
|
let rows = await db.execute(
|
|
|
|
"SELECT id, guid FROM moz_bookmarks WHERE parent = :parentId",
|
|
|
|
{ parentId: PlacesUtils.placesRootId }
|
|
|
|
);
|
|
|
|
|
|
|
|
let guids = [];
|
|
|
|
for (let row of rows) {
|
|
|
|
guids.push({
|
|
|
|
id: row.getResultByName("id"),
|
|
|
|
guid: row.getResultByName("guid")
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return guids;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-04-10 13:10:33 +04:00
|
|
|
this.PlacesBackups = {
|
2014-03-19 17:33:02 +04:00
|
|
|
/**
|
|
|
|
* Matches the backup filename:
|
|
|
|
* 0: file name
|
|
|
|
* 1: date in form Y-m-d
|
|
|
|
* 2: bookmarks count
|
|
|
|
* 3: contents hash
|
|
|
|
* 4: file extension
|
|
|
|
*/
|
2015-09-24 14:32:23 +03:00
|
|
|
get filenamesRegex() {
|
|
|
|
return filenamesRegex;
|
|
|
|
},
|
2013-04-10 13:10:33 +04:00
|
|
|
|
|
|
|
get folder() {
|
2013-10-07 10:52:08 +04:00
|
|
|
Deprecated.warning(
|
|
|
|
"PlacesBackups.folder is deprecated and will be removed in a future version",
|
|
|
|
"https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
|
2014-02-04 17:43:20 +04:00
|
|
|
return this._folder;
|
|
|
|
},
|
2013-10-07 10:52:08 +04:00
|
|
|
|
2014-02-04 17:43:20 +04:00
|
|
|
/**
|
|
|
|
* This exists just to avoid spamming deprecate warnings from internal calls
|
|
|
|
* needed to support deprecated methods themselves.
|
|
|
|
*/
|
|
|
|
get _folder() {
|
2017-08-04 11:49:22 +03:00
|
|
|
let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
2013-04-17 10:08:11 +04:00
|
|
|
bookmarksBackupDir.append(this.profileRelativeFolderPath);
|
2013-04-10 13:10:33 +04:00
|
|
|
if (!bookmarksBackupDir.exists()) {
|
|
|
|
bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8));
|
|
|
|
if (!bookmarksBackupDir.exists())
|
2016-08-04 10:28:58 +03:00
|
|
|
throw ("Unable to create bookmarks backup folder");
|
2013-04-10 13:10:33 +04:00
|
|
|
}
|
2014-02-04 17:43:20 +04:00
|
|
|
delete this._folder;
|
|
|
|
return this._folder = bookmarksBackupDir;
|
2013-04-10 13:10:33 +04:00
|
|
|
},
|
|
|
|
|
2013-10-07 10:52:08 +04:00
|
|
|
/**
|
|
|
|
* Gets backup folder asynchronously.
|
|
|
|
* @return {Promise}
|
|
|
|
* @resolve the folder (the folder string path).
|
|
|
|
*/
|
|
|
|
getBackupFolder: function PB_getBackupFolder() {
|
2017-05-12 15:47:41 +03:00
|
|
|
return (async () => {
|
2014-02-04 17:43:20 +04:00
|
|
|
if (this._backupFolder) {
|
|
|
|
return this._backupFolder;
|
2013-10-07 10:52:08 +04:00
|
|
|
}
|
|
|
|
let profileDir = OS.Constants.Path.profileDir;
|
|
|
|
let backupsDirPath = OS.Path.join(profileDir, this.profileRelativeFolderPath);
|
2017-05-12 15:42:39 +03:00
|
|
|
await OS.File.makeDir(backupsDirPath, { ignoreExisting: true });
|
2014-02-04 17:43:20 +04:00
|
|
|
return this._backupFolder = backupsDirPath;
|
2017-05-12 15:47:41 +03:00
|
|
|
})();
|
2013-10-07 10:52:08 +04:00
|
|
|
},
|
|
|
|
|
2015-09-24 14:32:23 +03:00
|
|
|
get profileRelativeFolderPath() {
|
|
|
|
return "bookmarkbackups";
|
|
|
|
},
|
2013-04-17 10:08:11 +04:00
|
|
|
|
2013-04-10 13:10:33 +04:00
|
|
|
/**
|
|
|
|
* Cache current backups in a sorted (by date DESC) array.
|
|
|
|
*/
|
|
|
|
get entries() {
|
2013-10-07 10:52:08 +04:00
|
|
|
Deprecated.warning(
|
|
|
|
"PlacesBackups.entries is deprecated and will be removed in a future version",
|
|
|
|
"https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
|
2014-02-04 17:43:20 +04:00
|
|
|
return this._entries;
|
|
|
|
},
|
2013-10-07 10:52:08 +04:00
|
|
|
|
2014-02-04 17:43:20 +04:00
|
|
|
/**
|
|
|
|
* 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;
|
2013-04-10 13:10:33 +04:00
|
|
|
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).
|
2014-03-19 17:33:02 +04:00
|
|
|
if (!entry.isHidden() && filenamesRegex.test(entry.leafName)) {
|
2013-04-10 13:10:33 +04:00
|
|
|
// Remove bogus backups in future dates.
|
|
|
|
if (this.getDateForFile(entry) > new Date()) {
|
|
|
|
entry.remove(false);
|
|
|
|
continue;
|
|
|
|
}
|
2014-02-04 17:43:20 +04:00
|
|
|
this._entries.push(entry);
|
2013-04-10 13:10:33 +04:00
|
|
|
}
|
|
|
|
}
|
2014-02-04 17:43:20 +04:00
|
|
|
this._entries.sort((a, b) => {
|
2013-04-10 13:10:33 +04:00
|
|
|
let aDate = this.getDateForFile(a);
|
|
|
|
let bDate = this.getDateForFile(b);
|
2016-02-04 08:17:16 +03:00
|
|
|
return bDate - aDate;
|
2013-04-10 13:10:33 +04:00
|
|
|
});
|
2014-02-04 17:43:20 +04:00
|
|
|
return this._entries;
|
2013-04-10 13:10:33 +04:00
|
|
|
},
|
|
|
|
|
2013-10-07 10:52:08 +04:00
|
|
|
/**
|
|
|
|
* Cache current backups in a sorted (by date DESC) array.
|
|
|
|
* @return {Promise}
|
|
|
|
* @resolve a sorted array of string paths.
|
|
|
|
*/
|
|
|
|
getBackupFiles: function PB_getBackupFiles() {
|
2017-05-12 15:47:41 +03:00
|
|
|
return (async () => {
|
2014-02-04 17:43:20 +04:00
|
|
|
if (this._backupFiles)
|
|
|
|
return this._backupFiles;
|
|
|
|
|
2013-10-07 10:52:08 +04:00
|
|
|
this._backupFiles = [];
|
|
|
|
|
2017-05-12 15:42:39 +03:00
|
|
|
let backupFolderPath = await this.getBackupFolder();
|
2013-10-07 10:52:08 +04:00
|
|
|
let iterator = new OS.File.DirectoryIterator(backupFolderPath);
|
2017-05-12 15:42:39 +03:00
|
|
|
await iterator.forEach(aEntry => {
|
2014-02-14 18:42:35 +04:00
|
|
|
// 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);
|
2016-02-04 00:12:18 +03:00
|
|
|
return undefined;
|
2014-02-14 18:42:35 +04:00
|
|
|
}
|
|
|
|
|
2014-03-19 17:33:02 +04:00
|
|
|
if (filenamesRegex.test(aEntry.name)) {
|
2013-10-07 10:52:08 +04:00
|
|
|
// Remove bogus backups in future dates.
|
|
|
|
let filePath = aEntry.path;
|
|
|
|
if (this.getDateForFile(filePath) > new Date()) {
|
|
|
|
return OS.File.remove(filePath);
|
|
|
|
}
|
2016-08-04 01:54:59 +03:00
|
|
|
this._backupFiles.push(filePath);
|
2013-10-07 10:52:08 +04:00
|
|
|
}
|
2016-04-05 21:33:48 +03:00
|
|
|
|
|
|
|
return undefined;
|
2017-04-27 01:25:45 +03:00
|
|
|
});
|
2013-10-07 10:52:08 +04:00
|
|
|
iterator.close();
|
|
|
|
|
2014-02-04 17:43:20 +04:00
|
|
|
this._backupFiles.sort((a, b) => {
|
2013-10-07 10:52:08 +04:00
|
|
|
let aDate = this.getDateForFile(a);
|
|
|
|
let bDate = this.getDateForFile(b);
|
2016-02-04 08:17:16 +03:00
|
|
|
return bDate - aDate;
|
2014-02-04 17:43:20 +04:00
|
|
|
});
|
2013-10-07 10:52:08 +04:00
|
|
|
|
2014-02-04 17:43:20 +04:00
|
|
|
return this._backupFiles;
|
2017-05-12 15:47:41 +03:00
|
|
|
})();
|
2013-10-07 10:52:08 +04:00
|
|
|
},
|
|
|
|
|
2015-04-27 21:33:00 +03:00
|
|
|
/**
|
|
|
|
* 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("-");
|
|
|
|
},
|
|
|
|
|
2013-04-10 13:10:33 +04:00
|
|
|
/**
|
|
|
|
* Creates a filename for bookmarks backup files.
|
|
|
|
*
|
|
|
|
* @param [optional] aDateObj
|
|
|
|
* Date object used to build the filename.
|
|
|
|
* Will use current date if empty.
|
2014-05-24 21:48:00 +04:00
|
|
|
* @param [optional] bool - aCompress
|
|
|
|
* Determines if file extension is json or jsonlz4
|
|
|
|
Default is json
|
2013-04-10 13:10:33 +04:00
|
|
|
* @return A bookmarks backup filename.
|
|
|
|
*/
|
2014-05-24 21:48:00 +04:00
|
|
|
getFilenameForDate: function PB_getFilenameForDate(aDateObj, aCompress) {
|
2013-04-10 13:10:33 +04:00
|
|
|
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.
|
2015-04-27 21:33:00 +03:00
|
|
|
return "bookmarks-" + PlacesBackups.toISODateString(dateObj) + ".json" +
|
2014-05-24 21:48:00 +04:00
|
|
|
(aCompress ? "lz4" : "");
|
2013-04-10 13:10:33 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a Date object from a backup file. The date is the backup
|
|
|
|
* creation date.
|
|
|
|
*
|
|
|
|
* @param aBackupFile
|
2013-10-07 10:52:08 +04:00
|
|
|
* nsIFile or string path of the backup.
|
2013-04-10 13:10:33 +04:00
|
|
|
* @return A Date object for the backup's creation time.
|
|
|
|
*/
|
|
|
|
getDateForFile: function PB_getDateForFile(aBackupFile) {
|
2013-10-07 10:52:08 +04:00
|
|
|
let filename = (aBackupFile instanceof Ci.nsIFile) ? aBackupFile.leafName
|
|
|
|
: OS.Path.basename(aBackupFile);
|
2014-03-19 17:33:02 +04:00
|
|
|
let matches = filename.match(filenamesRegex);
|
2013-04-10 13:10:33 +04:00
|
|
|
if (!matches)
|
2016-08-04 10:28:58 +03:00
|
|
|
throw ("Invalid backup file name: " + filename);
|
2014-03-19 17:33:02 +04:00
|
|
|
return new Date(matches[1].replace(/-/g, "/"));
|
2013-04-10 13:10:33 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the most recent backup file.
|
|
|
|
*
|
|
|
|
* @returns nsIFile backup file
|
|
|
|
*/
|
2014-05-21 10:39:00 +04:00
|
|
|
getMostRecent: function PB_getMostRecent() {
|
2013-10-07 10:52:08 +04:00
|
|
|
Deprecated.warning(
|
|
|
|
"PlacesBackups.getMostRecent is deprecated and will be removed in a future version",
|
|
|
|
"https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
|
|
|
|
|
2014-02-04 17:43:20 +04:00
|
|
|
for (let i = 0; i < this._entries.length; i++) {
|
2015-01-15 14:25:00 +03:00
|
|
|
let rx = /\.json(lz4)?$/;
|
2014-02-04 17:43:20 +04:00
|
|
|
if (this._entries[i].leafName.match(rx))
|
|
|
|
return this._entries[i];
|
2013-04-10 13:10:33 +04:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
2013-10-07 10:52:08 +04:00
|
|
|
/**
|
|
|
|
* Get the most recent backup file.
|
|
|
|
*
|
|
|
|
* @return {Promise}
|
|
|
|
* @result the path to the file.
|
|
|
|
*/
|
2014-05-21 10:39:00 +04:00
|
|
|
getMostRecentBackup: function PB_getMostRecentBackup() {
|
2017-05-12 15:47:41 +03:00
|
|
|
return (async () => {
|
2017-05-12 15:42:39 +03:00
|
|
|
let entries = await this.getBackupFiles();
|
2013-10-07 10:52:08 +04:00
|
|
|
for (let entry of entries) {
|
2015-01-15 14:25:00 +03:00
|
|
|
let rx = /\.json(lz4)?$/;
|
2013-10-07 10:52:08 +04:00
|
|
|
if (OS.Path.basename(entry).match(rx)) {
|
2014-02-04 17:43:20 +04:00
|
|
|
return entry;
|
2013-10-07 10:52:08 +04:00
|
|
|
}
|
|
|
|
}
|
2014-02-04 17:43:20 +04:00
|
|
|
return null;
|
2017-05-12 15:47:41 +03:00
|
|
|
})();
|
2013-10-07 10:52:08 +04:00
|
|
|
},
|
|
|
|
|
2013-04-10 13:10:33 +04:00
|
|
|
/**
|
|
|
|
* 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".
|
|
|
|
*
|
2014-02-04 17:43:20 +04:00
|
|
|
* @param aFilePath
|
|
|
|
* OS.File path for the "bookmarks.json" file to be created.
|
2013-04-10 13:10:33 +04:00
|
|
|
* @return {Promise}
|
2013-07-31 18:51:09 +04:00
|
|
|
* @resolves the number of serialized uri nodes.
|
2014-02-04 17:43:20 +04:00
|
|
|
* @deprecated passing an nsIFile is deprecated
|
2013-04-10 13:10:33 +04:00
|
|
|
*/
|
2014-02-04 17:43:20 +04:00
|
|
|
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;
|
|
|
|
}
|
2017-05-12 15:47:41 +03:00
|
|
|
return (async () => {
|
2014-03-19 17:33:02 +04:00
|
|
|
let { count: nodeCount, hash: hash } =
|
2017-05-12 15:42:39 +03:00
|
|
|
await BookmarkJSONUtils.exportToFile(aFilePath);
|
2013-04-10 13:10:33 +04:00
|
|
|
|
2017-05-12 15:42:39 +03:00
|
|
|
let backupFolderPath = await this.getBackupFolder();
|
2014-02-04 17:43:20 +04:00
|
|
|
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));
|
2013-10-07 10:52:08 +04:00
|
|
|
if (!this._backupFiles) {
|
2017-05-12 15:42:39 +03:00
|
|
|
await this.getBackupFiles();
|
2013-10-07 10:52:08 +04:00
|
|
|
}
|
2014-02-04 17:43:20 +04:00
|
|
|
this._backupFiles.unshift(aFilePath);
|
2013-04-10 13:10:33 +04:00
|
|
|
} else {
|
|
|
|
// If we are saving to a folder different than our backups folder, then
|
2014-05-24 21:48:00 +04:00
|
|
|
// we also want to create a new compressed version in it.
|
2013-04-10 13:10:33 +04:00
|
|
|
// This way we ensure the latest valid backup is the same saved by the
|
|
|
|
// user. See bug 424389.
|
2017-05-12 15:42:39 +03:00
|
|
|
let mostRecentBackupFile = await this.getMostRecentBackup();
|
2014-03-19 17:33:02 +04:00
|
|
|
if (!mostRecentBackupFile ||
|
|
|
|
hash != getHashFromFilename(OS.Path.basename(mostRecentBackupFile))) {
|
2014-05-24 21:48:00 +04:00
|
|
|
let name = this.getFilenameForDate(undefined, true);
|
2014-03-19 17:33:02 +04:00
|
|
|
let newFilename = appendMetaDataToFilename(name,
|
|
|
|
{ count: nodeCount,
|
2016-12-30 02:34:54 +03:00
|
|
|
hash });
|
2014-03-19 17:33:02 +04:00
|
|
|
let newFilePath = OS.Path.join(backupFolderPath, newFilename);
|
2017-05-12 15:42:39 +03:00
|
|
|
let backupFile = await getBackupFileForSameDate(name);
|
2014-03-19 17:33:02 +04:00
|
|
|
if (backupFile) {
|
|
|
|
// There is already a backup for today, replace it.
|
2017-05-12 15:42:39 +03:00
|
|
|
await OS.File.remove(backupFile, { ignoreAbsent: true });
|
2014-03-19 17:33:02 +04:00
|
|
|
if (!this._backupFiles)
|
2017-05-12 15:42:39 +03:00
|
|
|
await this.getBackupFiles();
|
2014-03-19 17:33:02 +04:00
|
|
|
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)
|
2017-05-12 15:42:39 +03:00
|
|
|
await this.getBackupFiles();
|
2014-03-19 17:33:02 +04:00
|
|
|
this._backupFiles.unshift(newFilePath);
|
2013-10-07 10:52:08 +04:00
|
|
|
}
|
2017-05-12 15:42:39 +03:00
|
|
|
let jsonString = await OS.File.read(aFilePath);
|
|
|
|
await OS.File.writeAtomic(newFilePath, jsonString, { compression: "lz4" });
|
2014-03-19 17:33:02 +04:00
|
|
|
}
|
2013-04-10 13:10:33 +04:00
|
|
|
}
|
2013-07-31 18:51:09 +04:00
|
|
|
|
2014-02-04 17:43:20 +04:00
|
|
|
return nodeCount;
|
2017-05-12 15:47:41 +03:00
|
|
|
})();
|
2013-04-10 13:10:33 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a dated backup in <profile>/bookmarkbackups.
|
2014-05-24 21:48:00 +04:00
|
|
|
* Stores the bookmarks using a lz4 compressed JSON file.
|
2013-04-10 13:10:33 +04:00
|
|
|
* Note: any item that should not be backed up must be annotated with
|
|
|
|
* "places/excludeFromBackup".
|
|
|
|
*
|
|
|
|
* @param [optional] int aMaxBackups
|
2014-03-19 17:33:02 +04:00
|
|
|
* 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.
|
2013-04-10 13:10:33 +04:00
|
|
|
* @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) {
|
2017-05-12 15:47:41 +03:00
|
|
|
let limitBackups = async () => {
|
2017-05-12 15:42:39 +03:00
|
|
|
let backupFiles = await this.getBackupFiles();
|
2014-03-19 17:33:02 +04:00
|
|
|
if (typeof aMaxBackups == "number" && aMaxBackups > -1 &&
|
|
|
|
backupFiles.length >= aMaxBackups) {
|
|
|
|
let numberOfBackupsToDelete = backupFiles.length - aMaxBackups;
|
|
|
|
while (numberOfBackupsToDelete--) {
|
|
|
|
this._entries.pop();
|
|
|
|
let oldestBackup = this._backupFiles.pop();
|
2017-05-12 15:42:39 +03:00
|
|
|
await OS.File.remove(oldestBackup);
|
2013-04-10 13:10:33 +04:00
|
|
|
}
|
2014-03-19 17:33:02 +04:00
|
|
|
}
|
2017-05-12 15:47:41 +03:00
|
|
|
};
|
2013-04-10 13:10:33 +04:00
|
|
|
|
2017-05-12 15:47:41 +03:00
|
|
|
return (async () => {
|
2014-03-19 17:33:02 +04:00
|
|
|
if (aMaxBackups === 0) {
|
|
|
|
// Backups are disabled, delete any existing one and bail out.
|
2017-05-12 15:42:39 +03:00
|
|
|
await limitBackups(0);
|
2014-03-19 17:33:02 +04:00
|
|
|
return;
|
2013-04-10 13:10:33 +04:00
|
|
|
}
|
|
|
|
|
2014-03-19 17:33:02 +04:00
|
|
|
// Ensure to initialize _backupFiles
|
|
|
|
if (!this._backupFiles)
|
2017-05-12 15:42:39 +03:00
|
|
|
await this.getBackupFiles();
|
2014-05-24 21:48:00 +04:00
|
|
|
let newBackupFilename = this.getFilenameForDate(undefined, true);
|
2014-03-19 17:33:02 +04:00
|
|
|
// If we already have a backup for today we should do nothing, unless we
|
|
|
|
// were required to enforce a new backup.
|
2017-05-12 15:42:39 +03:00
|
|
|
let backupFile = await getBackupFileForSameDate(newBackupFilename);
|
2014-03-19 17:33:02 +04:00
|
|
|
if (backupFile && !aForceBackup)
|
|
|
|
return;
|
|
|
|
|
2013-07-31 18:51:09 +04:00
|
|
|
if (backupFile) {
|
2014-03-19 17:33:02 +04:00
|
|
|
// In case there is a backup for today we should recreate it.
|
|
|
|
this._backupFiles.shift();
|
|
|
|
this._entries.shift();
|
2017-05-12 15:42:39 +03:00
|
|
|
await OS.File.remove(backupFile, { ignoreAbsent: true });
|
2013-07-31 18:51:09 +04:00
|
|
|
}
|
|
|
|
|
2014-03-19 17:33:02 +04:00
|
|
|
// 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.
|
2017-05-12 15:42:39 +03:00
|
|
|
let mostRecentBackupFile = await this.getMostRecentBackup();
|
2014-03-19 17:33:02 +04:00
|
|
|
let mostRecentHash = mostRecentBackupFile &&
|
|
|
|
getHashFromFilename(OS.Path.basename(mostRecentBackupFile));
|
|
|
|
|
2013-07-31 18:51:09 +04:00
|
|
|
// Save bookmarks to a backup file.
|
2017-05-12 15:42:39 +03:00
|
|
|
let backupFolder = await this.getBackupFolder();
|
2014-02-04 17:43:20 +04:00
|
|
|
let newBackupFile = OS.Path.join(backupFolder, newBackupFilename);
|
2014-03-19 17:33:02 +04:00
|
|
|
let newFilenameWithMetaData;
|
|
|
|
try {
|
|
|
|
let { count: nodeCount, hash: hash } =
|
2017-05-12 15:42:39 +03:00
|
|
|
await BookmarkJSONUtils.exportToFile(newBackupFile,
|
2014-05-24 21:48:00 +04:00
|
|
|
{ compress: true,
|
|
|
|
failIfHashIs: mostRecentHash });
|
2014-03-19 17:33:02 +04:00
|
|
|
newFilenameWithMetaData = appendMetaDataToFilename(newBackupFilename,
|
|
|
|
{ count: nodeCount,
|
2016-12-30 02:34:54 +03:00
|
|
|
hash });
|
2015-12-10 14:46:49 +03:00
|
|
|
} catch (ex) {
|
|
|
|
if (!ex.becauseSameHash) {
|
|
|
|
throw ex;
|
|
|
|
}
|
2014-03-19 17:33:02 +04:00
|
|
|
// 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;
|
2014-06-06 18:09:00 +04:00
|
|
|
// Ensure we retain the proper extension when renaming
|
|
|
|
// the most recent backup file.
|
|
|
|
if (/\.json$/.test(OS.Path.basename(mostRecentBackupFile)))
|
|
|
|
newBackupFilename = this.getFilenameForDate();
|
2014-03-19 17:33:02 +04:00
|
|
|
newFilenameWithMetaData = appendMetaDataToFilename(
|
|
|
|
newBackupFilename,
|
|
|
|
{ count: this.getBookmarkCountForFile(mostRecentBackupFile),
|
|
|
|
hash: mostRecentHash });
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append metadata to the backup filename.
|
2014-02-04 17:43:20 +04:00
|
|
|
let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData);
|
2017-05-12 15:42:39 +03:00
|
|
|
await OS.File.move(newBackupFile, newBackupFileWithMetadata);
|
2014-03-19 17:33:02 +04:00
|
|
|
this._entries.unshift(new localFileCtor(newBackupFileWithMetadata));
|
2014-03-19 19:46:37 +04:00
|
|
|
this._backupFiles.unshift(newBackupFileWithMetadata);
|
2013-07-31 18:51:09 +04:00
|
|
|
|
2014-03-19 17:33:02 +04:00
|
|
|
// Limit the number of backups.
|
2017-05-12 15:42:39 +03:00
|
|
|
await limitBackups(aMaxBackups);
|
2017-05-12 15:47:41 +03:00
|
|
|
})();
|
2014-03-19 19:46:37 +04:00
|
|
|
},
|
|
|
|
|
2013-07-31 18:51:09 +04:00
|
|
|
/**
|
|
|
|
* Gets the bookmark count for backup file.
|
|
|
|
*
|
2013-10-07 10:52:08 +04:00
|
|
|
* @param aFilePath
|
|
|
|
* File path The backup file.
|
2013-07-31 18:51:09 +04:00
|
|
|
*
|
|
|
|
* @return the bookmark count or null.
|
|
|
|
*/
|
2013-10-07 10:52:08 +04:00
|
|
|
getBookmarkCountForFile: function PB_getBookmarkCountForFile(aFilePath) {
|
2013-07-31 18:51:09 +04:00
|
|
|
let count = null;
|
2013-10-07 10:52:08 +04:00
|
|
|
let filename = OS.Path.basename(aFilePath);
|
2014-03-19 17:33:02 +04:00
|
|
|
let matches = filename.match(filenamesRegex);
|
|
|
|
if (matches && matches[2])
|
|
|
|
count = matches[2];
|
2013-07-31 18:51:09 +04:00
|
|
|
return count;
|
|
|
|
},
|
|
|
|
|
2014-02-04 17:43:20 +04:00
|
|
|
/**
|
|
|
|
* 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
|
2016-02-04 01:22:33 +03:00
|
|
|
* * type: type of the originating node as defined in PlacesUtils
|
2014-02-04 17:43:20 +04:00
|
|
|
* The following properties exist only for a subset of bookmarks:
|
|
|
|
* * annos: array of annotations
|
|
|
|
* * uri: url
|
2014-03-03 20:51:42 +04:00
|
|
|
* * iconuri: favicon's url
|
2014-02-04 17:43:20 +04:00
|
|
|
* * 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
|
|
|
|
*/
|
2017-05-12 15:42:39 +03:00
|
|
|
async getBookmarksTree() {
|
2014-05-22 20:06:57 +04:00
|
|
|
let startTime = Date.now();
|
2017-05-12 15:42:39 +03:00
|
|
|
let root = await PlacesUtils.promiseBookmarksTree(PlacesUtils.bookmarks.rootGuid, {
|
2014-05-22 20:06:57 +04:00
|
|
|
excludeItemsCallback: aItem => {
|
|
|
|
return aItem.annos &&
|
|
|
|
aItem.annos.find(a => a.name == PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
|
2014-09-17 15:59:23 +04:00
|
|
|
},
|
|
|
|
includeItemIds: true
|
2014-02-04 17:43:20 +04:00
|
|
|
});
|
|
|
|
|
|
|
|
try {
|
2014-05-22 20:06:57 +04:00
|
|
|
Services.telemetry
|
|
|
|
.getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
|
|
|
|
.add(Date.now() - startTime);
|
|
|
|
} catch (ex) {
|
|
|
|
Components.utils.reportError("Unable to report telemetry.");
|
2014-02-04 17:43:20 +04:00
|
|
|
}
|
2014-05-22 20:06:57 +04:00
|
|
|
return [root, root.itemsCount];
|
2017-05-12 10:04:51 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wrapper for PlacesUtils.bookmarks.eraseEverything that removes non-default
|
|
|
|
* roots.
|
|
|
|
*
|
|
|
|
* Note that default roots are preserved, only their children will be removed.
|
|
|
|
*
|
|
|
|
* TODO Ideally we wouldn't need to worry about non-default roots. However,
|
|
|
|
* until bug 1310299 is fixed, we still need to manage them.
|
|
|
|
*
|
|
|
|
* @param {Object} [options={}]
|
|
|
|
* Additional options. Currently supports the following properties:
|
|
|
|
* - source: The change source, forwarded to all bookmark observers.
|
|
|
|
* Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
|
|
|
|
*
|
|
|
|
* @return {Promise} resolved when the removal is complete.
|
|
|
|
* @resolves once the removal is complete.
|
|
|
|
*/
|
|
|
|
async eraseEverythingIncludingUserRoots(options = {}) {
|
|
|
|
if (!options.source) {
|
|
|
|
options.source = PlacesUtils.bookmarks.SOURCES.DEFAULT;
|
|
|
|
}
|
|
|
|
|
|
|
|
let excludeItems =
|
|
|
|
PlacesUtils.annotations.getItemsWithAnnotation(PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
|
2017-06-16 20:12:18 +03:00
|
|
|
|
2017-05-12 10:04:51 +03:00
|
|
|
let rootFolderChildren = await getTopLevelFolderIds();
|
|
|
|
|
|
|
|
// We only need to do top-level roots here.
|
|
|
|
for (let child of rootFolderChildren) {
|
|
|
|
if (!PlacesUtils.bookmarks.userContentRoots.includes(child.guid) &&
|
|
|
|
child.guid != PlacesUtils.bookmarks.tagsGuid &&
|
|
|
|
!excludeItems.includes(child.id)) {
|
|
|
|
await PlacesUtils.bookmarks.remove(child.guid, {source: options.source});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return PlacesUtils.bookmarks.eraseEverything(options);
|
|
|
|
},
|
|
|
|
}
|