Data directory migration
This adds a new button to the Advanced prefs to migrate the data directory to $HOME/Zotero. The button only appears if the data directory is set to the default location within a profile directory (including the other program from the one running, even though that's technically stored as a custom data directory). On Mac/Linux, directories within the data directory are moved with /bin/mv. On Windows, or if that fails, they're copied recursively using OS.File.move() (which annoyingly doesn't reliably support directory moving). The former should be instantaneous on most systems (unless the data directory or 'storage' were on a different filesystem from $HOME). If the database fails to transfer, migration fails and the data directory setting remains on the old directory. If the database transfers but other files fail, the data directory setting is updated. In both cases, the user is encouraged to migrate remaining files manually with a button that reveals the directories and quits the program. This isn't yet tested on Linux or Windows, and migration isn't yet suggested automatically. Adds Zotero.File.reveal(), Zotero.File.directoryIsEmpty(), and Zotero.File.moveDirectory().
This commit is contained in:
parent
cfe76a6f83
commit
79700969e1
|
@ -36,18 +36,41 @@ Zotero_Preferences.Advanced = {
|
||||||
this.onDataDirLoad();
|
this.onDataDirLoad();
|
||||||
},
|
},
|
||||||
|
|
||||||
revealDataDirectory: function () {
|
|
||||||
var dataDir = Zotero.getZoteroDirectory();
|
migrateDataDirectory: Zotero.Promise.coroutine(function* () {
|
||||||
dataDir.QueryInterface(Components.interfaces.nsILocalFile);
|
var currentDir = Zotero.getZoteroDirectory().path;
|
||||||
try {
|
var defaultDir = Zotero.getDefaultDataDir();
|
||||||
dataDir.reveal();
|
if (currentDir == defaultDir) {
|
||||||
|
Zotero.debug("Already using default directory");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch (e) {
|
|
||||||
// On platforms that don't support nsILocalFile.reveal() (e.g. Linux),
|
Components.utils.import("resource://zotero/config.js")
|
||||||
// launch the directory
|
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
||||||
window.opener.ZoteroPane_Local.launchFile(dataDir);
|
.getService(Components.interfaces.nsIPromptService);
|
||||||
|
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
|
||||||
|
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL);
|
||||||
|
var index = ps.confirmEx(window,
|
||||||
|
Zotero.getString('zotero.preferences.advanced.migrateDataDir.title'),
|
||||||
|
Zotero.getString(
|
||||||
|
'zotero.preferences.advanced.migrateDataDir.directoryWillBeMoved',
|
||||||
|
[ZOTERO_CONFIG.CLIENT_NAME, defaultDir]
|
||||||
|
) + '\n\n'
|
||||||
|
+ Zotero.getString(
|
||||||
|
'zotero.preferences.advanced.migrateDataDir.appMustBeRestarted', Zotero.appName
|
||||||
|
),
|
||||||
|
buttonFlags,
|
||||||
|
Zotero.getString('general.restartApp', Zotero.appName),
|
||||||
|
null, null, null, {}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index == 0) {
|
||||||
|
yield Zotero.File.putContentsAsync(
|
||||||
|
OS.Path.join(currentDir, Zotero.DATA_DIR_MIGRATION_MARKER), currentDir
|
||||||
|
);
|
||||||
|
Zotero.Utilities.Internal.quitZotero(true);
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
|
|
||||||
|
|
||||||
runIntegrityCheck: Zotero.Promise.coroutine(function* () {
|
runIntegrityCheck: Zotero.Promise.coroutine(function* () {
|
||||||
|
@ -195,10 +218,11 @@ Zotero_Preferences.Advanced = {
|
||||||
var useDataDir = Zotero.Prefs.get('useDataDir');
|
var useDataDir = Zotero.Prefs.get('useDataDir');
|
||||||
var dataDir = Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir');
|
var dataDir = Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir');
|
||||||
var defaultDataDir = Zotero.getDefaultDataDir();
|
var defaultDataDir = Zotero.getDefaultDataDir();
|
||||||
|
var currentDir = Zotero.getZoteroDirectory().path;
|
||||||
|
|
||||||
// Change "Use profile directory" label to home directory location unless using profile dir
|
// Change "Use profile directory" label to home directory location unless using profile dir
|
||||||
if (useDataDir || Zotero.getZoteroDirectory().path == defaultDataDir) {
|
if (useDataDir || currentDir == defaultDataDir) {
|
||||||
document.getElementById('defaultDataDir').setAttribute(
|
document.getElementById('default-data-dir').setAttribute(
|
||||||
'label', Zotero.getString('dataDir.default', Zotero.getDefaultDataDir())
|
'label', Zotero.getString('dataDir.default', Zotero.getDefaultDataDir())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -208,14 +232,17 @@ Zotero_Preferences.Advanced = {
|
||||||
useDataDir = false;
|
useDataDir = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('dataDirPath').setAttribute('disabled', !useDataDir);
|
document.getElementById('data-dir-path').setAttribute('disabled', !useDataDir);
|
||||||
|
document.getElementById('migrate-data-dir').setAttribute(
|
||||||
|
'hidden', !Zotero.canMigrateDataDirectory()
|
||||||
|
);
|
||||||
|
|
||||||
return useDataDir;
|
return useDataDir;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
onDataDirUpdate: function (event) {
|
onDataDirUpdate: function (event) {
|
||||||
var radiogroup = document.getElementById('dataDir');
|
var radiogroup = document.getElementById('data-dir');
|
||||||
var useDataDir = Zotero.Prefs.get('useDataDir');
|
var useDataDir = Zotero.Prefs.get('useDataDir');
|
||||||
var newUseDataDir = radiogroup.selectedIndex == 1;
|
var newUseDataDir = radiogroup.selectedIndex == 1;
|
||||||
|
|
||||||
|
@ -226,14 +253,14 @@ Zotero_Preferences.Advanced = {
|
||||||
// This call shows a filepicker if needed, forces a restart if required, and does nothing if
|
// This call shows a filepicker if needed, forces a restart if required, and does nothing if
|
||||||
// cancel was pressed or value hasn't changed
|
// cancel was pressed or value hasn't changed
|
||||||
Zotero.chooseZoteroDirectory(true, !newUseDataDir, function () {
|
Zotero.chooseZoteroDirectory(true, !newUseDataDir, function () {
|
||||||
Zotero_Preferences.openURL('http://zotero.org/support/zotero_data');
|
Zotero_Preferences.openURL('https://zotero.org/support/zotero_data');
|
||||||
});
|
});
|
||||||
radiogroup.selectedIndex = this._usingDefaultDataDir() ? 0 : 1;
|
radiogroup.selectedIndex = this._usingDefaultDataDir() ? 0 : 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
chooseDataDir: function(event) {
|
chooseDataDir: function(event) {
|
||||||
document.getElementById('dataDir').selectedIndex = 1;
|
document.getElementById('data-dir').selectedIndex = 1;
|
||||||
//this.onDataDirUpdate(event);
|
//this.onDataDirUpdate(event);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -182,14 +182,14 @@
|
||||||
<groupbox>
|
<groupbox>
|
||||||
<caption label="&zotero.preferences.dataDir;"/>
|
<caption label="&zotero.preferences.dataDir;"/>
|
||||||
|
|
||||||
<radiogroup id="dataDir"
|
<radiogroup id="data-dir"
|
||||||
preference="pref-useDataDir"
|
preference="pref-useDataDir"
|
||||||
onsyncfrompreference="return Zotero_Preferences.Advanced.onDataDirLoad()"
|
onsyncfrompreference="return Zotero_Preferences.Advanced.onDataDirLoad()"
|
||||||
onsynctopreference="return Zotero_Preferences.Advanced.onDataDirUpdate(event);">
|
onsynctopreference="return Zotero_Preferences.Advanced.onDataDirUpdate(event);">
|
||||||
<radio id="defaultDataDir" label="&zotero.preferences.dataDir.useProfile;" value="false"/>
|
<radio id="default-data-dir" label="&zotero.preferences.dataDir.useProfile;" value="false"/>
|
||||||
<hbox>
|
<hbox>
|
||||||
<radio label="&zotero.preferences.dataDir.custom;" value="true"/>
|
<radio label="&zotero.preferences.dataDir.custom;" value="true"/>
|
||||||
<textbox id="dataDirPath" preference="pref-dataDir"
|
<textbox id="data-dir-path" preference="pref-dataDir"
|
||||||
onsyncfrompreference="return Zotero_Preferences.Advanced.getDataDirPath();"
|
onsyncfrompreference="return Zotero_Preferences.Advanced.getDataDirPath();"
|
||||||
readonly="true" flex="1"/>
|
readonly="true" flex="1"/>
|
||||||
<button label="&zotero.preferences.dataDir.choose;"
|
<button label="&zotero.preferences.dataDir.choose;"
|
||||||
|
@ -199,7 +199,9 @@
|
||||||
|
|
||||||
<hbox>
|
<hbox>
|
||||||
<button label="&zotero.preferences.dataDir.reveal;"
|
<button label="&zotero.preferences.dataDir.reveal;"
|
||||||
oncommand="Zotero_Preferences.Advanced.revealDataDirectory()"/>
|
oncommand="Zotero.revealDataDirectory()"/>
|
||||||
|
<button id="migrate-data-dir" label="&zotero.preferences.dataDir.migrate;"
|
||||||
|
oncommand="Zotero_Preferences.Advanced.migrateDataDirectory()" hidden="true"/>
|
||||||
</hbox>
|
</hbox>
|
||||||
</groupbox>
|
</groupbox>
|
||||||
|
|
||||||
|
|
|
@ -437,11 +437,33 @@ Zotero.File = new function(){
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise<Boolean>}
|
||||||
|
*/
|
||||||
|
this.directoryIsEmpty = Zotero.Promise.coroutine(function* (path) {
|
||||||
|
var it = new OS.File.DirectoryIterator(path);
|
||||||
|
try {
|
||||||
|
let entry = yield it.next();
|
||||||
|
Zotero.debug(entry);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e != StopIteration) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
it.close();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a generator with an OS.File.DirectoryIterator, closing the
|
* Run a generator with an OS.File.DirectoryIterator, closing the
|
||||||
* iterator when done
|
* iterator when done
|
||||||
*
|
*
|
||||||
* The DirectoryInterator is passed as the first parameter to the generator.
|
* The DirectoryIterator is passed as the first parameter to the generator.
|
||||||
*
|
*
|
||||||
* Zotero.File.iterateDirectory(path, function* (iterator) {
|
* Zotero.File.iterateDirectory(path, function* (iterator) {
|
||||||
* while (true) {
|
* while (true) {
|
||||||
|
@ -465,6 +487,133 @@ Zotero.File = new function(){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move directory (using mv on macOS/Linux, recursively on Windows)
|
||||||
|
*
|
||||||
|
* @param {Boolean} [options.allowExistingTarget=false] - If true, merge files into an existing
|
||||||
|
* target directory if one exists rather than throwing an error
|
||||||
|
* @param {Function} options.noOverwrite - Function that returns true if the file at the given
|
||||||
|
* path should throw an error rather than overwrite an existing file in the target
|
||||||
|
*/
|
||||||
|
this.moveDirectory = Zotero.Promise.coroutine(function* (oldDir, newDir, options = {}) {
|
||||||
|
var maxDepth = options.maxDepth || 10;
|
||||||
|
var cmd = "/bin/mv";
|
||||||
|
var useCmd = !Zotero.isWin && (yield OS.File.exists(cmd));
|
||||||
|
|
||||||
|
if (!options.allowExistingTarget && (yield OS.File.exists(newDir))) {
|
||||||
|
throw new Error(newDir + " exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
// Throw certain known errors (no more disk space) to interrupt the operation
|
||||||
|
function checkError(e) {
|
||||||
|
if (!(e instanceof OS.File.Error)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Zotero.isWin) {
|
||||||
|
switch (e.unixErrno) {
|
||||||
|
case OS.Constants.libc.ENOSPC:
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addError(e) {
|
||||||
|
errors.push(e);
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootDir = oldDir;
|
||||||
|
var moveSubdirs = Zotero.Promise.coroutine(function* (oldDir, depth) {
|
||||||
|
if (!depth) return;
|
||||||
|
|
||||||
|
// Create target directory
|
||||||
|
try {
|
||||||
|
yield Zotero.File.createDirectoryIfMissingAsync(newDir + oldDir.substr(rootDir.length));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
addError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.debug("Moving files in " + oldDir);
|
||||||
|
|
||||||
|
yield Zotero.File.iterateDirectory(oldDir, function* (iterator) {
|
||||||
|
while (true) {
|
||||||
|
let entry = yield iterator.next();
|
||||||
|
let dest = newDir + entry.path.substr(rootDir.length);
|
||||||
|
|
||||||
|
// Move files in directory
|
||||||
|
if (!entry.isDir) {
|
||||||
|
try {
|
||||||
|
yield OS.File.move(
|
||||||
|
entry.path,
|
||||||
|
dest,
|
||||||
|
{
|
||||||
|
noOverwrite: options
|
||||||
|
&& options.noOverwrite
|
||||||
|
&& options.noOverwrite(entry.path)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
checkError(e);
|
||||||
|
Zotero.debug("Error moving " + entry.path);
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Move directory with external command if possible and the directory doesn't
|
||||||
|
// already exist in target
|
||||||
|
let moved = false;
|
||||||
|
|
||||||
|
if (useCmd && !(yield OS.File.exists(dest))) {
|
||||||
|
let args = [entry.path, dest];
|
||||||
|
try {
|
||||||
|
yield Zotero.Utilities.Internal.exec(cmd, args);
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
checkError(e);
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, recurse into subdirectories to copy files individually
|
||||||
|
if (!moved) {
|
||||||
|
try {
|
||||||
|
yield moveSubdirs(entry.path, depth - 1);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
checkError(e);
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove directory after moving everything within
|
||||||
|
//
|
||||||
|
// Don't try to remove root directory if there've been errors, since it won't work.
|
||||||
|
// (Deeper directories might fail too, but we don't worry about those.)
|
||||||
|
if (!errors.length || oldDir != rootDir) {
|
||||||
|
Zotero.debug("Removing " + oldDir);
|
||||||
|
try {
|
||||||
|
yield OS.File.removeEmptyDir(oldDir);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
yield moveSubdirs(oldDir, maxDepth);
|
||||||
|
return errors;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a data: URI from an nsIFile
|
* Generate a data: URI from an nsIFile
|
||||||
|
@ -1059,4 +1208,42 @@ Zotero.File = new function(){
|
||||||
this.isDropboxDirectory = function(path) {
|
this.isDropboxDirectory = function(path) {
|
||||||
return path.toLowerCase().indexOf('dropbox') != -1;
|
return path.toLowerCase().indexOf('dropbox') != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.reveal = Zotero.Promise.coroutine(function* (file) {
|
||||||
|
if (!(yield OS.File.exists(file))) {
|
||||||
|
throw new Error(file + " does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.debug("Revealing " + file);
|
||||||
|
|
||||||
|
var nsIFile = this.pathToFile(file);
|
||||||
|
nsIFile.QueryInterface(Components.interfaces.nsILocalFile);
|
||||||
|
try {
|
||||||
|
nsIFile.reveal();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
// On platforms that don't support nsILocalFile.reveal() (e.g. Linux),
|
||||||
|
// launch the directory
|
||||||
|
let zp = Zotero.getActiveZoteroPane();
|
||||||
|
if (zp) {
|
||||||
|
try {
|
||||||
|
let info = yield OS.File.stat(file);
|
||||||
|
// Launch parent directory for files
|
||||||
|
if (!info.isDir) {
|
||||||
|
file = OS.Path.dirname(file);
|
||||||
|
}
|
||||||
|
Zotero.launchFile(file);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,6 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
// Privileged (public) methods
|
// Privileged (public) methods
|
||||||
this.getProfileDirectory = getProfileDirectory;
|
this.getProfileDirectory = getProfileDirectory;
|
||||||
this.getStorageDirectory = getStorageDirectory;
|
this.getStorageDirectory = getStorageDirectory;
|
||||||
this.getZoteroDatabase = getZoteroDatabase;
|
|
||||||
this.chooseZoteroDirectory = chooseZoteroDirectory;
|
this.chooseZoteroDirectory = chooseZoteroDirectory;
|
||||||
this.debug = debug;
|
this.debug = debug;
|
||||||
this.log = log;
|
this.log = log;
|
||||||
|
@ -67,7 +66,8 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
Components.utils.import("resource://zotero/bluebird.js", this);
|
Components.utils.import("resource://zotero/bluebird.js", this);
|
||||||
|
|
||||||
this.getActiveZoteroPane = function() {
|
this.getActiveZoteroPane = function() {
|
||||||
return Services.wm.getMostRecentWindow("navigator:browser").ZoteroPane;
|
var win = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
return win ? win.ZoteroPane : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -103,7 +103,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
this.hiDPISuffix = "";
|
this.hiDPISuffix = "";
|
||||||
|
|
||||||
var _startupErrorHandler;
|
var _startupErrorHandler;
|
||||||
var _zoteroDirectory = false;
|
var _dataDirectory = false;
|
||||||
var _localizedStringBundle;
|
var _localizedStringBundle;
|
||||||
|
|
||||||
var _locked = false;
|
var _locked = false;
|
||||||
|
@ -310,14 +310,22 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
}
|
}
|
||||||
// DEBUG: handle more startup errors
|
// DEBUG: handle more startup errors
|
||||||
else {
|
else {
|
||||||
throw (e);
|
throw e;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Zotero.isConnector) {
|
||||||
|
yield Zotero.checkForDataDirectoryMigration(dataDir.path, this.getDefaultDataDir());
|
||||||
|
if (this.skipLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure data directory isn't in Dropbox, etc.
|
||||||
if (Zotero.isStandalone) {
|
if (Zotero.isStandalone) {
|
||||||
Zotero.checkForUnsafeDataDirectory(dataDir.path);
|
Zotero.checkForUnsafeDataDirectory(dataDir.path);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register shutdown handler to call Zotero.shutdown()
|
// Register shutdown handler to call Zotero.shutdown()
|
||||||
var _shutdownObserver = {observe:function() { Zotero.shutdown().done() }};
|
var _shutdownObserver = {observe:function() { Zotero.shutdown().done() }};
|
||||||
Services.obs.addObserver(_shutdownObserver, "quit-application", false);
|
Services.obs.addObserver(_shutdownObserver, "quit-application", false);
|
||||||
|
@ -917,10 +925,10 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
return [defaultProfile, nSections > 1];
|
return [defaultProfile, nSections > 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.getZoteroDirectory = function () {
|
this.getZoteroDirectory = function () {
|
||||||
if (_zoteroDirectory != false) {
|
if (_dataDirectory != false) {
|
||||||
// Return a clone of the file pointer so that callers can modify it
|
return Zotero.File.pathToFile(_dataDirectory);
|
||||||
return _zoteroDirectory.clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var file = Components.classes["@mozilla.org/file/local;1"]
|
var file = Components.classes["@mozilla.org/file/local;1"]
|
||||||
|
@ -950,17 +958,38 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
throw (e);
|
throw (e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// If there's a migration marker in this directory and no database, migration was
|
||||||
|
// interrupted before the database could be moved (or moving failed), so use the source
|
||||||
|
// directory specified in the marker file.
|
||||||
|
let migrationMarker = OS.Path.join(prefVal, this.DATA_DIR_MIGRATION_MARKER);
|
||||||
|
let dbFile = OS.Path.join(prefVal, this.getDatabaseFilename());
|
||||||
|
if (Zotero.File.pathToFile(migrationMarker).exists()
|
||||||
|
&& !(Zotero.File.pathToFile(dbFile).exists())) {
|
||||||
|
let fileStr = Zotero.File.getContents(migrationMarker);
|
||||||
|
try {
|
||||||
|
file = Zotero.File.pathToFile(fileStr);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
Zotero.debug(`Invalid path '${fileStr}' in marker file`, 1);
|
||||||
|
e = { name: "NS_ERROR_FILE_NOT_FOUND" };
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
file = Zotero.File.pathToFile(prefVal);
|
file = Zotero.File.pathToFile(prefVal);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
Zotero.debug(`Invalid path '${prefVal}' in dataDir pref`, 1);
|
Zotero.debug(`Invalid path '${prefVal}' in dataDir pref`, 1);
|
||||||
e = { name: "NS_ERROR_FILE_NOT_FOUND" };
|
e = { name: "NS_ERROR_FILE_NOT_FOUND" };
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
let dataDir = this.getDefaultDataDir();
|
let dataDir = this.getDefaultDataDir();
|
||||||
|
|
||||||
|
@ -971,10 +1000,10 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
// Check for ~/Zotero/zotero.sqlite
|
// Check for ~/Zotero/zotero.sqlite
|
||||||
dataDir = Zotero.File.pathToFile(dataDir);
|
dataDir = Zotero.File.pathToFile(dataDir);
|
||||||
let dbFile = dataDir.clone();
|
let dbFile = dataDir.clone();
|
||||||
dbFile.append(ZOTERO_CONFIG.ID + '.sqlite');
|
dbFile.append(this.getDatabaseFilename());
|
||||||
if (dbFile.exists()) {
|
if (dbFile.exists()) {
|
||||||
Zotero.debug("Using data directory " + dataDir.path);
|
Zotero.debug("Using data directory " + dataDir.path);
|
||||||
_zoteroDirectory = dataDir;
|
this._cacheDataDirectory(dataDir.path);
|
||||||
|
|
||||||
// Set as a custom data directory so that 4.0 uses it
|
// Set as a custom data directory so that 4.0 uses it
|
||||||
this.setDataDirectory(dataDir.path);
|
this.setDataDirectory(dataDir.path);
|
||||||
|
@ -987,7 +1016,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
profileSubdir.append('zotero');
|
profileSubdir.append('zotero');
|
||||||
if (profileSubdir.exists()) {
|
if (profileSubdir.exists()) {
|
||||||
Zotero.debug("Using data directory " + profileSubdir.path);
|
Zotero.debug("Using data directory " + profileSubdir.path);
|
||||||
_zoteroDirectory = profileSubdir;
|
this._cacheDataDirectory(profileSubdir.path);
|
||||||
return profileSubdir.clone();
|
return profileSubdir.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1084,12 +1113,16 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
Zotero.File.createDirectoryIfMissing(file);
|
Zotero.File.createDirectoryIfMissing(file);
|
||||||
}
|
}
|
||||||
Zotero.debug("Using data directory " + file.path);
|
Zotero.debug("Using data directory " + file.path);
|
||||||
_zoteroDirectory = file;
|
this._cacheDataDirectory(file.path);
|
||||||
return file.clone();
|
return file.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.getDefaultDataDir = function () {
|
this.getDefaultDataDir = function () {
|
||||||
|
// Keep data directory siloed within profile directory for tests
|
||||||
|
if (Zotero.test) {
|
||||||
|
return OS.Path.join(OS.Constants.Path.profileDir, "test-data-dir");
|
||||||
|
}
|
||||||
return OS.Path.join(OS.Constants.Path.homeDir, ZOTERO_CONFIG.CLIENT_NAME);
|
return OS.Path.join(OS.Constants.Path.homeDir, ZOTERO_CONFIG.CLIENT_NAME);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1102,8 +1135,13 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getZoteroDatabase(name, ext){
|
|
||||||
name = name ? name + '.sqlite' : 'zotero.sqlite';
|
this.getDatabaseFilename = function (name) {
|
||||||
|
return (name || ZOTERO_CONFIG.ID) + '.sqlite';
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getZoteroDatabase = function (name, ext) {
|
||||||
|
name = this.getDatabaseFilename(name);
|
||||||
ext = ext ? '.' + ext : '';
|
ext = ext ? '.' + ext : '';
|
||||||
|
|
||||||
var file = Zotero.getZoteroDirectory();
|
var file = Zotero.getZoteroDirectory();
|
||||||
|
@ -1187,7 +1225,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
}
|
}
|
||||||
else if (file.directoryEntries.hasMoreElements()) {
|
else if (file.directoryEntries.hasMoreElements()) {
|
||||||
let dbfile = file.clone();
|
let dbfile = file.clone();
|
||||||
dbfile.append('zotero.sqlite');
|
dbfile.append(this.getDatabaseFilename());
|
||||||
|
|
||||||
// Warn if non-empty and no zotero.sqlite
|
// Warn if non-empty and no zotero.sqlite
|
||||||
if (!dbfile.exists()) {
|
if (!dbfile.exists()) {
|
||||||
|
@ -1260,6 +1298,11 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this._cacheDataDirectory = function (dir) {
|
||||||
|
_dataDirectory = dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
this.forceNewDataDirectory = function(win) {
|
this.forceNewDataDirectory = function(win) {
|
||||||
if (!win) {
|
if (!win) {
|
||||||
win = Services.wm.getMostRecentWindow('navigator:browser');
|
win = Services.wm.getMostRecentWindow('navigator:browser');
|
||||||
|
@ -1338,6 +1381,314 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this.isLegacyDataDirectory = function (dir) {
|
||||||
|
// 'zotero'
|
||||||
|
return OS.Path.basename(dir) == ZOTERO_CONFIG.ID
|
||||||
|
// '69pmactz.default'
|
||||||
|
&& OS.Path.basename(OS.Path.dirname(dir)).match(/^[0-9a-z]{8}\..+/)
|
||||||
|
// 'Profiles'
|
||||||
|
&& OS.Path.basename(OS.Path.dirname(OS.Path.dirname(dir))) == 'Profiles';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.canMigrateDataDirectory = function () {
|
||||||
|
// If (not default location) && (not useDataDir or within legacy location)
|
||||||
|
var currentDir = this.getZoteroDirectory().path;
|
||||||
|
if (currentDir == this.getDefaultDataDir()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy default or set to legacy default from other program (Standalone/Z4Fx) to share data
|
||||||
|
if (!Zotero.Prefs.get('useDataDir') || this.isLegacyDataDirectory(currentDir)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this.revealDataDirectory = function () {
|
||||||
|
return Zotero.File.reveal(this.getZoteroDirectory().path);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
this.DATA_DIR_MIGRATION_MARKER = 'migrate-dir';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate data directory if necessary and show any errors
|
||||||
|
*
|
||||||
|
* @param {String} dataDir - Current directory
|
||||||
|
* @param {String} targetDir - Target directory, which may be the same; except in tests, this is
|
||||||
|
* the default data directory
|
||||||
|
*/
|
||||||
|
this.checkForDataDirectoryMigration = Zotero.Promise.coroutine(function* (dataDir, newDir) {
|
||||||
|
let migrationMarker = OS.Path.join(dataDir, Zotero.DATA_DIR_MIGRATION_MARKER);
|
||||||
|
try {
|
||||||
|
var exists = yield OS.File.exists(migrationMarker)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
if (!exists) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let oldDir;
|
||||||
|
let partial = false;
|
||||||
|
|
||||||
|
// Not set to the default directory, so use current as old directory
|
||||||
|
if (dataDir != newDir) {
|
||||||
|
oldDir = dataDir;
|
||||||
|
}
|
||||||
|
// Unfinished migration -- already using new directory, so get path to previous
|
||||||
|
// directory from the migration marker
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
oldDir = yield Zotero.File.getContentsAsync(migrationMarker);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
partial = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let errors;
|
||||||
|
try {
|
||||||
|
errors = yield Zotero.migrateDataDirectory(oldDir, newDir, partial);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Complete failure (failed to create new directory, copy marker, or move database)
|
||||||
|
Zotero.debug("Migration failed", 1);
|
||||||
|
Zotero.logError(e);
|
||||||
|
|
||||||
|
let ps = Services.prompt;
|
||||||
|
let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
|
||||||
|
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
|
||||||
|
let index = ps.confirmEx(null,
|
||||||
|
Zotero.getString('dataDir.migration.failure.title'),
|
||||||
|
Zotero.getString('dataDir.migration.failure.full.text1')
|
||||||
|
+ "\n\n"
|
||||||
|
+ e
|
||||||
|
+ "\n\n"
|
||||||
|
+ Zotero.getString('dataDir.migration.failure.full.text2', Zotero.appName)
|
||||||
|
+ "\n\n"
|
||||||
|
+ Zotero.getString('dataDir.migration.failure.full.current', oldDir)
|
||||||
|
+ "\n\n"
|
||||||
|
+ Zotero.getString('dataDir.migration.failure.full.recommended', newDir),
|
||||||
|
buttonFlags,
|
||||||
|
Zotero.getString('dataDir.migration.failure.full.showCurrentDirectoryAndQuit', Zotero.appName),
|
||||||
|
Zotero.getString('general.notNow'),
|
||||||
|
null, null, {}
|
||||||
|
);
|
||||||
|
if (index == 0) {
|
||||||
|
yield Zotero.File.reveal(oldDir);
|
||||||
|
this.skipLoading = true;
|
||||||
|
Zotero.Utilities.Internal.quitZotero();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set data directory again
|
||||||
|
Zotero.debug("Using new data directory " + newDir);
|
||||||
|
this._cacheDataDirectory(newDir);
|
||||||
|
|
||||||
|
// At least the database was copied, but other things failed
|
||||||
|
if (errors.length) {
|
||||||
|
let ps = Services.prompt;
|
||||||
|
let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
|
||||||
|
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
|
||||||
|
let index = ps.confirmEx(null,
|
||||||
|
Zotero.getString('dataDir.migration.failure.title'),
|
||||||
|
Zotero.getString('dataDir.migration.failure.partial.text',
|
||||||
|
[ZOTERO_CONFIG.CLIENT_NAME, Zotero.appName])
|
||||||
|
+ "\n\n"
|
||||||
|
+ Zotero.getString('dataDir.migration.failure.partial.old', oldDir)
|
||||||
|
+ "\n\n"
|
||||||
|
+ Zotero.getString('dataDir.migration.failure.partial.new', newDir),
|
||||||
|
buttonFlags,
|
||||||
|
Zotero.getString('dataDir.migration.failure.partial.showDirectoriesAndQuit', Zotero.appName),
|
||||||
|
Zotero.getString('general.notNow'),
|
||||||
|
null, null, {}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Focus the first file/folder in the old directory
|
||||||
|
if (index == 0) {
|
||||||
|
try {
|
||||||
|
let it = new OS.File.DirectoryIterator(oldDir);
|
||||||
|
let entry;
|
||||||
|
try {
|
||||||
|
entry = yield it.next();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e != StopIteration) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
it.close();
|
||||||
|
}
|
||||||
|
if (entry) {
|
||||||
|
yield Zotero.File.reveal(entry.path);
|
||||||
|
}
|
||||||
|
// Focus the database file in the new directory
|
||||||
|
yield Zotero.File.reveal(OS.Path.join(newDir, this.getDatabaseFilename()));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.skipLoading = true;
|
||||||
|
Zotero.Utilities.Internal.quitZotero();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively moves data directory from one location to another
|
||||||
|
* and updates the data directory setting
|
||||||
|
*
|
||||||
|
* If moving the database file fails, an error is thrown.
|
||||||
|
* Otherwise, an array of errors is returned.
|
||||||
|
*
|
||||||
|
* @param {String} oldDir
|
||||||
|
* @param {String} newDir
|
||||||
|
* @return {Error[]}
|
||||||
|
*/
|
||||||
|
this.migrateDataDirectory = Zotero.Promise.coroutine(function* (oldDir, newDir, partial) {
|
||||||
|
var dbName = this.getDatabaseFilename();
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
function addError(e) {
|
||||||
|
errors.push(e);
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(yield OS.File.exists(oldDir))) {
|
||||||
|
Zotero.debug(`Old directory ${oldDir} doesn't exist -- nothing to migrate`);
|
||||||
|
try {
|
||||||
|
let newMigrationMarker = OS.Path.join(newDir, Zotero.DATA_DIR_MIGRATION_MARKER);
|
||||||
|
Zotero.debug("Removing " + newMigrationMarker);
|
||||||
|
yield OS.File.remove(newMigrationMarker);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partial) {
|
||||||
|
Zotero.debug(`Continuing data directory migration from ${oldDir} to ${newDir}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Zotero.debug(`Migrating data directory from ${oldDir} to ${newDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new directory
|
||||||
|
if (!partial) {
|
||||||
|
try {
|
||||||
|
yield OS.File.makeDir(
|
||||||
|
newDir,
|
||||||
|
{
|
||||||
|
ignoreExisting: false,
|
||||||
|
unixMode: 0o755
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// If default dir exists and is non-empty, move it out of the way
|
||||||
|
// ("Zotero-1", "Zotero-2", …)
|
||||||
|
if (e instanceof OS.File.Error && e.becauseExists) {
|
||||||
|
if (!(yield Zotero.File.directoryIsEmpty(newDir))) {
|
||||||
|
let i = 1;
|
||||||
|
while (true) {
|
||||||
|
let backupDir = newDir + "-" + i++;
|
||||||
|
if (yield OS.File.exists(backupDir)) {
|
||||||
|
if (i > 5) {
|
||||||
|
throw new Error("Too many backup directories "
|
||||||
|
+ "-- stopped at " + backupDir);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Zotero.debug(`Moving existing directory to ${backupDir}`);
|
||||||
|
yield Zotero.File.moveDirectory(newDir, backupDir);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
yield OS.File.makeDir(
|
||||||
|
newDir,
|
||||||
|
{
|
||||||
|
ignoreExisting: false,
|
||||||
|
unixMode: 0o755
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy marker
|
||||||
|
let oldMarkerFile = OS.Path.join(oldDir, this.DATA_DIR_MIGRATION_MARKER);
|
||||||
|
// Marker won't exist on subsequent attempts after partial failure
|
||||||
|
if (yield OS.File.exists(oldMarkerFile)) {
|
||||||
|
yield OS.File.copy(oldMarkerFile, OS.Path.join(newDir, this.DATA_DIR_MIGRATION_MARKER));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the data directory setting first. If moving the database fails, getZoteroDirectory()
|
||||||
|
// will continue to use the old directory based on the migration marker
|
||||||
|
Zotero.setDataDirectory(newDir);
|
||||||
|
|
||||||
|
// Move database
|
||||||
|
if (!partial) {
|
||||||
|
Zotero.debug("Moving " + dbName);
|
||||||
|
yield OS.File.move(OS.Path.join(oldDir, dbName), OS.Path.join(newDir, dbName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once the database has been moved, we can clear the migration marker from the old directory.
|
||||||
|
// If the migration is interrupted after this, it can be continued later based on the migration
|
||||||
|
// marker in the new directory.
|
||||||
|
try {
|
||||||
|
yield OS.File.remove(OS.Path.join(oldDir, this.DATA_DIR_MIGRATION_MARKER));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = errors.concat(yield Zotero.File.moveDirectory(
|
||||||
|
oldDir,
|
||||||
|
newDir,
|
||||||
|
{
|
||||||
|
allowExistingTarget: true,
|
||||||
|
// Don't overwrite root files (except for hidden files like .DS_Store)
|
||||||
|
noOverwrite: path => {
|
||||||
|
return OS.Path.dirname(path) == oldDir && !OS.Path.basename(path).startsWith('.')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
Zotero.logError("Not all files were transferred from " + oldDir + " to " + newDir);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
let newMigrationMarker = OS.Path.join(newDir, Zotero.DATA_DIR_MIGRATION_MARKER);
|
||||||
|
Zotero.debug("Removing " + newMigrationMarker);
|
||||||
|
yield OS.File.remove(newMigrationMarker);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
addError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch a file, the best way we can
|
* Launch a file, the best way we can
|
||||||
|
|
|
@ -197,6 +197,7 @@
|
||||||
<!ENTITY zotero.preferences.dataDir.custom "Custom:">
|
<!ENTITY zotero.preferences.dataDir.custom "Custom:">
|
||||||
<!ENTITY zotero.preferences.dataDir.choose "Choose...">
|
<!ENTITY zotero.preferences.dataDir.choose "Choose...">
|
||||||
<!ENTITY zotero.preferences.dataDir.reveal "Show Data Directory">
|
<!ENTITY zotero.preferences.dataDir.reveal "Show Data Directory">
|
||||||
|
<!ENTITY zotero.preferences.dataDir.migrate "Migrate Data Directory…">
|
||||||
|
|
||||||
<!ENTITY zotero.preferences.attachmentBaseDir.caption "Linked Attachment Base Directory">
|
<!ENTITY zotero.preferences.attachmentBaseDir.caption "Linked Attachment Base Directory">
|
||||||
<!ENTITY zotero.preferences.attachmentBaseDir.message "Zotero will use relative paths for linked file attachments within the base directory, allowing you to access files on different computers as long as the file structure within the base directory remains the same.">
|
<!ENTITY zotero.preferences.attachmentBaseDir.message "Zotero will use relative paths for linked file attachments within the base directory, allowing you to access files on different computers as long as the file structure within the base directory remains the same.">
|
||||||
|
|
|
@ -131,6 +131,16 @@ dataDir.selectedDirEmpty.useNewDir = Use the new directory?
|
||||||
dataDir.moveFilesToNewLocation = Be sure to move files from your existing Zotero data directory to the new location before reopening %1$S.
|
dataDir.moveFilesToNewLocation = Be sure to move files from your existing Zotero data directory to the new location before reopening %1$S.
|
||||||
dataDir.incompatibleDbVersion.title = Incompatible Database Version
|
dataDir.incompatibleDbVersion.title = Incompatible Database Version
|
||||||
dataDir.incompatibleDbVersion.text = The currently selected data directory is not compatible with Zotero Standalone, which can share a database only with Zotero for Firefox 2.1b3 or later.\n\nUpgrade to the latest version of Zotero for Firefox first or select a different data directory for use with Zotero Standalone.
|
dataDir.incompatibleDbVersion.text = The currently selected data directory is not compatible with Zotero Standalone, which can share a database only with Zotero for Firefox 2.1b3 or later.\n\nUpgrade to the latest version of Zotero for Firefox first or select a different data directory for use with Zotero Standalone.
|
||||||
|
dataDir.migration.failure.title = Data Directory Migration Error
|
||||||
|
dataDir.migration.failure.partial.text = Some files in your old %1$S data directory could not be transferred to the new location. You should close %2$S and attempt to move the remaining files manually.
|
||||||
|
dataDir.migration.failure.partial.old = Old directory: %S
|
||||||
|
dataDir.migration.failure.partial.new = New directory: %S
|
||||||
|
dataDir.migration.failure.partial.showDirectoriesAndQuit = Show Directories and Quit %S
|
||||||
|
dataDir.migration.failure.full.text1 = Your data directory could not be migrated.
|
||||||
|
dataDir.migration.failure.full.text2 = It is recommended that you close %S and manually move your data directory to the new default location.
|
||||||
|
dataDir.migration.failure.full.current = Current location: %S
|
||||||
|
dataDir.migration.failure.full.recommended = Recommended location: %S
|
||||||
|
dataDir.migration.failure.full.showCurrentDirectoryAndQuit = Show Current Directory and Quit %S
|
||||||
|
|
||||||
app.standalone = Zotero Standalone
|
app.standalone = Zotero Standalone
|
||||||
app.firefox = Zotero for Firefox
|
app.firefox = Zotero for Firefox
|
||||||
|
@ -621,6 +631,9 @@ zotero.preferences.advanced.resetTranslators = Reset Translators
|
||||||
zotero.preferences.advanced.resetTranslators.changesLost = Any new or modified translators will be lost.
|
zotero.preferences.advanced.resetTranslators.changesLost = Any new or modified translators will be lost.
|
||||||
zotero.preferences.advanced.resetStyles = Reset Styles
|
zotero.preferences.advanced.resetStyles = Reset Styles
|
||||||
zotero.preferences.advanced.resetStyles.changesLost = Any new or modified styles will be lost.
|
zotero.preferences.advanced.resetStyles.changesLost = Any new or modified styles will be lost.
|
||||||
|
zotero.preferences.advanced.migrateDataDir.title = Migrate Data Directory
|
||||||
|
zotero.preferences.advanced.migrateDataDir.directoryWillBeMoved = Your %1$S data directory will be moved to %2$S.
|
||||||
|
zotero.preferences.advanced.migrateDataDir.appMustBeRestarted = %S must be restarted to complete the migration.
|
||||||
|
|
||||||
zotero.preferences.advanced.debug.title = Debug Output Submitted
|
zotero.preferences.advanced.debug.title = Debug Output Submitted
|
||||||
zotero.preferences.advanced.debug.sent = Debug output has been sent to the Zotero server.\n\nThe Debug ID is D%S.
|
zotero.preferences.advanced.debug.sent = Debug output has been sent to the Zotero server.\n\nThe Debug ID is D%S.
|
||||||
|
|
314
test/tests/zoteroTest.js
Normal file
314
test/tests/zoteroTest.js
Normal file
|
@ -0,0 +1,314 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("Zotero Core Functions", function () {
|
||||||
|
var tmpDir, oldDir, newDir, dbFilename, oldDBFile, newDBFile, oldStorageDir, newStorageDir,
|
||||||
|
oldTranslatorsDir, newTranslatorsDir, translatorName1, translatorName2,
|
||||||
|
oldStorageDir1, newStorageDir1, storageFile1, oldStorageDir2, newStorageDir2, storageFile2,
|
||||||
|
str1, str2, str3, str4, str5, str6,
|
||||||
|
oldMigrationMarker, newMigrationMarker,
|
||||||
|
stub1, stub2, stub3;
|
||||||
|
|
||||||
|
before(function* () {
|
||||||
|
tmpDir = yield getTempDirectory();
|
||||||
|
oldDir = OS.Path.join(tmpDir, "old");
|
||||||
|
newDir = OS.Path.join(tmpDir, "new");
|
||||||
|
dbFilename = Zotero.getDatabaseFilename();
|
||||||
|
oldDBFile = OS.Path.join(oldDir, dbFilename);
|
||||||
|
newDBFile = OS.Path.join(newDir, dbFilename);
|
||||||
|
oldStorageDir = OS.Path.join(oldDir, "storage");
|
||||||
|
newStorageDir = OS.Path.join(newDir, "storage");
|
||||||
|
oldTranslatorsDir = OS.Path.join(oldDir, "translators");
|
||||||
|
newTranslatorsDir = OS.Path.join(newDir, "translators");
|
||||||
|
translatorName1 = 'a.js';
|
||||||
|
translatorName2 = 'b.js';
|
||||||
|
oldStorageDir1 = OS.Path.join(oldStorageDir, 'AAAAAAAA');
|
||||||
|
newStorageDir1 = OS.Path.join(newStorageDir, 'AAAAAAAA');
|
||||||
|
storageFile1 = 'test.pdf';
|
||||||
|
oldStorageDir2 = OS.Path.join(oldStorageDir, 'BBBBBBBB');
|
||||||
|
newStorageDir2 = OS.Path.join(newStorageDir, 'BBBBBBBB');
|
||||||
|
storageFile2 = 'test.html';
|
||||||
|
str1 = '1';
|
||||||
|
str2 = '2';
|
||||||
|
str3 = '3';
|
||||||
|
str4 = '4';
|
||||||
|
str5 = '5';
|
||||||
|
str6 = '6';
|
||||||
|
oldMigrationMarker = OS.Path.join(oldDir, Zotero.DATA_DIR_MIGRATION_MARKER);
|
||||||
|
newMigrationMarker = OS.Path.join(newDir, Zotero.DATA_DIR_MIGRATION_MARKER);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function* () {
|
||||||
|
stub1 = sinon.stub(Zotero, "setDataDirectory");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function* () {
|
||||||
|
yield OS.File.removeDir(oldDir);
|
||||||
|
yield OS.File.removeDir(newDir);
|
||||||
|
Zotero._cacheDataDirectory(false);
|
||||||
|
|
||||||
|
stub1.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
var disableCommandMode = function () {
|
||||||
|
// Force non-mv mode
|
||||||
|
var origFunc = OS.File.exists;
|
||||||
|
stub2 = sinon.stub(OS.File, "exists", function (path) {
|
||||||
|
if (path == '/bin/mv') {
|
||||||
|
return Zotero.Promise.resolve(false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return origFunc(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var resetCommandMode = function () {
|
||||||
|
stub2.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
var populateDataDirectory = Zotero.Promise.coroutine(function* (dir, srcDir) {
|
||||||
|
yield OS.File.makeDir(dir, { unixMode: 0o755 });
|
||||||
|
let storageDir = OS.Path.join(dir, 'storage');
|
||||||
|
let storageDir1 = OS.Path.join(storageDir, 'AAAAAAAA');
|
||||||
|
let storageDir2 = OS.Path.join(storageDir, 'BBBBBBBB');
|
||||||
|
let translatorsDir = OS.Path.join(dir, 'translators');
|
||||||
|
let migrationMarker = OS.Path.join(dir, Zotero.DATA_DIR_MIGRATION_MARKER);
|
||||||
|
|
||||||
|
// Database
|
||||||
|
yield Zotero.File.putContentsAsync(OS.Path.join(dir, dbFilename), str1);
|
||||||
|
// Database backup
|
||||||
|
yield Zotero.File.putContentsAsync(OS.Path.join(dir, dbFilename + '.bak'), str2);
|
||||||
|
// 'storage' directory
|
||||||
|
yield OS.File.makeDir(storageDir, { unixMode: 0o755 });
|
||||||
|
// 'storage' folders
|
||||||
|
yield OS.File.makeDir(storageDir1, { unixMode: 0o755 });
|
||||||
|
yield Zotero.File.putContentsAsync(OS.Path.join(storageDir1, storageFile1), str2);
|
||||||
|
yield OS.File.makeDir(storageDir2, { unixMode: 0o755 });
|
||||||
|
yield Zotero.File.putContentsAsync(OS.Path.join(storageDir2, storageFile2), str3);
|
||||||
|
// 'translators' and some translators
|
||||||
|
yield OS.File.makeDir(translatorsDir, { unixMode: 0o755 });
|
||||||
|
yield Zotero.File.putContentsAsync(OS.Path.join(translatorsDir, translatorName1), str4);
|
||||||
|
yield Zotero.File.putContentsAsync(OS.Path.join(translatorsDir, translatorName2), str5);
|
||||||
|
// Migration marker
|
||||||
|
yield Zotero.File.putContentsAsync(migrationMarker, srcDir || dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
var checkMigration = Zotero.Promise.coroutine(function* (options = {}) {
|
||||||
|
if (!options.skipOldDir) {
|
||||||
|
assert.isFalse(yield OS.File.exists(oldDir));
|
||||||
|
}
|
||||||
|
yield assert.eventually.equal(Zotero.File.getContentsAsync(newDBFile), str1);
|
||||||
|
yield assert.eventually.equal(Zotero.File.getContentsAsync(newDBFile + '.bak'), str2);
|
||||||
|
if (!options.skipStorageFile1) {
|
||||||
|
yield assert.eventually.equal(
|
||||||
|
Zotero.File.getContentsAsync(OS.Path.join(newStorageDir1, storageFile1)), str2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
yield assert.eventually.equal(
|
||||||
|
Zotero.File.getContentsAsync(OS.Path.join(newStorageDir2, storageFile2)), str3
|
||||||
|
);
|
||||||
|
yield assert.eventually.equal(
|
||||||
|
Zotero.File.getContentsAsync(OS.Path.join(newTranslatorsDir, translatorName1)), str4
|
||||||
|
);
|
||||||
|
yield assert.eventually.equal(
|
||||||
|
Zotero.File.getContentsAsync(OS.Path.join(newTranslatorsDir, translatorName2)), str5
|
||||||
|
);
|
||||||
|
if (!options.skipNewMarker) {
|
||||||
|
assert.isFalse(yield OS.File.exists(newMigrationMarker));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.skipSetDataDirectory) {
|
||||||
|
assert.ok(stub1.calledOnce);
|
||||||
|
assert.ok(stub1.calledWith(newDir));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("#checkForDataDirectoryMigration()", function () {
|
||||||
|
let stub3;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
disableCommandMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
resetCommandMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show error on partial failure", function* () {
|
||||||
|
yield populateDataDirectory(oldDir);
|
||||||
|
|
||||||
|
let origFunc = OS.File.move;
|
||||||
|
let stub3 = sinon.stub(OS.File, "move", function () {
|
||||||
|
if (OS.Path.basename(arguments[0]) == storageFile1) {
|
||||||
|
return Zotero.Promise.reject(new Error("Error"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return origFunc(...arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let stub4 = sinon.stub(Zotero.File, "reveal").returns(Zotero.Promise.resolve());
|
||||||
|
let stub5 = sinon.stub(Zotero.Utilities.Internal, "quitZotero");
|
||||||
|
|
||||||
|
var promise = waitForDialog();
|
||||||
|
yield Zotero.checkForDataDirectoryMigration(oldDir, newDir);
|
||||||
|
yield promise;
|
||||||
|
|
||||||
|
assert.isTrue(stub4.calledTwice);
|
||||||
|
assert.isTrue(stub4.getCall(0).calledWith(oldStorageDir));
|
||||||
|
assert.isTrue(stub4.getCall(1).calledWith(newDBFile));
|
||||||
|
assert.isTrue(stub5.called);
|
||||||
|
|
||||||
|
stub3.restore();
|
||||||
|
stub4.restore();
|
||||||
|
stub5.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show error on full failure", function* () {
|
||||||
|
yield populateDataDirectory(oldDir);
|
||||||
|
|
||||||
|
let origFunc = OS.File.move;
|
||||||
|
let stub3 = sinon.stub(OS.File, "move", function () {
|
||||||
|
if (OS.Path.basename(arguments[0]) == dbFilename) {
|
||||||
|
return Zotero.Promise.reject(new Error("Error"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return origFunc(...arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let stub4 = sinon.stub(Zotero.File, "reveal").returns(Zotero.Promise.resolve());
|
||||||
|
let stub5 = sinon.stub(Zotero.Utilities.Internal, "quitZotero");
|
||||||
|
|
||||||
|
var promise = waitForDialog();
|
||||||
|
yield Zotero.checkForDataDirectoryMigration(oldDir, newDir);
|
||||||
|
yield promise;
|
||||||
|
|
||||||
|
assert.isTrue(stub4.calledOnce);
|
||||||
|
assert.isTrue(stub4.calledWith(oldDir));
|
||||||
|
assert.isTrue(stub5.called);
|
||||||
|
|
||||||
|
stub3.restore();
|
||||||
|
stub4.restore();
|
||||||
|
stub5.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove marker if old directory doesn't exist", function* () {
|
||||||
|
yield populateDataDirectory(newDir, oldDir);
|
||||||
|
yield Zotero.checkForDataDirectoryMigration(newDir, newDir);
|
||||||
|
yield checkMigration({
|
||||||
|
skipSetDataDirectory: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("#migrateDataDirectory()", function () {
|
||||||
|
// Define tests and store for running in non-mv mode
|
||||||
|
var tests = [];
|
||||||
|
function add(desc, fn) {
|
||||||
|
it(desc, fn);
|
||||||
|
tests.push([desc, fn]);
|
||||||
|
}
|
||||||
|
|
||||||
|
add("should move all files and folders", function* () {
|
||||||
|
yield populateDataDirectory(oldDir);
|
||||||
|
yield Zotero.migrateDataDirectory(oldDir, newDir);
|
||||||
|
yield checkMigration();
|
||||||
|
});
|
||||||
|
|
||||||
|
add("should resume partial migration with just marker copied", function* () {
|
||||||
|
yield populateDataDirectory(oldDir);
|
||||||
|
yield OS.File.makeDir(newDir, { unixMode: 0o755 });
|
||||||
|
|
||||||
|
yield OS.File.copy(oldMigrationMarker, newMigrationMarker);
|
||||||
|
|
||||||
|
yield Zotero.migrateDataDirectory(oldDir, newDir, true);
|
||||||
|
yield checkMigration();
|
||||||
|
});
|
||||||
|
|
||||||
|
add("should resume partial migration with database moved", function* () {
|
||||||
|
yield populateDataDirectory(oldDir);
|
||||||
|
yield OS.File.makeDir(newDir, { unixMode: 0o755 });
|
||||||
|
|
||||||
|
yield OS.File.copy(oldMigrationMarker, newMigrationMarker);
|
||||||
|
yield OS.File.move(OS.Path.join(oldDir, dbFilename), OS.Path.join(newDir, dbFilename));
|
||||||
|
|
||||||
|
yield Zotero.migrateDataDirectory(oldDir, newDir, true);
|
||||||
|
yield checkMigration();
|
||||||
|
});
|
||||||
|
|
||||||
|
add("should resume partial migration with some storage directories moved", function* () {
|
||||||
|
yield populateDataDirectory(oldDir);
|
||||||
|
yield populateDataDirectory(newDir, oldDir);
|
||||||
|
|
||||||
|
// Moved: DB, DB backup, one storage dir
|
||||||
|
// Not moved: one storage dir, translators dir
|
||||||
|
yield OS.File.remove(oldDBFile);
|
||||||
|
yield OS.File.remove(oldDBFile + '.bak');
|
||||||
|
yield OS.File.removeDir(oldStorageDir1);
|
||||||
|
yield OS.File.removeDir(newTranslatorsDir);
|
||||||
|
yield OS.File.removeDir(newStorageDir2);
|
||||||
|
|
||||||
|
yield Zotero.migrateDataDirectory(oldDir, newDir, true);
|
||||||
|
yield checkMigration();
|
||||||
|
});
|
||||||
|
|
||||||
|
add("should move existing directory out of the way", function* () {
|
||||||
|
yield populateDataDirectory(oldDir);
|
||||||
|
yield OS.File.makeDir(newDir, { unixMode: 0o755 });
|
||||||
|
yield Zotero.File.putContentsAsync(OS.Path.join(newDir, 'existing'), '');
|
||||||
|
|
||||||
|
yield Zotero.migrateDataDirectory(oldDir, newDir);
|
||||||
|
yield checkMigration();
|
||||||
|
|
||||||
|
assert.isTrue(yield OS.File.exists(OS.Path.join(newDir + "-1", 'existing')));
|
||||||
|
yield OS.File.removeDir(newDir + "-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run all tests again without using mv
|
||||||
|
//
|
||||||
|
// On Windows these will just be duplicates of the above tests.
|
||||||
|
describe("non-mv mode", function () {
|
||||||
|
tests.forEach(arr => {
|
||||||
|
it(arr[0] + " [non-mv]", arr[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
disableCommandMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
resetCommandMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle partial failure", function* () {
|
||||||
|
yield populateDataDirectory(oldDir);
|
||||||
|
|
||||||
|
let origFunc = OS.File.move;
|
||||||
|
let stub3 = sinon.stub(OS.File, "move", function () {
|
||||||
|
if (OS.Path.basename(arguments[0]) == storageFile1) {
|
||||||
|
return Zotero.Promise.reject(new Error("Error"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return origFunc(...arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
yield Zotero.migrateDataDirectory(oldDir, newDir);
|
||||||
|
|
||||||
|
stub3.restore();
|
||||||
|
|
||||||
|
yield checkMigration({
|
||||||
|
skipOldDir: true,
|
||||||
|
skipStorageFile1: true,
|
||||||
|
skipNewMarker: true
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.isTrue(yield OS.File.exists(OS.Path.join(oldStorageDir1, storageFile1)));
|
||||||
|
assert.isFalse(yield OS.File.exists(OS.Path.join(oldStorageDir2, storageFile2)));
|
||||||
|
assert.isFalse(yield OS.File.exists(oldTranslatorsDir));
|
||||||
|
assert.isTrue(yield OS.File.exists(newMigrationMarker));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user