// Bookmark Backup 0.4.1 // By Pike // Contributor(s): // Nickolay Ponomarev (Multiple file backup code) function BackupService() { this._init(); } BackupService.prototype = { _STRINGS_URL: 'chrome://bookmarkbackup/locale/bookmarkbackup.properties', _CREATE_PERMISSIONS: 0700, performBackup: function() { // Create logger this._logger = new BackupServiceLogger(); this._logger.log('Backup begun'); try { // Init vars var profileDir = this._dirService.get('ProfD', Components.interfaces.nsILocalFile); var stringBundleService = Components.classes['@mozilla.org/intl/stringbundle;1'].getService(Components.interfaces.nsIStringBundleService); var localeStrings = stringBundleService.createBundle(this._STRINGS_URL); // Find bookmarks file and backups dir var bookmarksFile = this._getBookmarksFile(); var backupsDirPath = localeStrings.GetStringFromName('backupsFolderName'); var dayNameDirPath = localeStrings.GetStringFromName('dayOfWeek.' + (new Date()).getDay()); var backupsDir = this._getBackupsDir(backupsDirPath, dayNameDirPath, bookmarksFile); if (backupsDir) { // Get names of additional files to backup var backupFileNames = []; try { var backupList = this._prefBranch.getComplexValue('bookmarkbackup.backupList', Components.interfaces.nsISupportsString).data; if (backupList) { backupFileNames = backupList.split('|'); } } catch (err) {} // Backup bookmarks file if (bookmarksFile) { this._copyFile(bookmarksFile.parent, backupsDir.clone(), [bookmarksFile.leafName]); } // Backup additional files for (var i = 0; i < backupFileNames.length; i++) { if (backupFileNames[i]) { var fileSections = backupFileNames[i].split('/'); this._copyFile(profileDir.clone(), backupsDir.clone(), fileSections); } } this._logger.log('Backup completed'); } } catch (err) { if (err instanceof BackupFolderNotCreated) { this._logger.log('Error: Today\'s backup dir could not be created at: ' + err.path); } else { this._logger.log('Error: Backup aborted due to:\n' + err); } } this._logger.addBlankLine(); this._logger = null; }, _getBookmarksFile: function() { var bookmarksFile; // Check if the bookmarks file is in a custom location (the BMarks directory service key // does *not* take this pref into account) if (this._prefBranch.prefHasUserValue('browser.bookmarks.file')) { try { bookmarksFile = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile); bookmarksFile.initWithPath(this._prefBranch.getCharPref('browser.bookmarks.file')); } catch (err) { this._logger.log('Warning: Custom bookmarks location (browser.bookmarks.file) is invalid'); bookmarksFile = null; } } if (!bookmarksFile) { try { bookmarksFile = this._dirService.get('BMarks', Components.interfaces.nsILocalFile); } catch (err) {} } return bookmarksFile; }, _getStandardBackupsDir: function(backupsDirName, dayNameDirPath, bookmarksFile) { var standardBackupsDir; if (bookmarksFile) // Use directory in same place as bookmarks.html { standardBackupsDir = bookmarksFile.parent; } else // Use directory in profile { standardBackupsDir = this._dirService.get('ProfD', Components.interfaces.nsILocalFile); } standardBackupsDir.append(backupsDirName); standardBackupsDir.append(dayNameDirPath); return standardBackupsDir; }, _getCustomBackupsDir: function(dayNameDirPath) { var customBackupsDir; try { customBackupsDir = this._prefBranch.getComplexValue('bookmarkbackup.location', Components.interfaces.nsILocalFile); customBackupsDir.append(dayNameDirPath); } catch (err) { customBackupsDir = null; } return customBackupsDir; }, _getBackupsDir: function(backupsDirName, dayNameDirPath, bookmarksFile) { var backupsDir; // Read pref that determines whether to delete any existing dir var deletePrevious = false; try { deletePrevious = this._prefBranch.getBoolPref('bookmarkbackup.deletePrevious'); } catch (err) {} // If a custom location was set, try to get it if (this._prefBranch.prefHasUserValue('bookmarkbackup.location')) { backupsDir = this._getCustomBackupsDir(dayNameDirPath); // Create backup dir if it doesn't exist yet if (backupsDir) { if (deletePrevious && backupsDir.exists()) { backupsDir.remove(true); this._logger.log('Today\'s backup dir deleted at: ' + backupsDir.path); } if (!backupsDir.exists()) { try { backupsDir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, this._CREATE_PERMISSIONS); this._logger.log('Today\'s backup dir created at: ' + backupsDir.path); } catch (err) { backupsDir = null; } } else { this._logger.log('Today\'s backup dir found at: ' + backupsDir.path); } } // Custom dir could not be used, reset to standard location instead if (!backupsDir) { this._logger.log('Warning: Custom backup location was invalid, resetting to default location (usually the user profile)'); this._prefBranch.clearUserPref('bookmarkbackup.location'); } } // If no custom location was set, or custom location was invalid, get standard location if (!backupsDir) { backupsDir = this._getStandardBackupsDir(backupsDirName, dayNameDirPath, bookmarksFile); // Create backup dir if it doesn't exist yet if (backupsDir) { if (deletePrevious && backupsDir.exists()) { backupsDir.remove(true); this._logger.log('Today\'s backup dir deleted at: ' + backupsDir.path); } if (!backupsDir.exists()) { try { backupsDir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, this._CREATE_PERMISSIONS); this._logger.log('Today\'s backup dir created at: ' + backupsDir.path); } catch (err) { throw new BackupFolderNotCreated(backupsDir.path); } } else { this._logger.log('Today\'s backup dir found at: ' + backupsDir.path); } } } return backupsDir; }, _copyFile: function(fromFileOrDir, toFileOrDir, relPath) // Perform recursive backup. An alternative would be using nsIFile.remove(true) on the existing // backup, but this function is safer as (in theory) it should only remove files that are about // to be overwritten, parameter types are: (nsIFile, nsIFile, string array) { if (fromFileOrDir.exists()) { if (relPath.length > 0) { var pathSection = relPath.shift(); var wildcard = /^\*\.(\w*)$/i.exec(pathSection); if (wildcard) { this._copyChildren(fromFileOrDir, toFileOrDir, relPath, wildcard[1]); } else { fromFileOrDir.append(pathSection); toFileOrDir.append(pathSection); this._copyFile(fromFileOrDir, toFileOrDir, relPath); } } else if (fromFileOrDir.isDirectory()) { this._copyChildren(fromFileOrDir, toFileOrDir, relPath, null); } else { try { if (toFileOrDir.exists()) { toFileOrDir.remove(false); } fromFileOrDir.copyTo(toFileOrDir.parent, toFileOrDir.leafName); this._logger.log(toFileOrDir.leafName + ' successfully copied'); } catch (err) { this._logger.log('Warning: ' + fromFileOrDir.path + ' could not be copied to ' + toFileOrDir.path); } } } else { this._logger.log('Warning: ' + fromFileOrDir.path + ' could not be found'); } }, _copyChildren: function(fromDir, toDir, relPath, fileExt) { // Check fromDir exists and is a directory, DO NOT check toDir since it may not exist yet if (fromDir.exists() && fromDir.isDirectory()) { var pattern = null; if (fileExt) { pattern = new RegExp('\\.' + fileExt + '$'); } var dirEnum = fromDir.directoryEntries; while (dirEnum.hasMoreElements()) { var fromChild = dirEnum.getNext().QueryInterface(Components.interfaces.nsIFile); if (!pattern || fromChild.leafName.match(pattern)) { var toChild = toDir.clone(); toChild.append(fromChild.leafName); this._copyFile(fromChild, toChild, relPath); } } } }, _init: function() { // Setup object properties this._observerService = Components.classes['@mozilla.org/observer-service;1'].getService(Components.interfaces.nsIObserverService); this._dirService = Components.classes['@mozilla.org/file/directory_service;1'].getService(Components.interfaces.nsIProperties); this._prefBranch = Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces.nsIPrefBranch); this._timerHandle = Components.classes['@mozilla.org/timer;1'].createInstance(Components.interfaces.nsITimer); // Add observers this._observerService.addObserver(this, 'profile-after-change', false); this._observerService.addObserver(this, 'profile-before-change', false); this._observerService.addObserver(this, 'xpcom-shutdown', false); }, _destruct: function() { // Remove observers this._observerService.removeObserver(this, 'profile-after-change'); this._observerService.removeObserver(this, 'profile-before-change'); this._observerService.removeObserver(this, 'xpcom-shutdown'); // Clear object properties this._observerService = null; this._dirService = null; this._prefBranch = null; this._timerHandle = null; }, _onLoad: function() { // Perform initial backup var startupBackup = false; try { startupBackup = this._prefBranch.getBoolPref('bookmarkbackup.startupBackup'); } catch (err) {} if (startupBackup) { this.performBackup(); } // Start cyclic backup var backupInterval = 0; try { backupInterval = this._prefBranch.getIntPref('bookmarkbackup.interval'); } catch (err) {} if (backupInterval > 0) // Setting the pref to 0 disables the cyclic backup { backupInterval *= 60000; // Convert from minutes to milliseconds this._timerHandle.init(this, backupInterval, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); } }, _onUnload: function() { // Stop cyclic backup this._timerHandle.cancel(); // Perform final backup var shutdownBackup = true; try { shutdownBackup = this._prefBranch.getBoolPref('bookmarkbackup.shutdownBackup'); } catch (err) {} if (shutdownBackup) { this.performBackup(); } }, observe: function(subject, topic, data) { if (topic == 'profile-after-change') { this._onLoad(); } else if (topic == 'profile-before-change') { this._onUnload(); } else if (topic == 'xpcom-shutdown') { this._destruct(); } else if (topic == 'timer-callback' && subject == this._timerHandle) { this.performBackup(); } }, QueryInterface: function(iid) { if (iid.equals(Components.interfaces.iBackupService) || iid.equals(Components.interfaces.nsIObserver) || iid.equals(Components.interfaces.nsISupports)) { return this; } else { throw Components.results.NS_ERROR_NO_INTERFACE; } } }; function BackupFolderNotCreated(path) { this.path = path; } function BackupServiceLogger() { this._init(); } BackupServiceLogger.prototype = { _CREATE_PERMISSIONS: 0600, _FILE_FLAGS: 0x02 | 0x08 | 0x10, // WRONLY | CREATE_FILE | APPEND log: function(message) { this._writeOutput((new Date().toLocaleString()) + ': ' + message + '\n'); }, addBlankLine: function() { this._writeOutput('\n'); }, _writeOutput: function(output) { if (this._logFile) { try { var fileOutputStream = Components.classes['@mozilla.org/network/file-output-stream;1'].createInstance(Components.interfaces.nsIFileOutputStream); fileOutputStream.init(this._logFile, this._FILE_FLAGS, this._CREATE_PERMISSIONS, 0); fileOutputStream.write(output, output.length); fileOutputStream.close(); } catch (err) {} } }, _init: function() { var prefBranch = Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces.nsIPrefBranch); try { if (prefBranch.getBoolPref('bookmarkbackup.loggingEnabled')) { var dirService = Components.classes['@mozilla.org/file/directory_service;1'].getService(Components.interfaces.nsIProperties); this._logFile = dirService.get('ProfD', Components.interfaces.nsILocalFile); this._logFile.append('bookmarkbackup.log'); } } catch (err) {} } }; var BackupServiceFactory = { createInstance: function(outer, iid) { if (outer) { throw Components.results.NS_ERROR_NO_AGGREGATION; } return (new BackupService()).QueryInterface(iid); } }; var BackupServiceModule = { _NAME: 'BackupService@pikey.me.uk', _CID: Components.ID('{451b1443-ed41-11d9-920f-00055d74c2e4}'), _CONTRACT_ID: '@pikey.me.uk/backup-service;1', registerSelf: function(compMgr, fileSpec, location, type) { compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar); compMgr.registerFactoryLocation(this._CID, this._NAME, this._CONTRACT_ID, fileSpec, location, type); var categoryManager = Components.classes['@mozilla.org/categorymanager;1'].getService(Components.interfaces.nsICategoryManager); categoryManager.addCategoryEntry('app-startup', this._NAME, 'service,' + this._CONTRACT_ID, true, true); }, unregisterSelf: function(compMgr, fileSpec, location) { compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar); compMgr.unregisterFactoryLocation(this._CID, fileSpec); var categoryManager = Components.classes['@mozilla.org/categorymanager;1'].getService(Components.interfaces.nsICategoryManager); categoryManager.deleteCategoryEntry('app-startup', this._NAME, true); }, getClassObject: function(compMgr, cid, iid) { if (!iid.equals(Components.interfaces.nsIFactory)) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; } if (cid.equals(this._CID)) { return BackupServiceFactory; } else { throw Components.results.NS_ERROR_NO_INTERFACE; } }, canUnload: function(compMgr) { return true; } }; function NSGetModule(compMgr, fileSpec) { return BackupServiceModule; }