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) {
yield Zotero.File.putContentsAsync(
OS.Path.join(currentDir, Zotero.DATA_DIR_MIGRATION_MARKER), currentDir
);
yield Zotero.markDataDirectoryForMigration(currentDir);
Zotero.Utilities.Internal.quitZotero(true);
}
}),

View file

@ -318,6 +318,12 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
}
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());
if (this.skipLoading) {
return;
@ -975,13 +981,14 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
let dbFile = OS.Path.join(prefVal, this.getDatabaseFilename());
if (Zotero.File.pathToFile(migrationMarker).exists()
&& !(Zotero.File.pathToFile(dbFile).exists())) {
let fileStr = Zotero.File.getContents(migrationMarker);
let contents = Zotero.File.getContents(migrationMarker);
try {
file = Zotero.File.pathToFile(fileStr);
let { sourceDir } = JSON.parse(contents);
file = Zotero.File.pathToFile(sourceDir);
}
catch (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" };
throw e;
}
@ -1435,6 +1442,17 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
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
*
@ -1443,7 +1461,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
* the default data directory
*/
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 {
var exists = yield OS.File.exists(migrationMarker)
}
@ -1457,6 +1475,20 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
let oldDir;
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
if (dataDir != newDir) {
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
// directory from the migration marker
else {
try {
oldDir = yield Zotero.File.getContentsAsync(migrationMarker);
}
catch (e) {
Zotero.logError(e);
return false;
}
oldDir = sourceDir;
partial = true;
}
@ -1480,6 +1506,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
}.bind(this);
let errors;
let mode = automatic ? 'automatic' : 'manual';
try {
this.showZoteroPaneProgressMeter(Zotero.getString("dataDir.migration.inProgress"));
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);
let index = ps.confirmEx(null,
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"
+ e
+ "\n\n"
+ Zotero.getString('dataDir.migration.failure.full.text2', Zotero.appName)
+ Zotero.getString(`dataDir.migration.failure.full.${mode}.text2`, Zotero.appName)
+ "\n\n"
+ Zotero.getString('dataDir.migration.failure.full.current', oldDir)
+ "\n\n"
@ -1532,7 +1559,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
+ (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.text',
Zotero.getString(`dataDir.migration.failure.partial.${mode}.text`,
[ZOTERO_CONFIG.CLIENT_NAME, Zotero.appName])
+ "\n\n"
+ 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);
Zotero.debug("Removing " + newMigrationMarker);
yield OS.File.remove(newMigrationMarker);
Zotero.debug("Migration successful");
}
catch (e) {
addError(e);

View file

@ -461,8 +461,9 @@ var ZoteroPane = new function()
function isShowing() {
var zoteroPane = document.getElementById('zotero-pane-stack');
return zoteroPane.getAttribute('hidden') != 'true' &&
zoteroPane.getAttribute('collapsed') != 'true';
return zoteroPane
&& zoteroPane.getAttribute('hidden') != 'true'
&& zoteroPane.getAttribute('collapsed') != 'true';
}
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.migration.inProgress = Migration in progress — do not interrupt…
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.new = New directory: %S
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.text2 = It is recommended that you close %S and manually move your data directory to the new default location.
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.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.recommended = Recommended location: %S
dataDir.migration.failure.full.showCurrentDirectoryAndQuit = Show Current Directory and Quit

View file

@ -68,7 +68,7 @@ describe("Zotero Core Functions", function () {
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 });
let storageDir = OS.Path.join(dir, 'storage');
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, translatorName2), str5);
// 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 = {}) {
@ -137,74 +143,115 @@ describe("Zotero Core Functions", function () {
resetCommandMode();
});
it("should show error on partial failure", function* () {
Zotero.Debug.init(true);
yield populateDataDirectory(oldDir);
let origFunc = OS.File.move;
let stub3 = sinon.stub(OS.File, "move", function () {
if (OS.Path.basename(arguments[0]) == storageFile1) {
return Zotero.Promise.reject(new Error("Error"));
}
else {
let args;
if (Zotero.platformMajorVersion < 46) {
args = Array.from(arguments);
var tests = [];
function add(desc, fn) {
tests.push([desc, fn]);
}
add("should show error on partial failure", function (automatic) {
return function* () {
Zotero.Debug.init(true);
yield populateDataDirectory(oldDir, null, automatic);
let origFunc = OS.File.move;
let stub3 = sinon.stub(OS.File, "move", function () {
if (OS.Path.basename(arguments[0]) == storageFile1) {
return Zotero.Promise.reject(new Error("Error"));
}
else {
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");
var promise2;
// Click "Try Again" the first time, and then "Show Directories and Quit Zotero"
var promise = waitForDialog(function () {
promise2 = waitForDialog(null, 'extra1');
});
yield Zotero.checkForDataDirectoryMigration(oldDir, newDir);
yield promise;
yield promise2;
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();
});
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 promise = waitForDialog(function (dialog) {
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");
assert.include(
dialog.document.documentElement.textContent,
Zotero.getString(
`dataDir.migration.failure.partial.${automatic ? 'automatic' : 'manual'}.text`,
[ZOTERO_CONFIG.CLIENT_NAME, Zotero.appName]
)
);
});
yield Zotero.checkForDataDirectoryMigration(oldDir, newDir);
yield promise;
yield promise2;
assert.isTrue(stub4.calledTwice);
assert.isTrue(stub4.getCall(0).calledWith(oldStorageDir));
assert.isTrue(stub4.getCall(1).calledWith(newDBFile));
assert.isTrue(stub5.called);
stub3.restore();
stub4.restore();
stub5.restore();
};
});
it("should show error on full failure", function* () {
yield populateDataDirectory(oldDir);
let origFunc = OS.File.move;
let stub3 = sinon.stub(OS.File, "move", function () {
if (OS.Path.basename(arguments[0]) == dbFilename) {
return Zotero.Promise.reject(new Error("Error"));
}
else {
return origFunc(...arguments);
}
add("should show error on full failure", function (automatic) {
return function* () {
yield populateDataDirectory(oldDir, null, automatic);
let origFunc = OS.File.move;
let stub3 = sinon.stub(OS.File, "move", function () {
if (OS.Path.basename(arguments[0]) == dbFilename) {
return Zotero.Promise.reject(new Error("Error"));
}
else {
return origFunc(...arguments);
}
});
let stub4 = sinon.stub(Zotero.File, "reveal").returns(Zotero.Promise.resolve());
let stub5 = sinon.stub(Zotero.Utilities.Internal, "quitZotero");
var promise = waitForDialog(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* () {