Migrate data directory automatically on macOS and Linux

If data directory is within the profile directory and we can move the
subdirectories instantaneously with /bin/mv, just do it silently at startup.
This commit is contained in:
Dan Stillman 2016-11-22 01:40:39 -05:00
parent 288d0c7c06
commit ef3e098586
5 changed files with 164 additions and 86 deletions

View File

@ -65,9 +65,7 @@ Zotero_Preferences.Advanced = {
); );
if (index == 0) { if (index == 0) {
yield Zotero.File.putContentsAsync( yield Zotero.markDataDirectoryForMigration(currentDir);
OS.Path.join(currentDir, Zotero.DATA_DIR_MIGRATION_MARKER), currentDir
);
Zotero.Utilities.Internal.quitZotero(true); Zotero.Utilities.Internal.quitZotero(true);
} }
}), }),

View File

@ -318,6 +318,12 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
} }
if (!Zotero.isConnector) { if (!Zotero.isConnector) {
// On macOS and Linux, migrate the data directory automatically
if (this.canMigrateDataDirectory(dataDir.path)
// Should match check in Zotero.File.moveDirectory()
&& !Zotero.isWin && (yield OS.File.exists("/bin/mv"))) {
yield this.markDataDirectoryForMigration(dataDir.path, true);
}
yield Zotero.checkForDataDirectoryMigration(dataDir.path, this.getDefaultDataDir()); yield Zotero.checkForDataDirectoryMigration(dataDir.path, this.getDefaultDataDir());
if (this.skipLoading) { if (this.skipLoading) {
return; return;
@ -975,13 +981,14 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
let dbFile = OS.Path.join(prefVal, this.getDatabaseFilename()); let dbFile = OS.Path.join(prefVal, this.getDatabaseFilename());
if (Zotero.File.pathToFile(migrationMarker).exists() if (Zotero.File.pathToFile(migrationMarker).exists()
&& !(Zotero.File.pathToFile(dbFile).exists())) { && !(Zotero.File.pathToFile(dbFile).exists())) {
let fileStr = Zotero.File.getContents(migrationMarker); let contents = Zotero.File.getContents(migrationMarker);
try { try {
file = Zotero.File.pathToFile(fileStr); let { sourceDir } = JSON.parse(contents);
file = Zotero.File.pathToFile(sourceDir);
} }
catch (e) { catch (e) {
Zotero.logError(e); Zotero.logError(e);
Zotero.debug(`Invalid path '${fileStr}' in marker file`, 1); Zotero.debug(`Invalid marker file:\n\n${contents}`, 1);
e = { name: "NS_ERROR_FILE_NOT_FOUND" }; e = { name: "NS_ERROR_FILE_NOT_FOUND" };
throw e; throw e;
} }
@ -1435,6 +1442,17 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
this.DATA_DIR_MIGRATION_MARKER = 'migrate-dir'; 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 * Migrate data directory if necessary and show any errors
* *
@ -1443,7 +1461,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
* the default data directory * the default data directory
*/ */
this.checkForDataDirectoryMigration = Zotero.Promise.coroutine(function* (dataDir, newDir) { this.checkForDataDirectoryMigration = Zotero.Promise.coroutine(function* (dataDir, newDir) {
let migrationMarker = OS.Path.join(dataDir, Zotero.DATA_DIR_MIGRATION_MARKER); let migrationMarker = OS.Path.join(dataDir, this.DATA_DIR_MIGRATION_MARKER);
try { try {
var exists = yield OS.File.exists(migrationMarker) var exists = yield OS.File.exists(migrationMarker)
} }
@ -1457,6 +1475,20 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
let oldDir; let oldDir;
let partial = false; let partial = false;
// Check whether this is an automatic or manual migration
let contents;
try {
contents = yield Zotero.File.getContentsAsync(migrationMarker);
var { 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 // Not set to the default directory, so use current as old directory
if (dataDir != newDir) { if (dataDir != newDir) {
oldDir = dataDir; oldDir = dataDir;
@ -1464,13 +1496,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
// Unfinished migration -- already using new directory, so get path to previous // Unfinished migration -- already using new directory, so get path to previous
// directory from the migration marker // directory from the migration marker
else { else {
try { oldDir = sourceDir;
oldDir = yield Zotero.File.getContentsAsync(migrationMarker);
}
catch (e) {
Zotero.logError(e);
return false;
}
partial = true; partial = true;
} }
@ -1480,6 +1506,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
}.bind(this); }.bind(this);
let errors; let errors;
let mode = automatic ? 'automatic' : 'manual';
try { try {
this.showZoteroPaneProgressMeter(Zotero.getString("dataDir.migration.inProgress")); this.showZoteroPaneProgressMeter(Zotero.getString("dataDir.migration.inProgress"));
errors = yield Zotero.migrateDataDirectory(oldDir, newDir, partial, progressHandler); errors = yield Zotero.migrateDataDirectory(oldDir, newDir, partial, progressHandler);
@ -1494,11 +1521,11 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING); + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
let index = ps.confirmEx(null, let index = ps.confirmEx(null,
Zotero.getString('dataDir.migration.failure.title'), Zotero.getString('dataDir.migration.failure.title'),
Zotero.getString('dataDir.migration.failure.full.text1') Zotero.getString(`dataDir.migration.failure.full.${mode}.text1`, ZOTERO_CONFIG.CLIENT_NAME)
+ "\n\n" + "\n\n"
+ e + e
+ "\n\n" + "\n\n"
+ Zotero.getString('dataDir.migration.failure.full.text2', Zotero.appName) + Zotero.getString(`dataDir.migration.failure.full.${mode}.text2`, Zotero.appName)
+ "\n\n" + "\n\n"
+ Zotero.getString('dataDir.migration.failure.full.current', oldDir) + Zotero.getString('dataDir.migration.failure.full.current', oldDir)
+ "\n\n" + "\n\n"
@ -1532,7 +1559,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING); + (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING);
let index = ps.confirmEx(null, let index = ps.confirmEx(null,
Zotero.getString('dataDir.migration.failure.title'), Zotero.getString('dataDir.migration.failure.title'),
Zotero.getString('dataDir.migration.failure.partial.text', Zotero.getString(`dataDir.migration.failure.partial.${mode}.text`,
[ZOTERO_CONFIG.CLIENT_NAME, Zotero.appName]) [ZOTERO_CONFIG.CLIENT_NAME, Zotero.appName])
+ "\n\n" + "\n\n"
+ Zotero.getString('dataDir.migration.failure.partial.old', oldDir) + Zotero.getString('dataDir.migration.failure.partial.old', oldDir)
@ -1714,6 +1741,8 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
let newMigrationMarker = OS.Path.join(newDir, Zotero.DATA_DIR_MIGRATION_MARKER); let newMigrationMarker = OS.Path.join(newDir, Zotero.DATA_DIR_MIGRATION_MARKER);
Zotero.debug("Removing " + newMigrationMarker); Zotero.debug("Removing " + newMigrationMarker);
yield OS.File.remove(newMigrationMarker); yield OS.File.remove(newMigrationMarker);
Zotero.debug("Migration successful");
} }
catch (e) { catch (e) {
addError(e); addError(e);

View File

@ -461,8 +461,9 @@ var ZoteroPane = new function()
function isShowing() { function isShowing() {
var zoteroPane = document.getElementById('zotero-pane-stack'); var zoteroPane = document.getElementById('zotero-pane-stack');
return zoteroPane.getAttribute('hidden') != 'true' && return zoteroPane
zoteroPane.getAttribute('collapsed') != 'true'; && zoteroPane.getAttribute('hidden') != 'true'
&& zoteroPane.getAttribute('collapsed') != 'true';
} }
function isFullScreen() { function isFullScreen() {

View File

@ -136,12 +136,15 @@ 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.inProgress = Migration in progress — do not interrupt… dataDir.migration.inProgress = Migration in progress — do not interrupt…
dataDir.migration.failure.title = Data Directory Migration Error 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. Close any open attachment files and try again. You can also quit %2$S and attempt to move the remaining files manually. dataDir.migration.failure.partial.automatic.text = %1$S attempted to move your data directory to a new default location, but some files could not be transferred. Close any open attachment files and try again. You can also quit %2$S and attempt to move the remaining files manually.
dataDir.migration.failure.partial.manual.text = Some files in your %1$S data directory could not be transferred to the new location. Close any open attachment files and try again. You can also quit %2$S and attempt to move the remaining files manually.
dataDir.migration.failure.partial.old = Old directory: %S dataDir.migration.failure.partial.old = Old directory: %S
dataDir.migration.failure.partial.new = New directory: %S dataDir.migration.failure.partial.new = New directory: %S
dataDir.migration.failure.partial.showDirectoriesAndQuit = Show Directories and Quit dataDir.migration.failure.partial.showDirectoriesAndQuit = Show Directories and Quit
dataDir.migration.failure.full.text1 = Your data directory could not be migrated. dataDir.migration.failure.full.automatic.text1 = %S attempted to move your data directory to a new default location, but the migration could not be completed.
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.automatic.text2 = It is recommended that you close %S and move your data directory manually.
dataDir.migration.failure.full.manual.text1 = Your %S data directory could not be migrated.
dataDir.migration.failure.full.manual.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.current = Current location: %S
dataDir.migration.failure.full.recommended = Recommended location: %S dataDir.migration.failure.full.recommended = Recommended location: %S
dataDir.migration.failure.full.showCurrentDirectoryAndQuit = Show Current Directory and Quit dataDir.migration.failure.full.showCurrentDirectoryAndQuit = Show Current Directory and Quit

View File

@ -68,7 +68,7 @@ describe("Zotero Core Functions", function () {
stub2.restore(); stub2.restore();
}; };
var populateDataDirectory = Zotero.Promise.coroutine(function* (dir, srcDir) { var populateDataDirectory = Zotero.Promise.coroutine(function* (dir, srcDir, automatic = false) {
yield OS.File.makeDir(dir, { unixMode: 0o755 }); yield OS.File.makeDir(dir, { unixMode: 0o755 });
let storageDir = OS.Path.join(dir, 'storage'); let storageDir = OS.Path.join(dir, 'storage');
let storageDir1 = OS.Path.join(storageDir, 'AAAAAAAA'); let storageDir1 = OS.Path.join(storageDir, 'AAAAAAAA');
@ -92,7 +92,13 @@ describe("Zotero Core Functions", function () {
yield Zotero.File.putContentsAsync(OS.Path.join(translatorsDir, translatorName1), str4); yield Zotero.File.putContentsAsync(OS.Path.join(translatorsDir, translatorName1), str4);
yield Zotero.File.putContentsAsync(OS.Path.join(translatorsDir, translatorName2), str5); yield Zotero.File.putContentsAsync(OS.Path.join(translatorsDir, translatorName2), str5);
// Migration marker // Migration marker
yield Zotero.File.putContentsAsync(migrationMarker, srcDir || dir); yield Zotero.File.putContentsAsync(
migrationMarker,
JSON.stringify({
sourceDir: srcDir || dir,
automatic
})
);
}); });
var checkMigration = Zotero.Promise.coroutine(function* (options = {}) { var checkMigration = Zotero.Promise.coroutine(function* (options = {}) {
@ -137,74 +143,115 @@ describe("Zotero Core Functions", function () {
resetCommandMode(); resetCommandMode();
}); });
it("should show error on partial failure", function* () { var tests = [];
Zotero.Debug.init(true); function add(desc, fn) {
yield populateDataDirectory(oldDir); tests.push([desc, fn]);
}
let origFunc = OS.File.move;
let stub3 = sinon.stub(OS.File, "move", function () { add("should show error on partial failure", function (automatic) {
if (OS.Path.basename(arguments[0]) == storageFile1) { return function* () {
return Zotero.Promise.reject(new Error("Error")); Zotero.Debug.init(true);
} yield populateDataDirectory(oldDir, null, automatic);
else {
let args; let origFunc = OS.File.move;
if (Zotero.platformMajorVersion < 46) { let stub3 = sinon.stub(OS.File, "move", function () {
args = Array.from(arguments); if (OS.Path.basename(arguments[0]) == storageFile1) {
return Zotero.Promise.reject(new Error("Error"));
} }
else { else {
args = arguments; let args;
if (Zotero.platformMajorVersion < 46) {
args = Array.from(arguments);
}
else {
args = arguments;
}
return origFunc(...args);
} }
return origFunc(...args); });
} let stub4 = sinon.stub(Zotero.File, "reveal").returns(Zotero.Promise.resolve());
}); let stub5 = sinon.stub(Zotero.Utilities.Internal, "quitZotero");
let stub4 = sinon.stub(Zotero.File, "reveal").returns(Zotero.Promise.resolve());
let stub5 = sinon.stub(Zotero.Utilities.Internal, "quitZotero"); var promise2;
// Click "Try Again" the first time, and then "Show Directories and Quit Zotero"
var promise2; var promise = waitForDialog(function (dialog) {
// Click "Try Again" the first time, and then "Show Directories and Quit Zotero" promise2 = waitForDialog(null, 'extra1');
var promise = waitForDialog(function () {
promise2 = waitForDialog(null, 'extra1'); // Make sure we're displaying the right message for this mode (automatic or manual)
}); Components.utils.import("resource://zotero/config.js");
yield Zotero.checkForDataDirectoryMigration(oldDir, newDir); assert.include(
yield promise; dialog.document.documentElement.textContent,
yield promise2; Zotero.getString(
`dataDir.migration.failure.partial.${automatic ? 'automatic' : 'manual'}.text`,
assert.isTrue(stub4.calledTwice); [ZOTERO_CONFIG.CLIENT_NAME, Zotero.appName]
assert.isTrue(stub4.getCall(0).calledWith(oldStorageDir)); )
assert.isTrue(stub4.getCall(1).calledWith(newDBFile)); );
assert.isTrue(stub5.called); });
yield Zotero.checkForDataDirectoryMigration(oldDir, newDir);
stub3.restore(); yield promise;
stub4.restore(); yield promise2;
stub5.restore();
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* () { add("should show error on full failure", function (automatic) {
yield populateDataDirectory(oldDir); return function* () {
yield populateDataDirectory(oldDir, null, automatic);
let origFunc = OS.File.move;
let stub3 = sinon.stub(OS.File, "move", function () { let origFunc = OS.File.move;
if (OS.Path.basename(arguments[0]) == dbFilename) { let stub3 = sinon.stub(OS.File, "move", function () {
return Zotero.Promise.reject(new Error("Error")); if (OS.Path.basename(arguments[0]) == dbFilename) {
} return Zotero.Promise.reject(new Error("Error"));
else { }
return origFunc(...arguments); 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(function (dialog) {
// Make sure we're displaying the right message for this mode (automatic or manual)
Components.utils.import("resource://zotero/config.js");
assert.include(
dialog.document.documentElement.textContent,
Zotero.getString(
`dataDir.migration.failure.full.${automatic ? 'automatic' : 'manual'}.text1`,
ZOTERO_CONFIG.CLIENT_NAME
)
);
});
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();
};
});
describe("automatic mode", function () {
tests.forEach(arr => {
it(arr[0], arr[1](true));
});
});
describe("manual mode", function () {
tests.forEach(arr => {
it(arr[0], arr[1](false));
}); });
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* () { it("should remove marker if old directory doesn't exist", function* () {