Point other profiles to new data dir after migration (+ code reorg)

Look for other profiles, from both apps (Firefox and Standalone), that
point to the data directory being migrated and update prefs.js in those
profiles to point to the new location.

Also reorganize code into Zotero.Profile and Zotero.DataDirectory
namespaces
This commit is contained in:
Dan Stillman 2016-11-26 22:41:26 -05:00
parent c0bf2b91f6
commit 4c0abb6816
13 changed files with 1338 additions and 919 deletions

View file

@ -230,7 +230,7 @@ var ZoteroOverlay = new function()
// Warn about unsafe data directory on first display // Warn about unsafe data directory on first display
let dataDir = Zotero.getZoteroDirectory(); let dataDir = Zotero.getZoteroDirectory();
Zotero.checkForUnsafeDataDirectory(dataDir.path); Zotero.DataDirectory.checkForUnsafeLocation(dataDir.path); // async
// Make sure tags splitter isn't missing for people upgrading from <2.0b7 // Make sure tags splitter isn't missing for people upgrading from <2.0b7
document.getElementById('zotero-tags-splitter').collapsed = false; document.getElementById('zotero-tags-splitter').collapsed = false;

View file

@ -38,8 +38,8 @@ Zotero_Preferences.Advanced = {
migrateDataDirectory: Zotero.Promise.coroutine(function* () { migrateDataDirectory: Zotero.Promise.coroutine(function* () {
var currentDir = Zotero.getZoteroDirectory().path; var currentDir = Zotero.DataDirectory.dir;
var defaultDir = Zotero.getDefaultDataDir(); var defaultDir = Zotero.DataDirectory.defaultDir;
if (currentDir == defaultDir) { if (currentDir == defaultDir) {
Zotero.debug("Already using default directory"); Zotero.debug("Already using default directory");
return; return;
@ -65,7 +65,7 @@ Zotero_Preferences.Advanced = {
); );
if (index == 0) { if (index == 0) {
yield Zotero.markDataDirectoryForMigration(currentDir); yield Zotero.DataDirectory.markForMigration(currentDir);
Zotero.Utilities.Internal.quitZotero(true); Zotero.Utilities.Internal.quitZotero(true);
} }
}), }),
@ -215,13 +215,13 @@ Zotero_Preferences.Advanced = {
onDataDirLoad: function () { onDataDirLoad: function () {
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 currentDir = Zotero.DataDirectory.dir;
var currentDir = Zotero.getZoteroDirectory().path; var defaultDataDir = Zotero.DataDirectory.defaultDir;
// 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 || currentDir == defaultDataDir) { if (useDataDir || currentDir == defaultDataDir) {
document.getElementById('default-data-dir').setAttribute( document.getElementById('default-data-dir').setAttribute(
'label', Zotero.getString('dataDir.default', Zotero.getDefaultDataDir()) 'label', Zotero.getString('dataDir.default', Zotero.DataDirectory.defaultDir)
); );
} }
@ -232,14 +232,14 @@ Zotero_Preferences.Advanced = {
document.getElementById('data-dir-path').setAttribute('disabled', !useDataDir); document.getElementById('data-dir-path').setAttribute('disabled', !useDataDir);
document.getElementById('migrate-data-dir').setAttribute( document.getElementById('migrate-data-dir').setAttribute(
'hidden', !Zotero.canMigrateDataDirectory() 'hidden', !Zotero.DataDirectory.canMigrate()
); );
return useDataDir; return useDataDir;
}, },
onDataDirUpdate: function (event) { onDataDirUpdate: Zotero.Promise.coroutine(function* (event) {
var radiogroup = document.getElementById('data-dir'); 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;
@ -250,11 +250,13 @@ 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 () { yield Zotero.DataDirectory.choose(
Zotero_Preferences.openURL('https://zotero.org/support/zotero_data'); true,
}); !newUseDataDir,
() => 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) {
@ -269,7 +271,7 @@ Zotero_Preferences.Advanced = {
var prefValue = Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir'); var prefValue = Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir');
// Don't show path if the default // Don't show path if the default
if (prefValue == Zotero.getDefaultDataDir()) { if (prefValue == Zotero.DataDirectory.defaultDir) {
return ''; return '';
} }
@ -285,7 +287,7 @@ Zotero_Preferences.Advanced = {
var dataDir = Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir'); var dataDir = Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir');
// Default home directory location // Default home directory location
if (dataDir == Zotero.getDefaultDataDir()) { if (dataDir == Zotero.DataDirectory.defaultDir) {
return true; return true;
} }

View file

@ -185,7 +185,7 @@
<radiogroup id="data-dir" <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="Zotero_Preferences.Advanced.onDataDirUpdate(event);">
<radio id="default-data-dir" 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"/>
@ -199,7 +199,7 @@
<hbox> <hbox>
<button label="&zotero.preferences.dataDir.reveal;" <button label="&zotero.preferences.dataDir.reveal;"
oncommand="Zotero.revealDataDirectory()"/> oncommand="Zotero.DataDirectory.reveal()"/>
<button id="migrate-data-dir" label="&zotero.preferences.dataDir.migrate;" <button id="migrate-data-dir" label="&zotero.preferences.dataDir.migrate;"
oncommand="Zotero_Preferences.Advanced.migrateDataDirectory()" hidden="true"/> oncommand="Zotero_Preferences.Advanced.migrateDataDirectory()" hidden="true"/>
</hbox> </hbox>

View file

@ -0,0 +1,847 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2016 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
Zotero.DataDirectory = {
MIGRATION_MARKER: 'migrate-dir',
get dir() {
if (!this._dir) {
throw new Error("Data directory not initialized");
}
return this._dir;
},
get defaultDir() {
// Use special data directory for tests
if (Zotero.test) {
return OS.Path.join(OS.Path.dirname(OS.Constants.Path.profileDir), "Zotero");
}
return OS.Path.join(OS.Constants.Path.homeDir, ZOTERO_CONFIG.CLIENT_NAME);
},
get legacyDirName() {
return ZOTERO_CONFIG.ID;
},
_dir: null,
_warnOnUnsafeLocation: true,
init: Zotero.Promise.coroutine(function* () {
var file;
if (Zotero.Prefs.get('useDataDir')) {
let prefVal = Zotero.Prefs.get('dataDir');
// Convert old persistent descriptor pref to string path and clear obsolete lastDataDir pref
//
// persistentDescriptor now appears to return (and parse) a string path anyway on macOS,
// which is the only place where it didn't use a string path to begin with, but be explicit
// just in case there's some difference.
//
// A post-Mozilla prefs migration should do this same check, and then this conditional can
// be removed.
if (Zotero.Prefs.get('lastDataDir')) {
let nsIFile;
try {
nsIFile = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
nsIFile.persistentDescriptor = prefVal;
}
catch (e) {
Zotero.debug("Persistent descriptor in extensions.zotero.dataDir did not resolve", 1);
e = { name: "NS_ERROR_FILE_NOT_FOUND" };
throw e;
}
// This removes lastDataDir
this.set(nsIFile.path);
file = nsIFile.path;
}
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.MIGRATION_MARKER);
let dbFile = OS.Path.join(prefVal, this.getDatabaseFilename());
if ((yield OS.File.exists(migrationMarker)) && !(OS.File.exists(dbFile))) {
let contents = yield Zotero.File.getContentsAsync(migrationMarker);
try {
let { sourceDir } = JSON.parse(contents);
file = OS.Path.normalize(sourceDir);
}
catch (e) {
Zotero.logError(e);
Zotero.debug(`Invalid marker file:\n\n${contents}`, 1);
e = { name: "NS_ERROR_FILE_NOT_FOUND" };
throw e;
}
}
else {
try {
file = OS.Path.normalize(prefVal);
}
catch (e) {
Zotero.logError(e);
Zotero.debug(`Invalid path '${prefVal}' in dataDir pref`, 1);
e = { name: "NS_ERROR_FILE_NOT_FOUND" };
throw e;
}
}
}
if (!(yield OS.File.exists(file)) && file != this.defaultDir) {
// If set to a legacy directory that doesn't exist, forget about it and just use the
// new default location, which will either exist or be created below. The most likely
// cause of this is a migration, so don't bother looking in other-app profiles.
if (this.isLegacy(file)) {
let newDefault = this.defaultDir;
Zotero.debug(`Legacy data directory ${file} from pref not found `
+ `-- reverting to ${newDefault}`, 1);
file = newDefault;
this.set(newDefault);
}
// For other custom directories that don't exist, show not-found dialog
else {
Zotero.debug("Custom data directory ${file} not found", 1);
throw { name: "NS_ERROR_FILE_NOT_FOUND" };
}
}
}
else {
let dataDir = this.defaultDir;
//
// TODO: asyncify
//
// Check for ~/Zotero/zotero.sqlite
let dbFile = OS.Path.join(dataDir, this.getDatabaseFilename());
if (yield OS.File.exists(dbFile)) {
Zotero.debug("Using data directory " + dataDir);
this._cache(dataDir);
// Set as a custom data directory so that 4.0 uses it
this.set(dataDir);
return dataDir;
}
// Check for 'zotero' dir in profile dir
let profileSubdir = OS.Path.join(Zotero.Profile.dir, this.legacyDirName);
if (yield OS.File.exists(profileSubdir)) {
Zotero.debug("Using data directory " + profileSubdir);
this._cache(profileSubdir);
return profileSubdir;
}
//
// If Standalone and no directory yet, check Firefox directory, or vice versa
//
let profilesParent = OS.Path.dirname(Zotero.Profile.getOtherAppProfilesDir());
Zotero.debug("Looking for existing profile in " + profilesParent);
// get default profile
var defProfile;
try {
defProfile = yield Zotero.Profile.getDefaultInProfilesDir(profilesParent);
} catch(e) {
Zotero.debug("An error occurred locating the Firefox profile; not "+
"attempting to migrate from Zotero for Firefox");
Zotero.logError(e);
}
if(defProfile) {
// get Zotero directory
let profileDir = defProfile[0].path;
Zotero.debug("Found default profile at " + profileDir);
// copy prefs
let prefsFile = OS.Path.join(profileDir, "prefs.js");
if (yield OS.File.exists(prefsFile)) {
// build sandbox
var sandbox = new Components.utils.Sandbox("http://www.example.com/");
Components.utils.evalInSandbox(
"var prefs = {};"+
"function user_pref(key, val) {"+
"prefs[key] = val;"+
"}"
, sandbox);
// remove comments
var prefsJs = yield Zotero.File.getContentsAsync(prefsFile);
prefsJs = prefsJs.replace(/^#[^\r\n]*$/mg, "");
// evaluate
Components.utils.evalInSandbox(prefsJs, sandbox);
var prefs = sandbox.prefs;
for(var key in prefs) {
if(key.substr(0, ZOTERO_CONFIG.PREF_BRANCH.length) === ZOTERO_CONFIG.PREF_BRANCH
&& key !== "extensions.zotero.firstRun2") {
Zotero.Prefs.set(key.substr(ZOTERO_CONFIG.PREF_BRANCH.length), prefs[key]);
}
}
// If data directory setting was transferred, use that
if (Zotero.Prefs.get('useDataDir')) {
return this.init();
}
}
// If there's a data directory in the default profile for the alternative app, use that
let zoteroDir = OS.Path.join(profileDir, this.legacyDirName);
if (yield OS.File.exists(zoteroDir)) {
this.set(zoteroDir);
file = zoteroDir;
}
}
if (!file) {
file = dataDir;
}
}
Zotero.debug("Using data directory " + file);
yield Zotero.File.createDirectoryIfMissingAsync(file);
this._cache(file);
}),
_cache: function (dir) {
this._dir = dir;
},
/**
* @return {Boolean} - True if the directory changed; false otherwise
*/
set: function (path) {
var origPath = Zotero.Prefs.get('dataDir');
Zotero.Prefs.set('dataDir', path);
// Clear legacy pref
Zotero.Prefs.clear('lastDataDir');
Zotero.Prefs.set('useDataDir', true);
return path != origPath;
},
choose: Zotero.Promise.coroutine(function* (forceQuitNow, useHomeDir, moreInfoCallback) {
var win = Services.wm.getMostRecentWindow('navigator:browser');
var ps = Services.prompt;
if (useHomeDir) {
let changed = this.set(this.defaultDir);
if (!changed) {
return false;
}
}
else {
var nsIFilePicker = Components.interfaces.nsIFilePicker;
while (true) {
var fp = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(nsIFilePicker);
fp.init(win, Zotero.getString('dataDir.selectDir'), nsIFilePicker.modeGetFolder);
fp.displayDirectory = Zotero.File.pathToFile(this.dir);
fp.appendFilters(nsIFilePicker.filterAll);
if (fp.show() == nsIFilePicker.returnOK) {
var file = fp.file;
let dialogText = '';
let dialogTitle = '';
if (file.path == (Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir'))) {
Zotero.debug("Data directory hasn't changed");
return false;
}
// In dropbox folder
if (Zotero.File.isDropboxDirectory(file.path)) {
dialogTitle = Zotero.getString('general.warning');
dialogText = Zotero.getString('dataDir.unsafeLocation.selected.dropbox') + "\n\n"
+ Zotero.getString('dataDir.unsafeLocation.selected.useAnyway');
}
else if (file.directoryEntries.hasMoreElements()) {
let dbfile = file.clone();
dbfile.append(this.getDatabaseFilename());
// Warn if non-empty and no zotero.sqlite
if (!dbfile.exists()) {
dialogTitle = Zotero.getString('dataDir.selectedDirNonEmpty.title');
dialogText = Zotero.getString('dataDir.selectedDirNonEmpty.text');
}
}
// Directory empty
else {
dialogTitle = Zotero.getString('dataDir.selectedDirEmpty.title');
dialogText = Zotero.getString('dataDir.selectedDirEmpty.text', Zotero.appName) + '\n\n'
+ Zotero.getString('dataDir.selectedDirEmpty.useNewDir');
}
// Warning dialog to be displayed
if(dialogText !== '') {
let buttonFlags = ps.STD_YES_NO_BUTTONS;
if (moreInfoCallback) {
buttonFlags += ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING;
}
let index = ps.confirmEx(null,
dialogTitle,
dialogText,
buttonFlags,
null,
null,
moreInfoCallback ? Zotero.getString('general.moreInformation') : null,
null, {});
// Not OK -- return to file picker
if (index == 1) {
continue;
}
else if (index == 2) {
setTimeout(function () {
moreInfoCallback();
}, 1);
return false;
}
}
this.set(file.path);
break;
}
else {
return false;
}
}
}
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING);
if (!forceQuitNow) {
buttonFlags += (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
}
var app = Zotero.appName;
var index = ps.confirmEx(null,
Zotero.getString('general.restartRequired'),
Zotero.getString('general.restartRequiredForChange', app)
+ "\n\n" + Zotero.getString('dataDir.moveFilesToNewLocation', app),
buttonFlags,
Zotero.getString('general.quitApp', app),
forceQuitNow ? null : Zotero.getString('general.restartLater'),
null, null, {});
if (forceQuitNow || index == 0) {
Services.startup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit);
}
return useHomeDir ? true : file;
}),
forceChange: function (win) {
if (!win) {
win = Services.wm.getMostRecentWindow('navigator:browser');
}
var ps = Services.prompt;
var nsIFilePicker = Components.interfaces.nsIFilePicker;
while (true) {
var fp = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(nsIFilePicker);
fp.init(win, Zotero.getString('dataDir.selectNewDir', Zotero.clientName), nsIFilePicker.modeGetFolder);
fp.displayDirectory = Zotero.File.pathToFile(this.dir);
fp.appendFilters(nsIFilePicker.filterAll);
if (fp.show() == nsIFilePicker.returnOK) {
var file = fp.file;
if (file.directoryEntries.hasMoreElements()) {
ps.alert(null,
Zotero.getString('dataDir.mustSelectEmpty.title'),
Zotero.getString('dataDir.mustSelectEmpty.text')
);
continue;
}
this.set(file.path);
return file;
} else {
return false;
}
}
},
checkForUnsafeLocation: Zotero.Promise.coroutine(function* (path) {
if (this._warnOnUnsafeLocation && Zotero.File.isDropboxDirectory(path)
&& Zotero.Prefs.get('warnOnUnsafeDataDir')) {
this._warnOnUnsafeLocation = false;
let check = {value: false};
let index = Services.prompt.confirmEx(
null,
Zotero.getString('general.warning'),
Zotero.getString('dataDir.unsafeLocation.existing.dropbox') + "\n\n"
+ Zotero.getString('dataDir.unsafeLocation.existing.chooseDifferent'),
Services.prompt.STD_YES_NO_BUTTONS,
null, null, null,
Zotero.getString('general.dontShowWarningAgain'),
check
);
// Yes - display dialog.
if (index == 0) {
yield this.choose(true);
}
if (check.value) {
Zotero.Prefs.set('warnOnUnsafeDataDir', false);
}
}
}),
isLegacy: function (dir) {
// 'zotero'
return OS.Path.basename(dir) == this.legacyDirName
// '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';
},
canMigrate: function () {
// If (not default location) && (not useDataDir or within legacy location)
var currentDir = this.dir;
if (currentDir == this.defaultDir) {
return false;
}
// Legacy default or set to legacy default from other program (Standalone/Z4Fx) to share data
if (!Zotero.Prefs.get('useDataDir') || this.isLegacy(currentDir)) {
return true;
}
return false;
},
reveal: function () {
return Zotero.File.reveal(this.dir);
},
markForMigration: function (dir, automatic = false) {
return Zotero.File.putContentsAsync(
OS.Path.join(dir, this.MIGRATION_MARKER),
JSON.stringify({
sourceDir: dir,
automatic
})
);
},
/**
* 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
*/
checkForMigration: Zotero.Promise.coroutine(function* (dataDir, newDir) {
if (!this.canMigrate(dataDir)) {
return false;
}
let migrationMarker = OS.Path.join(dataDir, this.MIGRATION_MARKER);
try {
var exists = yield OS.File.exists(migrationMarker)
}
catch (e) {
Zotero.logError(e);
}
let automatic = false;
if (!exists) {
// Migrate automatically on macOS and Linux -- this should match the check in
// Zotero.File.moveDirectory()
if (!Zotero.isWin && (yield OS.File.exists("/bin/mv"))) {
automatic = true;
}
else {
return false;
}
}
// Check for an existing pipe from other running versions of Zotero pointing at the same data
// directory, and skip migration if found
try {
let foundPipe = yield Zotero.IPC.pipeExists();
if (foundPipe) {
Zotero.debug("Found existing pipe -- skipping migration");
return false;
}
}
catch (e) {
Zotero.logError("Error checking for pipe -- skipping migration:\n\n" + e);
return false;
}
// If there are other profiles pointing to the old directory, make sure we can edit the prefs.js
// file before doing anything, or else we risk orphaning a 4.0 installation
try {
let otherProfiles = yield Zotero.Profile.findOtherProfilesUsingDataDirectory(dataDir);
// 'touch' each prefs.js file to make sure we can access it
for (let dir of otherProfiles) {
let prefs = OS.Path.join(dir, "prefs.js");
yield OS.File.setDates(prefs);
}
}
catch (e) {
Zotero.logError(e);
Zotero.logError("Error checking other profiles -- skipping migration");
// TODO: After 5.0 has been out a while, remove this and let migration continue even if
// other profile directories can't be altered, with the assumption that they'll be running
// 5.0 already and will be pick up the new data directory automatically.
return false;
}
if (automatic) {
yield this.markForMigration(dataDir, true);
}
let sourceDir;
let oldDir;
let partial = false;
// Check whether this is an automatic or manual migration
let contents;
try {
contents = yield Zotero.File.getContentsAsync(migrationMarker);
({ sourceDir, automatic } = JSON.parse(contents));
}
catch (e) {
if (contents !== undefined) {
Zotero.debug(contents, 1);
}
Zotero.logError(e);
return 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 {
oldDir = sourceDir;
partial = true;
}
// Not yet used
let progressHandler = function (progress, progressMax) {
this.updateZoteroPaneProgressMeter(Math.round(progress / progressMax));
}.bind(this);
let errors;
let mode = automatic ? 'automatic' : 'manual';
// This can seemingly fail due to a race condition building the Standalone window,
// so just ignore it if it does
try {
Zotero.showZoteroPaneProgressMeter(Zotero.getString("dataDir.migration.inProgress"));
}
catch (e) {
Zotero.logError(e);
}
try {
errors = yield this.migrate(oldDir, newDir, partial, progressHandler);
}
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.${mode}.text1`, ZOTERO_CONFIG.CLIENT_NAME)
+ "\n\n"
+ e
+ "\n\n"
+ Zotero.getString(`dataDir.migration.failure.full.${mode}.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);
Zotero.skipLoading = true;
Zotero.Utilities.Internal.quitZotero();
}
return;
}
// Set data directory again
Zotero.debug("Using new data directory " + newDir);
this._cache(newDir);
// Tell Zotero for Firefox in connector mode to reload and find the new data directory
if (Zotero.isStandalone) {
Zotero.IPC.broadcast('reinit');
}
// 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)
+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING);
let index = ps.confirmEx(null,
Zotero.getString('dataDir.migration.failure.title'),
Zotero.getString(`dataDir.migration.failure.partial.${mode}.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('general.tryAgain'),
Zotero.getString('general.tryLater'),
Zotero.getString('dataDir.migration.failure.partial.showDirectoriesAndQuit', Zotero.appName),
null, {}
);
if (index == 0) {
return this.checkForMigration(newDir, newDir);
}
// Focus the first file/folder in the old directory
else if (index == 2) {
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 in this profile and any profiles pointing to the old location
*
* 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[]}
*/
migrate: 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, this.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.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.MIGRATION_MARKER));
}
// Update the data directory setting first. If moving the database fails, get() will continue
// to use the old directory based on the migration marker
this.set(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.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, this.MIGRATION_MARKER);
Zotero.debug("Removing " + newMigrationMarker);
yield OS.File.remove(newMigrationMarker);
Zotero.debug("Migration successful");
}
catch (e) {
addError(e);
}
}
// Update setting in other profiles that point to this data directory
try {
let otherProfiles = yield Zotero.Profile.findOtherProfilesUsingDataDirectory(oldDir);
for (let dir of otherProfiles) {
try {
yield Zotero.Profile.updateProfileDataDirectory(dir, oldDir, newDir);
}
catch (e) {
Zotero.logError("Error updating " + OS.Path.join(dir.path, "prefs.js"));
Zotero.logError(e);
}
}
}
catch (e) {
Zotero.logError("Error updating other profiles to point to new location");
}
return errors;
}),
getDatabaseFilename: function (name) {
return (name || ZOTERO_CONFIG.ID) + '.sqlite';
},
getDatabase: function (name, ext) {
name = this.getDatabaseFilename(name);
ext = ext ? '.' + ext : '';
return OS.Path.join(this.dir, name + ext);
}
};

View file

@ -444,7 +444,6 @@ Zotero.File = new function(){
var it = new OS.File.DirectoryIterator(path); var it = new OS.File.DirectoryIterator(path);
try { try {
let entry = yield it.next(); let entry = yield it.next();
Zotero.debug(entry);
return false; return false;
} }
catch (e) { catch (e) {

View file

@ -0,0 +1,236 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2016 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
Components.utils.import("resource://gre/modules/osfile.jsm");
Zotero.Profile = {
dir: OS.Constants.Path.profileDir,
getDefaultInProfilesDir: Zotero.Promise.coroutine(function* (profilesDir) {
var profilesIni = OS.Path.join(profilesDir, "profiles.ini");
try {
var iniContents = yield Zotero.File.getContentsAsync(profilesIni);
}
catch (e) {
if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
return false;
}
throw e;
}
// cheap and dirty ini parser
var curSection = null;
var defaultSection = null;
var nSections = 0;
for (let line of iniContents.split(/(?:\r?\n|\r)/)) {
let tline = line.trim();
if(tline[0] == "[" && tline[tline.length-1] == "]") {
curSection = {};
if(tline != "[General]") nSections++;
} else if(curSection && tline != "") {
let equalsIndex = tline.indexOf("=");
let key = tline.substr(0, equalsIndex);
let val = tline.substr(equalsIndex+1);
curSection[key] = val;
if(key == "Default" && val == "1") {
defaultSection = curSection;
}
}
}
if (!defaultSection && curSection) defaultSection = curSection;
if (!defaultSection || !defaultSection.Path) return false;
var defaultProfile = defaultSection.IsRelative === "1"
? OS.Path.join(profilesDir, ...defaultSection.Path.split("/"))
: defaultSection.Path;
if (!(yield OS.File.exists(defaultProfile))) {
return false;
}
return [defaultProfile, nSections > 1];
}),
getProfilesDir: function () {
return OS.Path.dirname(this.dir);
},
/**
* Get the path to the Profiles directory of the other app from this one (Firefox or Zotero),
* which may or may not exist
*
* @return {String} - Path
*/
getOtherAppProfilesDir: function () {
var dir = OS.Path.dirname(OS.Path.dirname(OS.Path.dirname(this.dir)));
if (Zotero.isStandalone) {
if (Zotero.isWin) {
dir = OS.Path.join(OS.Path.dirname(dir), "Mozilla", "Firefox");
}
else if (Zotero.isMac) {
dir = OS.Path.join(dir, "Firefox");
}
else {
dir = OS.Path.join(dir, ".mozilla", "firefox");
}
}
else {
if (Zotero.isWin) {
dir = OS.Path.join(OS.Path.dirname(dir), "Zotero", "Zotero");
}
else if (Zotero.isMac) {
dir = OS.Path.join(dir, "Zotero");
} else {
dir = OS.Path.join(dir, ".zotero", "zotero");
}
}
return OS.Path.join(dir, "Profiles");
},
/**
* Find other profile directories (for this app or the other app) using the given data directory
*
* @return {String[]}
*/
findOtherProfilesUsingDataDirectory: Zotero.Promise.coroutine(function* (dataDir) {
let otherAppProfiles = yield this._findOtherAppProfiles();
let otherProfiles = (yield this._findOtherProfiles()).concat(otherAppProfiles);
// First get profiles pointing at this directory
otherProfiles = yield Zotero.Promise.filter(otherProfiles, Zotero.Promise.coroutine(function* (dir) {
let prefs = yield Zotero.File.getContentsAsync(OS.Path.join(dir, "prefs.js"));
prefs = prefs.trim().split(/(?:\r\n|\r|\n)/);
return prefs.some(line => {
return line.includes("extensions.zotero.useDataDir") && line.includes("true");
}) && prefs.some(line => {
return line.match(/extensions\.zotero\.(lastD|d)ataDir/) && line.includes(dataDir)
});
}));
// If the parent of the source directory is a profile directory from the other app, add that
// to the list, which addresses the situation where the source directory is a custom
// location for the current profile but is a default in the other app (meaning it wouldn't
// be added above).
let dataDirParent = OS.Path.dirname(dataDir);
if (otherAppProfiles.includes(dataDirParent) && !otherProfiles.includes(dataDirParent)) {
otherProfiles.push(dataDirParent);
}
if (otherProfiles.length) {
Zotero.debug("Found other profiles pointing to " + dataDir);
Zotero.debug(otherProfiles);
}
else {
Zotero.debug("No other profiles point to " + dataDir);
}
return otherProfiles;
}),
updateProfileDataDirectory: Zotero.Promise.coroutine(function* (profileDir, oldDir, newDir) {
let prefsFile = OS.Path.join(profileDir, "prefs.js");
let prefsFileTmp = OS.Path.join(profileDir, "prefs.js.tmp");
Zotero.debug("Updating " + prefsFile + " to point to new data directory");
let contents = yield Zotero.File.getContentsAsync(prefsFile);
contents = contents
.trim()
.split(/(?:\r\n|\r|\n)/)
// Remove existing lines
.filter(line => !line.match(/extensions\.zotero\.(useD|lastD|d)ataDir/));
// Shouldn't happen, but let's make sure we don't corrupt the prefs file
let safeVal = newDir.replace(/["]/g, "");
contents.push(
`user_pref("extensions.zotero.dataDir", "${safeVal}");`,
`user_pref("extensions.zotero.lastDataDir", "${safeVal}");`,
'user_pref("extensions.zotero.useDataDir", true);'
);
let lineSep = Zotero.isWin ? "\r\n" : "\n";
contents = contents.join(lineSep) + lineSep;
yield OS.File.writeAtomic(
prefsFile,
contents,
{
tmpPath: prefsFileTmp,
encoding: 'utf-8'
}
);
}),
//
// Private methods
//
/**
* Get all profile directories within the given directory
*
* @return {String[]} - Array of paths
*/
_getProfilesInDir: Zotero.Promise.coroutine(function* (profilesDir) {
var dirs = [];
yield Zotero.File.iterateDirectory(profilesDir, function* (iterator) {
while (true) {
let entry = yield iterator.next();
if (entry.isDir && (yield OS.File.exists(OS.Path.join(entry.path, "prefs.js")))) {
dirs.push(entry.path);
}
}
});
return dirs;
}),
/**
* Find other profile directories for this app (Firefox or Zotero)
*
* @return {String[]} - Array of paths
*/
_findOtherProfiles: Zotero.Promise.coroutine(function* () {
var profileDir = this.dir;
var profilesDir = this.getProfilesDir();
return this._getProfilesInDir(profilesDir).filter(dir => dir != profileDir);
}),
/**
* Find profile directories for the other app (Firefox or Zotero)
*
* @return {String[]} - Array of paths
*/
_findOtherAppProfiles: Zotero.Promise.coroutine(function* () {
var dir = this.getOtherAppProfilesDir();
return (yield OS.File.exists(dir)) ? this._getProfilesInDir(dir) : [];
})
};

View file

@ -138,7 +138,7 @@ Zotero.Sync.Data.Local = {
accept = true; accept = true;
} }
// else if (io.extra1) { // else if (io.extra1) {
// if (Zotero.forceNewDataDirectory(win)) { // if (Zotero.DataDirectory.forceChange(win)) {
// var ps = Services.prompt; // var ps = Services.prompt;
// ps.alert(null, // ps.alert(null,
// Zotero.getString('general.restartRequired'), // Zotero.getString('general.restartRequired'),

View file

@ -34,9 +34,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
*/ */
(function(){ (function(){
// Privileged (public) methods // Privileged (public) methods
this.getProfileDirectory = getProfileDirectory;
this.getStorageDirectory = getStorageDirectory; this.getStorageDirectory = getStorageDirectory;
this.chooseZoteroDirectory = chooseZoteroDirectory;
this.debug = debug; this.debug = debug;
this.log = log; this.log = log;
this.logError = logError; this.logError = logError;
@ -103,7 +101,6 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
this.hiDPISuffix = ""; this.hiDPISuffix = "";
var _startupErrorHandler; var _startupErrorHandler;
var _dataDirectory = false;
var _localizedStringBundle; var _localizedStringBundle;
var _locked = false; var _locked = false;
@ -275,7 +272,8 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
_addToolbarIcon(); _addToolbarIcon();
try { try {
var dataDir = Zotero.getZoteroDirectory(); yield Zotero.DataDirectory.init();
var dataDir = Zotero.DataDirectory.dir;
} }
catch (e) { catch (e) {
// Zotero dir not found // Zotero dir not found
@ -301,11 +299,11 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
// Revert to home directory // Revert to home directory
if (index == 1) { if (index == 1) {
Zotero.chooseZoteroDirectory(false, true); Zotero.DataDirectory.choose(false, true);
} }
// Locate data directory // Locate data directory
else if (index == 2) { else if (index == 2) {
Zotero.chooseZoteroDirectory(); Zotero.DataDirectory.choose();
} }
} }
_addToolbarIcon(); _addToolbarIcon();
@ -318,14 +316,16 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
} }
if (!Zotero.isConnector) { if (!Zotero.isConnector) {
yield Zotero.checkForDataDirectoryMigration(dataDir.path, this.getDefaultDataDir()); yield Zotero.DataDirectory.checkForMigration(
dataDir, Zotero.DataDirectory.defaultDir
);
if (this.skipLoading) { if (this.skipLoading) {
return; return;
} }
// Make sure data directory isn't in Dropbox, etc. // Make sure data directory isn't in Dropbox, etc.
if (Zotero.isStandalone) { if (Zotero.isStandalone) {
Zotero.checkForUnsafeDataDirectory(dataDir.path); yield Zotero.DataDirectory.checkForUnsafeLocation(dataDir);
} }
} }
@ -456,9 +456,9 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
Zotero.VersionHeader.init(); Zotero.VersionHeader.init();
// Check for data reset/restore // Check for data reset/restore
var dataDir = Zotero.getZoteroDirectory(); var dataDir = Zotero.DataDirectory.dir;
var restoreFile = OS.Path.join(dataDir.path, 'restore-from-server'); var restoreFile = OS.Path.join(dataDir, 'restore-from-server');
var resetDataDirFile = OS.Path.join(dataDir.path, 'reset-data-directory'); var resetDataDirFile = OS.Path.join(dataDir, 'reset-data-directory');
var result = yield Zotero.Promise.all([OS.File.exists(restoreFile), OS.File.exists(resetDataDirFile)]); var result = yield Zotero.Promise.all([OS.File.exists(restoreFile), OS.File.exists(resetDataDirFile)]);
if (result.some(r => r)) { if (result.some(r => r)) {
@ -478,7 +478,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
Zotero.restoreFromServer = true; Zotero.restoreFromServer = true;
} else if (Zotero.resetDataDir) { } else if (Zotero.resetDataDir) {
Zotero.initAutoSync = true; Zotero.initAutoSync = true;
var storageDir = OS.Path.join(dataDir.path, 'storage'); var storageDir = OS.Path.join(dataDir, 'storage');
yield Zotero.Promise.all([ yield Zotero.Promise.all([
OS.File.removeDir(storageDir, {ignoreAbsent: true}), OS.File.removeDir(storageDir, {ignoreAbsent: true}),
OS.File.remove(resetDataDirFile) OS.File.remove(resetDataDirFile)
@ -509,9 +509,6 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
if (Zotero.isStandalone) { if (Zotero.isStandalone) {
let dbSystemVersion = yield Zotero.Schema.getDBVersion('system'); let dbSystemVersion = yield Zotero.Schema.getDBVersion('system');
if (dbSystemVersion > 0 && dbSystemVersion < 31) { if (dbSystemVersion > 0 && dbSystemVersion < 31) {
var dir = Zotero.getProfileDirectory();
dir.append('zotero');
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.createInstance(Components.interfaces.nsIPromptService); .createInstance(Components.interfaces.nsIPromptService);
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
@ -534,8 +531,6 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
// Default location // Default location
if (index == 0) { if (index == 0) {
Zotero.File.createDirectoryIfMissing(dir);
Zotero.Prefs.set("useDataDir", false) Zotero.Prefs.set("useDataDir", false)
Services.startup.quit( Services.startup.quit(
@ -545,7 +540,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
} }
// Select new data directory // Select new data directory
else if (index == 1) { else if (index == 1) {
var dir = Zotero.chooseZoteroDirectory(true); let dir = yield Zotero.DataDirectory.choose(true);
if (!dir) { if (!dir) {
quit = true; quit = true;
} }
@ -889,284 +884,18 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
}); });
function getProfileDirectory(){ this.getProfileDirectory = function () {
return Services.dirsvc.get("ProfD", Components.interfaces.nsIFile); Zotero.warn("Zotero.getProfileDirectory() is deprecated -- use Zotero.Profile.dir");
} return Zotero.File.pathToFile(Zotero.Profile.dir);
function getDefaultProfile(prefDir) {
// find profiles.ini file
var profilesIni = prefDir.clone();
profilesIni.append("profiles.ini");
if(!profilesIni.exists()) return false;
var iniContents = Zotero.File.getContents(profilesIni);
// cheap and dirty ini parser
var curSection = null;
var defaultSection = null;
var nSections = 0;
for (let line of iniContents.split(/(?:\r?\n|\r)/)) {
let tline = line.trim();
if(tline[0] == "[" && tline[tline.length-1] == "]") {
curSection = {};
if(tline != "[General]") nSections++;
} else if(curSection && tline != "") {
let equalsIndex = tline.indexOf("=");
let key = tline.substr(0, equalsIndex);
let val = tline.substr(equalsIndex+1);
curSection[key] = val;
if(key == "Default" && val == "1") {
defaultSection = curSection;
}
}
}
if(!defaultSection && curSection) defaultSection = curSection;
// parse out ini to reveal profile
if(!defaultSection || !defaultSection.Path) return false;
if(defaultSection.IsRelative === "1") {
var defaultProfile = prefDir.clone().QueryInterface(Components.interfaces.nsILocalFile);
try {
for each(var dir in defaultSection.Path.split("/")) defaultProfile.append(dir);
} catch(e) {
Zotero.logError("Could not find profile at "+defaultSection.Path);
throw e;
}
} else {
var defaultProfile = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
defaultProfile.initWithPath(defaultSection.Path);
}
if(!defaultProfile.exists()) return false;
return [defaultProfile, nSections > 1];
} }
this.getZoteroDirectory = function () { this.getZoteroDirectory = function () {
if (_dataDirectory != false) { Zotero.warn("Zotero.getZoteroDirectory() is deprecated -- use Zotero.DataDirectory.dir");
return Zotero.File.pathToFile(_dataDirectory); return Zotero.File.pathToFile(Zotero.DataDirectory.dir);
}
var file;
if (Zotero.Prefs.get('useDataDir')) {
file = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
let prefVal = Zotero.Prefs.get('dataDir');
// Convert old persistent descriptor pref to string path and clear obsolete lastDataDir pref
//
// persistentDescriptor now appears to return (and parse) a string path anyway on macOS,
// which is the only place where it didn't use a string path to begin with, but be explicit
// just in case there's some difference.
//
// A post-Mozilla prefs migration should do this same check, and then this conditional can
// be removed.
if (Zotero.Prefs.get('lastDataDir')) {
try {
file.persistentDescriptor = prefVal;
}
catch (e) {
Zotero.debug("Persistent descriptor in extensions.zotero.dataDir did not resolve", 1);
e = { name: "NS_ERROR_FILE_NOT_FOUND" };
throw (e);
}
// This removes lastDataDir
this.setDataDirectory(file.path);
}
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 contents = Zotero.File.getContents(migrationMarker);
try {
let { sourceDir } = JSON.parse(contents);
file = Zotero.File.pathToFile(sourceDir);
}
catch (e) {
Zotero.logError(e);
Zotero.debug(`Invalid marker file:\n\n${contents}`, 1);
e = { name: "NS_ERROR_FILE_NOT_FOUND" };
throw e;
}
}
else {
try {
file = Zotero.File.pathToFile(prefVal);
}
catch (e) {
Zotero.logError(e);
Zotero.debug(`Invalid path '${prefVal}' in dataDir pref`, 1);
e = { name: "NS_ERROR_FILE_NOT_FOUND" };
throw e;
}
}
}
if (!file.exists() && file.path != this.getDefaultDataDir()) {
// If set to a legacy directory that doesn't exist, forget about it and just use the
// new default location, which will either exist or be created below. The most likely
// cause of this is a migration, so don't bother looking in other-app profiles.
if (this.isLegacyDataDirectory(file.path)) {
let newDefault = this.getDefaultDataDir();
Zotero.debug(`Legacy data directory ${file.path} from pref not found `
+ `-- reverting to ${newDefault}`, 1);
file = Zotero.File.pathToFile(newDefault);
this.setDataDirectory(newDefault);
}
// For other custom directories that don't exist, show not-found dialog
else {
Zotero.debug("Custom data directory ${file.path} not found", 1);
throw { name: "NS_ERROR_FILE_NOT_FOUND" };
}
}
}
else {
let dataDir = this.getDefaultDataDir();
//
// TODO: asyncify
//
// Check for ~/Zotero/zotero.sqlite
dataDir = Zotero.File.pathToFile(dataDir);
let dbFile = dataDir.clone();
dbFile.append(this.getDatabaseFilename());
if (dbFile.exists()) {
Zotero.debug("Using data directory " + dataDir.path);
this._cacheDataDirectory(dataDir.path);
// Set as a custom data directory so that 4.0 uses it
this.setDataDirectory(dataDir.path);
return dataDir.clone();
}
// Check for 'zotero' dir in profile dir
let profileSubdir = Zotero.getProfileDirectory();
profileSubdir.append('zotero');
if (profileSubdir.exists()) {
Zotero.debug("Using data directory " + profileSubdir.path);
this._cacheDataDirectory(profileSubdir.path);
return profileSubdir.clone();
}
//
// If Standalone and no directory yet, check Firefox directory, or vice versa
//
let prefDir = OS.Path.dirname(
OS.Path.dirname(
OS.Path.dirname(
OS.Constants.Path.profileDir
)
)
);
if(Zotero.isStandalone) {
if (Zotero.isWin) {
prefDir = OS.Path.join(OS.Path.dirname(prefDir), "Mozilla", "Firefox");
}
else if (Zotero.isMac) {
prefDir = OS.Path.join(prefDir, "Firefox");
}
else {
prefDir = OS.Path.join(prefDir, ".mozilla", "firefox");
}
}
else {
if (Zotero.isWin) {
prefDir = OS.Path.join(OS.Path.dirname(prefDir), "Zotero", "Zotero");
}
else if (Zotero.isMac) {
prefDir = OS.Path.join(prefDir, "Zotero");
} else {
prefDir = OS.Path.join(prefDir, ".zotero", "zotero");
}
}
Zotero.debug("Looking for existing profile in " + prefDir);
// get default profile
var defProfile;
try {
defProfile = getDefaultProfile(Zotero.File.pathToFile(prefDir));
} catch(e) {
Zotero.debug("An error occurred locating the Firefox profile; not "+
"attempting to migrate from Zotero for Firefox");
Zotero.logError(e);
}
if(defProfile) {
// get Zotero directory
let profileDir = defProfile[0].path;
Zotero.debug("Found default profile at " + profileDir);
// copy prefs
let prefsFile = OS.Path.join(profileDir, "prefs.js");
if (Zotero.File.pathToFile(prefsFile).exists()) {
// build sandbox
var sandbox = new Components.utils.Sandbox("http://www.example.com/");
Components.utils.evalInSandbox(
"var prefs = {};"+
"function user_pref(key, val) {"+
"prefs[key] = val;"+
"}"
, sandbox);
// remove comments
var prefsJs = Zotero.File.getContents(prefsFile);
prefsJs = prefsJs.replace(/^#[^\r\n]*$/mg, "");
// evaluate
Components.utils.evalInSandbox(prefsJs, sandbox);
var prefs = sandbox.prefs;
for(var key in prefs) {
if(key.substr(0, ZOTERO_CONFIG.PREF_BRANCH.length) === ZOTERO_CONFIG.PREF_BRANCH
&& key !== "extensions.zotero.firstRun2") {
Zotero.Prefs.set(key.substr(ZOTERO_CONFIG.PREF_BRANCH.length), prefs[key]);
}
}
// If data directory setting was transferred, use that
if (Zotero.Prefs.get('useDataDir')) {
return this.getZoteroDirectory();
}
}
// If there's a data directory in the default profile for the alternative app, use that
let zoteroDir = OS.Path.join(profileDir, ZOTERO_CONFIG.ID);
if (Zotero.File.pathToFile(zoteroDir).exists()) {
this.setDataDirectory(zoteroDir);
file = Zotero.File.pathToFile(zoteroDir);
}
}
if (!file) {
file = dataDir;
}
}
Zotero.debug("Using data directory " + file.path);
Zotero.File.createDirectoryIfMissing(file);
this._cacheDataDirectory(file.path);
return file.clone();
} }
this.getDefaultDataDir = function () {
// Use special data directory for tests
if (Zotero.test) {
return OS.Path.join(OS.Path.dirname(OS.Constants.Path.profileDir), "Zotero");
}
return OS.Path.join(OS.Constants.Path.homeDir, ZOTERO_CONFIG.CLIENT_NAME);
};
function getStorageDirectory(){ function getStorageDirectory(){
var file = Zotero.getZoteroDirectory(); var file = Zotero.getZoteroDirectory();
@ -1176,17 +905,9 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
} }
this.getDatabaseFilename = function (name) {
return (name || ZOTERO_CONFIG.ID) + '.sqlite';
};
this.getZoteroDatabase = function (name, ext) { this.getZoteroDatabase = function (name, ext) {
name = this.getDatabaseFilename(name); Zotero.warn("Zotero.getZoteroDatabase() is deprecated -- use Zotero.DataDirectory.getDatabase()");
ext = ext ? '.' + ext : ''; return Zotero.File.pathToFile(Zotero.DataDirectory.getDatabase(name, ext));
var file = Zotero.getZoteroDirectory();
file.append(name + ext);
return file;
} }
@ -1229,582 +950,6 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
} }
function chooseZoteroDirectory(forceQuitNow, useHomeDir, moreInfoCallback) {
var win = Services.wm.getMostRecentWindow('navigator:browser');
var ps = Services.prompt;
if (useHomeDir) {
let changed = this.setDataDirectory(this.getDefaultDataDir());
if (!changed) {
return false;
}
}
else {
var nsIFilePicker = Components.interfaces.nsIFilePicker;
while (true) {
var fp = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(nsIFilePicker);
fp.init(win, Zotero.getString('dataDir.selectDir'), nsIFilePicker.modeGetFolder);
fp.displayDirectory = Zotero.getZoteroDirectory();
fp.appendFilters(nsIFilePicker.filterAll);
if (fp.show() == nsIFilePicker.returnOK) {
var file = fp.file;
let dialogText = '';
let dialogTitle = '';
if (file.path == (Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir'))) {
Zotero.debug("Data directory hasn't changed");
return false;
}
// In dropbox folder
if (Zotero.File.isDropboxDirectory(file.path)) {
dialogTitle = Zotero.getString('general.warning');
dialogText = Zotero.getString('dataDir.unsafeLocation.selected.dropbox') + "\n\n"
+ Zotero.getString('dataDir.unsafeLocation.selected.useAnyway');
}
else if (file.directoryEntries.hasMoreElements()) {
let dbfile = file.clone();
dbfile.append(this.getDatabaseFilename());
// Warn if non-empty and no zotero.sqlite
if (!dbfile.exists()) {
dialogTitle = Zotero.getString('dataDir.selectedDirNonEmpty.title');
dialogText = Zotero.getString('dataDir.selectedDirNonEmpty.text');
}
}
// Directory empty
else {
dialogTitle = Zotero.getString('dataDir.selectedDirEmpty.title');
dialogText = Zotero.getString('dataDir.selectedDirEmpty.text', Zotero.appName) + '\n\n'
+ Zotero.getString('dataDir.selectedDirEmpty.useNewDir');
}
// Warning dialog to be displayed
if(dialogText !== '') {
let buttonFlags = ps.STD_YES_NO_BUTTONS;
if (moreInfoCallback) {
buttonFlags += ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING;
}
let index = ps.confirmEx(null,
dialogTitle,
dialogText,
buttonFlags,
null,
null,
moreInfoCallback ? Zotero.getString('general.moreInformation') : null,
null, {});
// Not OK -- return to file picker
if (index == 1) {
continue;
}
else if (index == 2) {
setTimeout(function () {
moreInfoCallback();
}, 1);
return false;
}
}
this.setDataDirectory(file.path);
break;
}
else {
return false;
}
}
}
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING);
if (!forceQuitNow) {
buttonFlags += (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
}
var app = Zotero.appName;
var index = ps.confirmEx(null,
Zotero.getString('general.restartRequired'),
Zotero.getString('general.restartRequiredForChange', app)
+ "\n\n" + Zotero.getString('dataDir.moveFilesToNewLocation', app),
buttonFlags,
Zotero.getString('general.quitApp', app),
forceQuitNow ? null : Zotero.getString('general.restartLater'),
null, null, {});
if (forceQuitNow || index == 0) {
Services.startup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit);
}
return useHomeDir ? true : file;
}
this._cacheDataDirectory = function (dir) {
_dataDirectory = dir;
};
this.forceNewDataDirectory = function(win) {
if (!win) {
win = Services.wm.getMostRecentWindow('navigator:browser');
}
var ps = Services.prompt;
var nsIFilePicker = Components.interfaces.nsIFilePicker;
while (true) {
var fp = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(nsIFilePicker);
fp.init(win, Zotero.getString('dataDir.selectNewDir', Zotero.clientName), nsIFilePicker.modeGetFolder);
fp.displayDirectory = Zotero.getZoteroDirectory();
fp.appendFilters(nsIFilePicker.filterAll);
if (fp.show() == nsIFilePicker.returnOK) {
var file = fp.file;
if (file.directoryEntries.hasMoreElements()) {
ps.alert(null,
Zotero.getString('dataDir.mustSelectEmpty.title'),
Zotero.getString('dataDir.mustSelectEmpty.text')
);
continue;
}
this.setDataDirectory(file.path);
return file;
} else {
return false;
}
}
};
this.warnOnUnsafeDataDir = true;
this.checkForUnsafeDataDirectory = function (path) {
if (this.warnOnUnsafeDataDir && Zotero.File.isDropboxDirectory(path)
&& Zotero.Prefs.get('warnOnUnsafeDataDir')) {
this.warnOnUnsafeDataDir = false;
let check = {value: false};
let index = Services.prompt.confirmEx(
null,
Zotero.getString('general.warning'),
Zotero.getString('dataDir.unsafeLocation.existing.dropbox') + "\n\n"
+ Zotero.getString('dataDir.unsafeLocation.existing.chooseDifferent'),
Services.prompt.STD_YES_NO_BUTTONS,
null, null, null,
Zotero.getString('general.dontShowWarningAgain'),
check
);
// Yes - display dialog.
if (index == 0) {
Zotero.chooseZoteroDirectory(true);
}
if (check.value) {
Zotero.Prefs.set('warnOnUnsafeDataDir', false);
}
}
}
/**
* @return {Boolean} - True if the directory changed; false otherwise
*/
this.setDataDirectory = function (path) {
var origPath = Zotero.Prefs.get('dataDir');
Zotero.Prefs.set('dataDir', path);
// Clear legacy pref
Zotero.Prefs.clear('lastDataDir');
Zotero.Prefs.set('useDataDir', true);
return path != origPath;
};
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';
this.markDataDirectoryForMigration = function (dir, automatic = false) {
return Zotero.File.putContentsAsync(
OS.Path.join(dir, this.DATA_DIR_MIGRATION_MARKER),
JSON.stringify({
sourceDir: dir,
automatic
})
);
};
/**
* 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) {
if (!this.canMigrateDataDirectory(dataDir)) {
return false;
}
let migrationMarker = OS.Path.join(dataDir, this.DATA_DIR_MIGRATION_MARKER);
try {
var exists = yield OS.File.exists(migrationMarker)
}
catch (e) {
Zotero.logError(e);
}
let automatic = false;
if (!exists) {
// Migrate automatically on macOS and Linux -- this should match the check in
// Zotero.File.moveDirectory()
if (!Zotero.isWin && (yield OS.File.exists("/bin/mv"))) {
automatic = true;
}
else {
return false;
}
}
// Check for an existing pipe from other running versions of Zotero pointing at the same data
// directory, and skip migration if found
try {
let foundPipe = yield Zotero.IPC.pipeExists();
if (foundPipe) {
Zotero.debug("Found existing pipe -- skipping migration");
return false;
}
}
catch (e) {
Zotero.logError("Error checking for pipe -- skipping migration:\n\n" + e);
return false;
}
if (automatic) {
yield this.markDataDirectoryForMigration(dataDir, true);
}
let sourceDir;
let oldDir;
let partial = false;
// Check whether this is an automatic or manual migration
let contents;
try {
contents = yield Zotero.File.getContentsAsync(migrationMarker);
({ sourceDir, automatic } = JSON.parse(contents));
}
catch (e) {
if (contents !== undefined) {
Zotero.debug(contents, 1);
}
Zotero.logError(e);
return 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 {
oldDir = sourceDir;
partial = true;
}
// Not yet used
let progressHandler = function (progress, progressMax) {
this.updateZoteroPaneProgressMeter(Math.round(progress / progressMax));
}.bind(this);
let errors;
let mode = automatic ? 'automatic' : 'manual';
// This can seemingly fail due to a race condition building the Standalone window,
// so just ignore it if it does
try {
this.showZoteroPaneProgressMeter(Zotero.getString("dataDir.migration.inProgress"));
}
catch (e) {
Zotero.logError(e);
}
try {
errors = yield Zotero.migrateDataDirectory(oldDir, newDir, partial, progressHandler);
}
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.${mode}.text1`, ZOTERO_CONFIG.CLIENT_NAME)
+ "\n\n"
+ e
+ "\n\n"
+ Zotero.getString(`dataDir.migration.failure.full.${mode}.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);
// Tell Zotero for Firefox in connector mode to reload and find the new data directory
if (this.isStandalone) {
Zotero.IPC.broadcast('reinit');
}
// 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)
+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING);
let index = ps.confirmEx(null,
Zotero.getString('dataDir.migration.failure.title'),
Zotero.getString(`dataDir.migration.failure.partial.${mode}.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('general.tryAgain'),
Zotero.getString('general.tryLater'),
Zotero.getString('dataDir.migration.failure.partial.showDirectoriesAndQuit', Zotero.appName),
null, {}
);
if (index == 0) {
return this.checkForDataDirectoryMigration(newDir, newDir);
}
// Focus the first file/folder in the old directory
else if (index == 2) {
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);
Zotero.debug("Migration successful");
}
catch (e) {
addError(e);
}
}
return errors;
});
/** /**
* Launch a file, the best way we can * Launch a file, the best way we can
*/ */

View file

@ -33,6 +33,7 @@ const Ci = Components.interfaces;
/** XPCOM files to be loaded for all modes **/ /** XPCOM files to be loaded for all modes **/
const xpcomFilesAll = [ const xpcomFilesAll = [
'zotero', 'zotero',
'dataDirectory',
'date', 'date',
'debug', 'debug',
'error', 'error',
@ -41,6 +42,7 @@ const xpcomFilesAll = [
'mimeTypeHandler', 'mimeTypeHandler',
'openurl', 'openurl',
'ipc', 'ipc',
'profile',
'progressWindow', 'progressWindow',
'translation/translate', 'translation/translate',
'translation/translate_firefox', 'translation/translate_firefox',

View file

@ -474,7 +474,6 @@ function getTestDataUrl(path) {
/** /**
* Returns an absolute path to an empty temporary directory * Returns an absolute path to an empty temporary directory
* (i.e., test/tests/data)
*/ */
var getTempDirectory = Zotero.Promise.coroutine(function* getTempDirectory() { var getTempDirectory = Zotero.Promise.coroutine(function* getTempDirectory() {
Components.utils.import("resource://gre/modules/osfile.jsm"); Components.utils.import("resource://gre/modules/osfile.jsm");

View file

@ -1,6 +1,6 @@
"use strict"; "use strict";
describe("Zotero Core Functions", function () { describe("Zotero.DataDirectory", function () {
var tmpDir, oldDir, newDir, dbFilename, oldDBFile, newDBFile, oldStorageDir, newStorageDir, var tmpDir, oldDir, newDir, dbFilename, oldDBFile, newDBFile, oldStorageDir, newStorageDir,
oldTranslatorsDir, newTranslatorsDir, translatorName1, translatorName2, oldTranslatorsDir, newTranslatorsDir, translatorName1, translatorName2,
oldStorageDir1, newStorageDir1, storageFile1, oldStorageDir2, newStorageDir2, storageFile2, oldStorageDir1, newStorageDir1, storageFile1, oldStorageDir2, newStorageDir2, storageFile2,
@ -12,7 +12,7 @@ describe("Zotero Core Functions", function () {
tmpDir = yield getTempDirectory(); tmpDir = yield getTempDirectory();
oldDir = OS.Path.join(tmpDir, "old"); oldDir = OS.Path.join(tmpDir, "old");
newDir = OS.Path.join(tmpDir, "new"); newDir = OS.Path.join(tmpDir, "new");
dbFilename = Zotero.getDatabaseFilename(); dbFilename = Zotero.DataDirectory.getDatabaseFilename();
oldDBFile = OS.Path.join(oldDir, dbFilename); oldDBFile = OS.Path.join(oldDir, dbFilename);
newDBFile = OS.Path.join(newDir, dbFilename); newDBFile = OS.Path.join(newDir, dbFilename);
oldStorageDir = OS.Path.join(oldDir, "storage"); oldStorageDir = OS.Path.join(oldDir, "storage");
@ -33,24 +33,23 @@ describe("Zotero Core Functions", function () {
str4 = '4'; str4 = '4';
str5 = '5'; str5 = '5';
str6 = '6'; str6 = '6';
oldMigrationMarker = OS.Path.join(oldDir, Zotero.DATA_DIR_MIGRATION_MARKER); oldMigrationMarker = OS.Path.join(oldDir, Zotero.DataDirectory.MIGRATION_MARKER);
newMigrationMarker = OS.Path.join(newDir, Zotero.DATA_DIR_MIGRATION_MARKER); newMigrationMarker = OS.Path.join(newDir, Zotero.DataDirectory.MIGRATION_MARKER);
stubs.canMigrate = sinon.stub(Zotero, "canMigrateDataDirectory").returns(true); stubs.canMigrate = sinon.stub(Zotero.DataDirectory, "canMigrate").returns(true);
// A pipe always exists during tests, since Zotero is running // A pipe always exists during tests, since Zotero is running
stubs.pipeExists = sinon.stub(Zotero.IPC, "pipeExists").returns(Zotero.Promise.resolve(false)); stubs.pipeExists = sinon.stub(Zotero.IPC, "pipeExists").returns(Zotero.Promise.resolve(false));
}); });
beforeEach(function* () { beforeEach(function* () {
// Trigger a call to setDataDirectory() now to avoid affecting the stub call count stubs.setDataDir = sinon.stub(Zotero.DataDirectory, "set");
Zotero.getZoteroDirectory();
stubs.setDataDir = sinon.stub(Zotero, "setDataDirectory");
}); });
afterEach(function* () { afterEach(function* () {
yield removeDir(oldDir); yield removeDir(oldDir);
yield removeDir(newDir); yield removeDir(newDir);
Zotero._cacheDataDirectory(false); Zotero.DataDirectory._cache(false);
yield Zotero.DataDirectory.init();
stubs.setDataDir.restore(); stubs.setDataDir.restore();
}); });
@ -83,7 +82,7 @@ describe("Zotero Core Functions", function () {
let storageDir1 = OS.Path.join(storageDir, 'AAAAAAAA'); let storageDir1 = OS.Path.join(storageDir, 'AAAAAAAA');
let storageDir2 = OS.Path.join(storageDir, 'BBBBBBBB'); let storageDir2 = OS.Path.join(storageDir, 'BBBBBBBB');
let translatorsDir = OS.Path.join(dir, 'translators'); let translatorsDir = OS.Path.join(dir, 'translators');
let migrationMarker = OS.Path.join(dir, Zotero.DATA_DIR_MIGRATION_MARKER); let migrationMarker = OS.Path.join(dir, Zotero.DataDirectory.MIGRATION_MARKER);
// Database // Database
yield Zotero.File.putContentsAsync(OS.Path.join(dir, dbFilename), str1); yield Zotero.File.putContentsAsync(OS.Path.join(dir, dbFilename), str1);
@ -141,7 +140,7 @@ describe("Zotero Core Functions", function () {
}); });
describe("#checkForDataDirectoryMigration()", function () { describe("#checkForMigration()", function () {
let fileMoveStub; let fileMoveStub;
before(function () { before(function () {
@ -198,7 +197,7 @@ describe("Zotero Core Functions", function () {
) )
); );
}); });
yield Zotero.checkForDataDirectoryMigration(oldDir, newDir); yield Zotero.DataDirectory.checkForMigration(oldDir, newDir);
yield promise; yield promise;
yield promise2; yield promise2;
@ -240,7 +239,7 @@ describe("Zotero Core Functions", function () {
) )
); );
}); });
yield Zotero.checkForDataDirectoryMigration(oldDir, newDir); yield Zotero.DataDirectory.checkForMigration(oldDir, newDir);
yield promise; yield promise;
assert.isTrue(stub2.calledOnce); assert.isTrue(stub2.calledOnce);
@ -267,7 +266,7 @@ describe("Zotero Core Functions", function () {
it("should remove marker if old directory doesn't exist", function* () { it("should remove marker if old directory doesn't exist", function* () {
yield populateDataDirectory(newDir, oldDir); yield populateDataDirectory(newDir, oldDir);
yield Zotero.checkForDataDirectoryMigration(newDir, newDir); yield Zotero.DataDirectory.checkForMigration(newDir, newDir);
yield checkMigration({ yield checkMigration({
skipSetDataDirectory: true skipSetDataDirectory: true
}); });
@ -275,7 +274,7 @@ describe("Zotero Core Functions", function () {
}); });
describe("#migrateDataDirectory()", function () { describe("#migrate()", function () {
// Define tests and store for running in non-mv mode // Define tests and store for running in non-mv mode
var tests = []; var tests = [];
function add(desc, fn) { function add(desc, fn) {
@ -285,7 +284,7 @@ describe("Zotero Core Functions", function () {
add("should move all files and folders", function* () { add("should move all files and folders", function* () {
yield populateDataDirectory(oldDir); yield populateDataDirectory(oldDir);
yield Zotero.migrateDataDirectory(oldDir, newDir); yield Zotero.DataDirectory.migrate(oldDir, newDir);
yield checkMigration(); yield checkMigration();
}); });
@ -295,7 +294,7 @@ describe("Zotero Core Functions", function () {
yield OS.File.copy(oldMigrationMarker, newMigrationMarker); yield OS.File.copy(oldMigrationMarker, newMigrationMarker);
yield Zotero.migrateDataDirectory(oldDir, newDir, true); yield Zotero.DataDirectory.migrate(oldDir, newDir, true);
yield checkMigration(); yield checkMigration();
}); });
@ -306,7 +305,7 @@ describe("Zotero Core Functions", function () {
yield OS.File.copy(oldMigrationMarker, newMigrationMarker); yield OS.File.copy(oldMigrationMarker, newMigrationMarker);
yield OS.File.move(OS.Path.join(oldDir, dbFilename), OS.Path.join(newDir, dbFilename)); yield OS.File.move(OS.Path.join(oldDir, dbFilename), OS.Path.join(newDir, dbFilename));
yield Zotero.migrateDataDirectory(oldDir, newDir, true); yield Zotero.DataDirectory.migrate(oldDir, newDir, true);
yield checkMigration(); yield checkMigration();
}); });
@ -322,7 +321,7 @@ describe("Zotero Core Functions", function () {
yield removeDir(newTranslatorsDir); yield removeDir(newTranslatorsDir);
yield removeDir(newStorageDir2); yield removeDir(newStorageDir2);
yield Zotero.migrateDataDirectory(oldDir, newDir, true); yield Zotero.DataDirectory.migrate(oldDir, newDir, true);
yield checkMigration(); yield checkMigration();
}); });
@ -331,7 +330,7 @@ describe("Zotero Core Functions", function () {
yield OS.File.makeDir(newDir, { unixMode: 0o755 }); yield OS.File.makeDir(newDir, { unixMode: 0o755 });
yield Zotero.File.putContentsAsync(OS.Path.join(newDir, 'existing'), ''); yield Zotero.File.putContentsAsync(OS.Path.join(newDir, 'existing'), '');
yield Zotero.migrateDataDirectory(oldDir, newDir); yield Zotero.DataDirectory.migrate(oldDir, newDir);
yield checkMigration(); yield checkMigration();
assert.isTrue(yield OS.File.exists(OS.Path.join(newDir + "-1", 'existing'))); assert.isTrue(yield OS.File.exists(OS.Path.join(newDir + "-1", 'existing')));
@ -374,7 +373,7 @@ describe("Zotero Core Functions", function () {
} }
}); });
yield Zotero.migrateDataDirectory(oldDir, newDir); yield Zotero.DataDirectory.migrate(oldDir, newDir);
stub1.restore(); stub1.restore();

190
test/tests/profileTest.js Normal file
View file

@ -0,0 +1,190 @@
"use strict";
describe("Zotero.Profile", function () {
var tmpDir;
var profile1 = "ht79g2qb.Test1";
var profile2 = "b9auumgf.Test2";
var profile3 = "7sgqhns3.Test3";
beforeEach(function* () {
tmpDir = yield getTempDirectory();
var contents = `[General]
StartWithLastProfile=0
[Profile0]
Name=Test 1
IsRelative=1
Path=Profiles/${profile1}
Default=1
[Profile1]
Name=Test 2
IsRelative=1
Path=Profiles/${profile2}
[Profile1]
Name=Test 3
IsRelative=1
Path=Profiles/${profile3}
`;
yield Zotero.File.putContentsAsync(OS.Path.join(tmpDir, "profiles.ini"), contents);
yield OS.File.makeDir(
OS.Path.join(tmpDir, "Profiles", profile1),
{
unixMode: 0o755,
from: tmpDir
}
);
yield OS.File.makeDir(OS.Path.join(tmpDir, "Profiles", profile2), { unixMode: 0o755 });
yield OS.File.makeDir(OS.Path.join(tmpDir, "Profiles", profile3), { unixMode: 0o755 });
});
describe("#getDefaultInProfilesDir()", function () {
it("should parse a profiles.ini file", function* () {
var [dir, multiple] = yield Zotero.Profile.getDefaultInProfilesDir(tmpDir);
assert.equal(dir, OS.Path.join(tmpDir, "Profiles", profile1));
});
});
describe("#findOtherProfilesUsingDataDirectory()", function () {
it("should find profile with directory as a custom location", function* () {
let dataDir = Zotero.DataDirectory.dir;
let contents1 = `user_pref("extensions.lastAppVersion", "49.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
`;
let contents2 = `user_pref("extensions.lastAppVersion", "50.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.zotero.dataDir", "${dataDir}");
user_pref("extensions.zotero.useDataDir", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
`;
let otherDir = OS.Path.join(OS.Path.dirname(dataDir), "Other");
let contents3 = `user_pref("extensions.lastAppVersion", "51.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.zotero.dataDir", "${otherDir}");
user_pref("extensions.zotero.useDataDir", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
`;
let prefsFile1 = OS.Path.join(tmpDir, "Profiles", profile1, "prefs.js");
let prefsFile2 = OS.Path.join(tmpDir, "Profiles", profile2, "prefs.js");
let prefsFile3 = OS.Path.join(tmpDir, "Profiles", profile3, "prefs.js");
yield Zotero.File.putContentsAsync(prefsFile1, contents1);
yield Zotero.File.putContentsAsync(prefsFile2, contents2);
yield Zotero.File.putContentsAsync(prefsFile3, contents3);
var stub = sinon.stub(Zotero.Profile, "getOtherAppProfilesDir")
.returns(OS.Path.join(tmpDir, "Profiles"));
var dirs = yield Zotero.Profile.findOtherProfilesUsingDataDirectory(dataDir);
stub.restore();
assert.sameMembers(dirs, [OS.Path.join(tmpDir, "Profiles", profile2)]);
assert.lengthOf(dirs, 1);
});
it("should find other-app profile with directory as a legacy default location", function* () {
let contents1 = `user_pref("extensions.lastAppVersion", "49.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
`;
let contents2 = `user_pref("extensions.lastAppVersion", "50.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
`;
let prefsFile1 = OS.Path.join(tmpDir, "Profiles", profile1, "prefs.js");
let prefsFile2 = OS.Path.join(tmpDir, "Profiles", profile2, "prefs.js");
yield Zotero.File.putContentsAsync(prefsFile1, contents1);
yield Zotero.File.putContentsAsync(prefsFile2, contents2);
var stub = sinon.stub(Zotero.Profile, "getOtherAppProfilesDir")
.returns(OS.Path.join(tmpDir, "Profiles"));
Components.utils.import("resource://zotero/config.js");
var dirs = yield Zotero.Profile.findOtherProfilesUsingDataDirectory(
OS.Path.join(OS.Path.dirname(prefsFile1), Zotero.DataDirectory.legacyDirName)
);
stub.restore();
assert.sameMembers(dirs, [OS.Path.join(tmpDir, "Profiles", profile1)]);
assert.lengthOf(dirs, 1);
});
});
describe("#updateProfileDataDirectory()", function () {
it("should add new lines to prefs.js", function* () {
let prefsFile = OS.Path.join(tmpDir, "Profiles", profile1, "prefs.js");
let oldDir = OS.Path.join(OS.Path.dirname(tmpDir), "Old", "Zotero");
let newDir = OS.Path.join(OS.Path.dirname(tmpDir), "New", "Zotero");
let contents = `user_pref("extensions.lastAppVersion", "50.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
`;
let addition = `user_pref("extensions.zotero.dataDir", "${newDir}");
user_pref("extensions.zotero.lastDataDir", "${newDir}");
user_pref("extensions.zotero.useDataDir", true);
`;
yield Zotero.File.putContentsAsync(prefsFile, contents);
yield Zotero.Profile.updateProfileDataDirectory(
OS.Path.join(tmpDir, "Profiles", profile1),
oldDir,
newDir
);
let newContents = yield Zotero.File.getContentsAsync(prefsFile);
assert.equal(newContents, contents + addition);
});
it("should replace existing lines in prefs.js", function* () {
let prefsFile = OS.Path.join(tmpDir, "Profiles", profile1, "prefs.js");
let oldDir = OS.Path.join(OS.Path.dirname(tmpDir), "Old", "Zotero");
let newDir = OS.Path.join(OS.Path.dirname(tmpDir), "New", "Zotero");
let contents = `user_pref("extensions.lastAppVersion", "50.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.zotero.dataDir", "old-mac-persistent-descriptor");
user_pref("extensions.zotero.lastDataDir", "${oldDir}");
user_pref("extensions.zotero.useDataDir", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
`;
let expectedContents = `user_pref("extensions.lastAppVersion", "50.0");
user_pref("extensions.shownSelectionUI", true);
user_pref("extensions.ui.locale.hidden", true);
user_pref("loop.copy.ticket", 196);
user_pref("extensions.zotero.dataDir", "${newDir}");
user_pref("extensions.zotero.lastDataDir", "${newDir}");
user_pref("extensions.zotero.useDataDir", true);
`;
yield Zotero.File.putContentsAsync(prefsFile, contents);
yield Zotero.Profile.updateProfileDataDirectory(
OS.Path.join(tmpDir, "Profiles", profile1),
oldDir,
newDir
);
let newContents = yield Zotero.File.getContentsAsync(prefsFile);
assert.equal(newContents, expectedContents);
});
});
});

View file

@ -79,7 +79,7 @@ describe("Zotero.Sync.Data.Local", function() {
// extra1 functionality not used at the moment // extra1 functionality not used at the moment
it.skip("should prompt for data reset and allow to choose a new data directory", function* (){ it.skip("should prompt for data reset and allow to choose a new data directory", function* (){
sinon.stub(Zotero, 'forceNewDataDirectory').returns(true); sinon.stub(Zotero.DataDirectory, 'forceChange').returns(true);
yield Zotero.Users.setCurrentUserID(1); yield Zotero.Users.setCurrentUserID(1);
yield Zotero.Users.setCurrentUsername("A"); yield Zotero.Users.setCurrentUsername("A");
@ -88,10 +88,10 @@ describe("Zotero.Sync.Data.Local", function() {
var cont = yield Zotero.Sync.Data.Local.checkUser(window, 2, "B"); var cont = yield Zotero.Sync.Data.Local.checkUser(window, 2, "B");
var resetDataDirFileExists = yield OS.File.exists(resetDataDirFile); var resetDataDirFileExists = yield OS.File.exists(resetDataDirFile);
assert.isTrue(cont); assert.isTrue(cont);
assert.isTrue(Zotero.forceNewDataDirectory.called); assert.isTrue(Zotero.DataDirectory.forceChange.called);
assert.isFalse(resetDataDirFileExists); assert.isFalse(resetDataDirFileExists);
Zotero.forceNewDataDirectory.restore(); Zotero.DataDirectory.forceChange.restore();
}); });
}); });