www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

dataDirectory.js (38031B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2016 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 "use strict";
     27 
     28 Zotero.DataDirectory = {
     29 	MIGRATION_MARKER: 'migrate-dir',
     30 	
     31 	get dir() {
     32 		if (!this._dir) {
     33 			throw new Error("Data directory not initialized");
     34 		}
     35 		return this._dir;
     36 	},
     37 	
     38 	get defaultDir() {
     39 		// Use special data directory for tests
     40 		if (Zotero.test) {
     41 			return OS.Path.join(OS.Path.dirname(OS.Constants.Path.profileDir), "Zotero");
     42 		}
     43 		return OS.Path.join(OS.Constants.Path.homeDir, ZOTERO_CONFIG.CLIENT_NAME);
     44 	},
     45 	
     46 	get legacyDirName() {
     47 		return ZOTERO_CONFIG.ID;
     48 	},
     49 	
     50 	_dir: null,
     51 	_warnOnUnsafeLocation: true,
     52 	
     53 	
     54 	init: Zotero.Promise.coroutine(function* () {
     55 		var dataDir;
     56 		var dbFilename = this.getDatabaseFilename();
     57 		// Handle directory specified on command line
     58 		if (Zotero.forceDataDir) {
     59 			let dir = Zotero.forceDataDir;
     60 			// Profile subdirectory
     61 			if (dir == 'profile') {
     62 				dataDir = OS.Path.join(Zotero.Profile.dir, this.legacyDirName);
     63 			}
     64 			// Absolute path
     65 			else {
     66 				// Ignore non-absolute paths
     67 				if ("winIsAbsolute" in OS.Path) {
     68 					if (!OS.Path.winIsAbsolute(dir)) {
     69 						dir = false;
     70 					}
     71 				}
     72 				else if (!dir.startsWith('/')) {
     73 					dir = false;
     74 				}
     75 				if (!dir) {
     76 					throw `-datadir requires an absolute path or 'profile' ('${Zotero.forceDataDir}' given)`;
     77 				}
     78 				
     79 				// Require parent directory to exist
     80 				if (!(yield OS.File.exists(OS.Path.dirname(dir)))) {
     81 					throw `Parent directory of -datadir ${dir} not found`;
     82 				}
     83 				
     84 				dataDir = dir;
     85 			}
     86 		}
     87 		else if (Zotero.Prefs.get('useDataDir')) {
     88 			let prefVal = Zotero.Prefs.get('dataDir');
     89 			// Convert old persistent descriptor pref to string path and clear obsolete lastDataDir pref
     90 			//
     91 			// persistentDescriptor now appears to return (and parse) a string path anyway on macOS,
     92 			// which is the only place where it didn't use a string path to begin with, but be explicit
     93 			// just in case there's some difference.
     94 			//
     95 			// A post-Mozilla prefs migration should do this same check, and then this conditional can
     96 			// be removed.
     97 			if (Zotero.Prefs.get('lastDataDir')) {
     98 				let nsIFile;
     99 				try {
    100 					nsIFile = Components.classes["@mozilla.org/file/local;1"]
    101 						.createInstance(Components.interfaces.nsILocalFile);
    102 					nsIFile.persistentDescriptor = prefVal;
    103 				}
    104 				catch (e) {
    105 					Zotero.debug("Persistent descriptor in extensions.zotero.dataDir did not resolve", 1);
    106 					e = { name: "NS_ERROR_FILE_NOT_FOUND" };
    107 					throw e;
    108 				}
    109 				// This removes lastDataDir
    110 				this.set(nsIFile.path);
    111 				dataDir = nsIFile.path;
    112 			}
    113 			else {
    114 				// If there's a migration marker in this directory and no database, migration was
    115 				// interrupted before the database could be moved (or moving failed), so use the source
    116 				// directory specified in the marker file.
    117 				let migrationMarker = OS.Path.join(prefVal, this.MIGRATION_MARKER);
    118 				let dbFile = OS.Path.join(prefVal, dbFilename);
    119 				
    120 				if ((yield OS.File.exists(migrationMarker)) && !(yield OS.File.exists(dbFile))) {
    121 					let contents = yield Zotero.File.getContentsAsync(migrationMarker);
    122 					try {
    123 						let { sourceDir } = JSON.parse(contents);
    124 						dataDir = OS.Path.normalize(sourceDir);
    125 					}
    126 					catch (e) {
    127 						Zotero.logError(e);
    128 						Zotero.debug(`Invalid marker file:\n\n${contents}`, 1);
    129 						throw { name: "NS_ERROR_FILE_NOT_FOUND" };
    130 					}
    131 				}
    132 				else {
    133 					try {
    134 						dataDir = OS.Path.normalize(prefVal);
    135 					}
    136 					catch (e) {
    137 						Zotero.logError(e);
    138 						Zotero.debug(`Invalid path '${prefVal}' in dataDir pref`, 1);
    139 						throw { name: "NS_ERROR_FILE_NOT_FOUND" };
    140 					}
    141 				}
    142 			}
    143 			
    144 			if (!(yield OS.File.exists(dataDir)) && dataDir != this.defaultDir) {
    145 				// If set to a legacy directory that doesn't exist, forget about it and just use the
    146 				// new default location, which will either exist or be created below. The most likely
    147 				// cause of this is a migration, so don't bother looking in other-app profiles.
    148 				if (this.isLegacy(dataDir)) {
    149 					let newDefault = this.defaultDir;
    150 					Zotero.debug(`Legacy data directory ${dataDir} from pref not found `
    151 						+ `-- reverting to ${newDefault}`, 1);
    152 					dataDir = newDefault;
    153 					this.set(newDefault);
    154 				}
    155 				// For other custom directories that don't exist, show not-found dialog
    156 				else {
    157 					Zotero.debug(`Custom data directory ${dataDir} not found`, 1);
    158 					throw { name: "NS_ERROR_FILE_NOT_FOUND" };
    159 				}
    160 			}
    161 			
    162 			try {
    163 				if (dataDir != this.defaultDir
    164 						&& this.isLegacy(dataDir)
    165 						&& (yield OS.File.exists(OS.Path.join(this.defaultDir, 'move-to-old')))) {
    166 					let newPath = this.defaultDir + '-Old';
    167 					if (yield OS.File.exists(newPath)) {
    168 						newPath += "-1";
    169 					}
    170 					yield Zotero.File.moveDirectory(this.defaultDir, newPath);
    171 					yield OS.File.remove(OS.Path.join(newPath, 'move-to-old'));
    172 				}
    173 			}
    174 			catch (e) {
    175 				Zotero.logError(e);
    176 			}
    177 		}
    178 		// New installation of 5.0+ with no data directory specified, so check all the places the data
    179 		// could be
    180 		else {
    181 			Zotero.fxProfileAccessError = false;
    182 			
    183 			dataDir = this.defaultDir;
    184 			
    185 			// If there's already a profile pointing to the default location, use a different
    186 			// data directory named after the profile, as long as one either doesn't exist yet or
    187 			// one does and it contains a database
    188 			try {
    189 				if ((yield Zotero.Profile.findOtherProfilesUsingDataDirectory(dataDir, false)).length) {
    190 					let profileName = OS.Path.basename(Zotero.Profile.dir).match(/[^.]+\.(.+)/)[1];
    191 					let newDataDir = this.defaultDir + ' ' + profileName;
    192 					if (!(yield OS.File.exists(newDataDir))
    193 							|| (yield OS.File.exists(OS.Path.join(newDataDir, dbFilename)))) {
    194 						dataDir = newDataDir;
    195 					}
    196 				}
    197 			}
    198 			catch (e) {
    199 				Zotero.logError(e);
    200 			}
    201 			
    202 			// Check for ~/Zotero/zotero.sqlite
    203 			let dbFile = OS.Path.join(dataDir, dbFilename);
    204 			if (yield OS.File.exists(dbFile)) {
    205 				Zotero.debug("Using data directory " + dataDir);
    206 				this._cache(dataDir);
    207 				
    208 				// Set as a custom data directory so that 4.0 uses it
    209 				this.set(dataDir);
    210 				
    211 				return dataDir;
    212 			}
    213 			
    214 			let useProfile = false;
    215 			let useFirefoxProfile = false;
    216 			let useFirefoxProfileCustom = false;
    217 			
    218 			// Check for <profile dir>/zotero/zotero.sqlite
    219 			let profileSubdirModTime;
    220 			try {
    221 				let dir = OS.Path.join(Zotero.Profile.dir, this.legacyDirName);
    222 				let dbFile = OS.Path.join(dir, dbFilename);
    223 				profileSubdirModTime = (yield OS.File.stat(dbFile)).lastModificationDate;
    224 				Zotero.debug(`Database found at ${dbFile}, last modified ${profileSubdirModTime}`);
    225 				dataDir = dir;
    226 				useProfile = true;
    227 			}
    228 			catch (e) {
    229 				if (!(e instanceof OS.File.Error && e.becauseNoSuchFile)) {
    230 					throw e;
    231 				}
    232 			}
    233 			
    234 			//
    235 			// Check Firefox directory
    236 			//
    237 			let profilesParent = OS.Path.dirname(Zotero.Profile.getOtherAppProfilesDir());
    238 			Zotero.debug("Looking for Firefox profile in " + profilesParent);
    239 			
    240 			// get default profile
    241 			var defProfile;
    242 			try {
    243 				defProfile = yield Zotero.Profile.getDefaultInProfilesDir(profilesParent);
    244 			}
    245 			catch (e) {
    246 				Zotero.debug("An error occurred locating the Firefox profile; "
    247 					+ "not attempting to migrate from Zotero for Firefox");
    248 				Zotero.logError(e);
    249 				Zotero.fxProfileAccessError = true;
    250 			}
    251 			if (defProfile) {
    252 				let profileDir = defProfile[0];
    253 				Zotero.debug("Found default profile at " + profileDir);
    254 				
    255 				// Read in prefs
    256 				let prefsFile = OS.Path.join(profileDir, "prefs.js");
    257 				if (yield OS.File.exists(prefsFile)) {
    258 					let prefs = yield Zotero.Profile.readPrefsFromFile(prefsFile);
    259 					
    260 					// Check for data dir pref
    261 					if (prefs['extensions.zotero.dataDir'] && prefs['extensions.zotero.useDataDir']) {
    262 						Zotero.debug(`Found custom dataDir of ${prefs['extensions.zotero.dataDir']}`);
    263 						let nsIFile;
    264 						try {
    265 							nsIFile = Components.classes["@mozilla.org/file/local;1"]
    266 								.createInstance(Components.interfaces.nsILocalFile);
    267 							nsIFile.persistentDescriptor = prefs['extensions.zotero.dataDir'];
    268 						}
    269 						catch (e) {
    270 							Zotero.logError(e);
    271 							if (!useProfile) {
    272 								Zotero.debug("Persistent descriptor in extensions.zotero.dataDir "
    273 									+ "did not resolve", 1);
    274 								throw { name: "NS_ERROR_FILE_NOT_FOUND" };
    275 							}
    276 						}
    277 						try {
    278 							let dbFile = OS.Path.join(nsIFile.path, dbFilename);
    279 							let mtime = (yield OS.File.stat(dbFile)).lastModificationDate;
    280 							Zotero.debug(`Database found at ${dbFile}, last modified ${mtime}`);
    281 							// If custom location has a newer DB, use that
    282 							if (!useProfile || mtime > profileSubdirModTime) {
    283 								dataDir = nsIFile.path;
    284 								useFirefoxProfileCustom = true;
    285 								useProfile = false;
    286 							}
    287 						}
    288 						catch (e) {
    289 							Zotero.logError(e);
    290 							// If we have a DB in the Zotero profile and get an error trying to
    291 							// access the custom location in Firefox, use the Zotero profile, since
    292 							// there's at least some chance it's right. Otherwise, throw an error.
    293 							if (!useProfile) {
    294 								// The error message normally gets the path from the pref, but
    295 								// we got it from the prefs file, so include it here
    296 								e.dataDir = nsIFile.path;
    297 								throw e;
    298 							}
    299 							Zotero.fxProfileAccessError = true;
    300 						}
    301 					}
    302 					// If no custom dir specified, check for a subdirectory
    303 					else {
    304 						try {
    305 							let dir = OS.Path.join(profileDir, this.legacyDirName);
    306 							let dbFile = OS.Path.join(dir, dbFilename);
    307 							let mtime = (yield OS.File.stat(dbFile)).lastModificationDate;
    308 							Zotero.debug(`Database found at ${dbFile}, last modified ${mtime}`);
    309 							// If newer than Zotero profile directory, use this one
    310 							if (!useProfile || mtime > profileSubdirModTime) {
    311 								dataDir = dir;
    312 								useFirefoxProfile = true;
    313 								useProfile = false;
    314 							}
    315 						}
    316 						// Legacy subdirectory doesn't exist or there was a problem accessing it, so
    317 						// just fall through to default location
    318 						catch (e) {
    319 							if (!(e instanceof OS.File.Error && e.becauseNoSuchFile)) {
    320 								Zotero.logError(e);
    321 								Zotero.fxProfileAccessError = true;
    322 							}
    323 						}
    324 					}
    325 					
    326 					// If using data directory from Zotero for Firefox, transfer those prefs, because
    327 					// the fact that that DB was more recent and wasn't set in the Zotero profile prefs
    328 					// means that they were using Firefox.
    329 					if (useFirefoxProfile || useFirefoxProfileCustom) {
    330 						for (let key in prefs) {
    331 							if (key.substr(0, ZOTERO_CONFIG.PREF_BRANCH.length) === ZOTERO_CONFIG.PREF_BRANCH
    332 									&& key !== "extensions.zotero.firstRun2") {
    333 								Zotero.Prefs.set(key.substr(ZOTERO_CONFIG.PREF_BRANCH.length), prefs[key]);
    334 							}
    335 						}
    336 						
    337 						// If data directory setting was transferred, use that
    338 						if (Zotero.Prefs.get('useDataDir')) {
    339 							return this.init();
    340 						}
    341 					}
    342 				}
    343 			}
    344 			
    345 			this.set(dataDir);
    346 		}
    347 		
    348 		Zotero.debug("Using data directory " + dataDir);
    349 		try {
    350 			yield Zotero.File.createDirectoryIfMissingAsync(dataDir);
    351 		}
    352 		catch (e) {
    353 			if (e instanceof OS.File.Error
    354 					&& (('unixErrno' in e && e.unixErrno == OS.Constants.libc.EACCES)
    355 						|| ('winLastError' in e && e.winLastError == OS.Constants.Win.ERROR_ACCESS_DENIED))) {
    356 				Zotero.restarting = true;
    357 				let isDefaultDir = dataDir == Zotero.DataDirectory.defaultDir;
    358 				let ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    359 					.createInstance(Components.interfaces.nsIPromptService);
    360 				let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
    361 					+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING;
    362 				if (!isDefaultDir) {
    363 					buttonFlags += ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING;
    364 				}
    365 				let title = Zotero.getString('general.accessDenied');
    366 				let msg = Zotero.getString('dataDir.dirCannotBeCreated', [Zotero.appName, dataDir])
    367 					+ "\n\n"
    368 					+ Zotero.getString('dataDir.checkDirWriteAccess', Zotero.appName);
    369 				
    370 				let index;
    371 				if (isDefaultDir) {
    372 					index = ps.confirmEx(null,
    373 						title,
    374 						msg,
    375 						buttonFlags,
    376 						Zotero.getString('dataDir.chooseNewDataDirectory'),
    377 						Zotero.getString('general.quit'),
    378 						null, null, {}
    379 					);
    380 					if (index == 0) {
    381 						let changed = yield Zotero.DataDirectory.choose(true);
    382 						if (!changed) {
    383 							Zotero.Utilities.Internal.quit();
    384 						}
    385 					}
    386 					else if (index == 1) {
    387 						Zotero.Utilities.Internal.quit();
    388 					}
    389 				}
    390 				else {
    391 					index = ps.confirmEx(null,
    392 						title,
    393 						msg,
    394 						buttonFlags,
    395 						Zotero.getString('dataDir.useDefaultLocation'),
    396 						Zotero.getString('general.quit'),
    397 						Zotero.getString('dataDir.chooseNewDataDirectory'),
    398 						null, {}
    399 					);
    400 					if (index == 0) {
    401 						Zotero.DataDirectory.set(Zotero.DataDirectory.defaultDir);
    402 						Zotero.Utilities.Internal.quit(true);
    403 					}
    404 					else if (index == 1) {
    405 						Zotero.Utilities.Internal.quit();
    406 					}
    407 					else if (index == 2) {
    408 						let changed = yield Zotero.DataDirectory.choose(true);
    409 						if (!changed) {
    410 							Zotero.Utilities.Internal.quit();
    411 							return;
    412 						}
    413 					}
    414 				}
    415 				return;
    416 			}
    417 		}
    418 		this._cache(dataDir);
    419 	}),
    420 	
    421 	
    422 	_cache: function (dir) {
    423 		this._dir = dir;
    424 	},
    425 	
    426 	
    427 	/**
    428 	 * @return {Boolean} - True if the directory changed; false otherwise
    429 	 */
    430 	set: function (path) {
    431 		var origPath = Zotero.Prefs.get('dataDir');
    432 		
    433 		Zotero.Prefs.set('dataDir', path);
    434 		// Clear legacy pref
    435 		Zotero.Prefs.clear('lastDataDir');
    436 		Zotero.Prefs.set('useDataDir', true);
    437 		
    438 		return path != origPath;
    439 	},
    440 	
    441 	
    442 	choose: Zotero.Promise.coroutine(function* (forceQuitNow, useHomeDir, moreInfoCallback) {
    443 		var win = Services.wm.getMostRecentWindow('navigator:browser');
    444 		var ps = Services.prompt;
    445 		
    446 		if (useHomeDir) {
    447 			let changed = this.set(this.defaultDir);
    448 			if (!changed) {
    449 				return false;
    450 			}
    451 		}
    452 		else {
    453 			var nsIFilePicker = Components.interfaces.nsIFilePicker;
    454 			while (true) {
    455 				var fp = Components.classes["@mozilla.org/filepicker;1"]
    456 							.createInstance(nsIFilePicker);
    457 				fp.init(win, Zotero.getString('dataDir.selectDir'), nsIFilePicker.modeGetFolder);
    458 				fp.displayDirectory = Zotero.File.pathToFile(
    459 					this._dir ? this._dir : OS.Path.dirname(this.defaultDir)
    460 				);
    461 				fp.appendFilters(nsIFilePicker.filterAll);
    462 				if (fp.show() == nsIFilePicker.returnOK) {
    463 					var file = fp.file;
    464 					let dialogText = '';
    465 					let dialogTitle = '';
    466 					
    467 					if (file.path == (Zotero.Prefs.get('lastDataDir') || Zotero.Prefs.get('dataDir'))) {
    468 						Zotero.debug("Data directory hasn't changed");
    469 						return false;
    470 					}
    471 					
    472 					// In dropbox folder
    473 					if (Zotero.File.isDropboxDirectory(file.path)) {
    474 						dialogTitle = Zotero.getString('general.warning');
    475 						dialogText = Zotero.getString('dataDir.unsafeLocation.selected.dropbox') + "\n\n"
    476 								+ Zotero.getString('dataDir.unsafeLocation.selected.useAnyway');
    477 					}
    478 					else if (file.directoryEntries.hasMoreElements()) {
    479 						let dbfile = file.clone();
    480 						dbfile.append(this.getDatabaseFilename());
    481 						
    482 						// Warn if non-empty and no zotero.sqlite
    483 						if (!dbfile.exists()) {
    484 							dialogTitle = Zotero.getString('dataDir.selectedDirNonEmpty.title');
    485 							dialogText = Zotero.getString('dataDir.selectedDirNonEmpty.text');
    486 						}
    487 					}
    488 					// Directory empty
    489 					else {
    490 						dialogTitle = Zotero.getString('dataDir.selectedDirEmpty.title');
    491 						dialogText = Zotero.getString('dataDir.selectedDirEmpty.text', Zotero.appName) + '\n\n'
    492 								+ Zotero.getString('dataDir.selectedDirEmpty.useNewDir');
    493 					}
    494 					// Warning dialog to be displayed
    495 					if(dialogText !== '') {
    496 						let buttonFlags = ps.STD_YES_NO_BUTTONS;
    497 						if (moreInfoCallback) {
    498 							buttonFlags += ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING;
    499 						}
    500 						let index = ps.confirmEx(null,
    501 							dialogTitle,
    502 							dialogText,
    503 							buttonFlags,
    504 							null,
    505 							null,
    506 							moreInfoCallback ? Zotero.getString('general.moreInformation') : null,
    507 							null, {});
    508 
    509 						// Not OK -- return to file picker
    510 						if (index == 1) {
    511 							continue;
    512 						}
    513 						else if (index == 2) {
    514 							setTimeout(function () {
    515 								moreInfoCallback();
    516 							}, 1);
    517 							return false;
    518 						}
    519 					}
    520 
    521 					this.set(file.path);
    522 					
    523 					break;
    524 				}
    525 				else {
    526 					return false;
    527 				}
    528 			}
    529 		}
    530 		
    531 		var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING);
    532 		if (!forceQuitNow) {
    533 			buttonFlags += (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
    534 		}
    535 		var app = Zotero.appName;
    536 		var index = ps.confirmEx(null,
    537 			Zotero.getString('general.restartRequired'),
    538 			Zotero.getString('general.restartRequiredForChange', app)
    539 				+ "\n\n" + Zotero.getString('dataDir.moveFilesToNewLocation', app),
    540 			buttonFlags,
    541 			Zotero.getString('general.quitApp', app),
    542 			forceQuitNow ? null : Zotero.getString('general.restartLater'),
    543 			null, null, {});
    544 		
    545 		if (forceQuitNow || index == 0) {
    546 			Services.startup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit);
    547 		}
    548 		
    549 		return useHomeDir ? true : file;
    550 	}),
    551 	
    552 	
    553 	forceChange: function (win) {
    554 		if (!win) {
    555 			win = Services.wm.getMostRecentWindow('navigator:browser');
    556 		}
    557 		var ps = Services.prompt;
    558 		
    559 		var nsIFilePicker = Components.interfaces.nsIFilePicker;
    560 		while (true) {
    561 			var fp = Components.classes["@mozilla.org/filepicker;1"]
    562 						.createInstance(nsIFilePicker);
    563 			fp.init(win, Zotero.getString('dataDir.selectNewDir', Zotero.clientName), nsIFilePicker.modeGetFolder);
    564 			fp.displayDirectory = Zotero.File.pathToFile(this.dir);
    565 			fp.appendFilters(nsIFilePicker.filterAll);
    566 			if (fp.show() == nsIFilePicker.returnOK) {
    567 				var file = fp.file;
    568 				
    569 				if (file.directoryEntries.hasMoreElements()) {
    570 					ps.alert(null,
    571 						Zotero.getString('dataDir.mustSelectEmpty.title'),
    572 						Zotero.getString('dataDir.mustSelectEmpty.text')
    573 					);
    574 					continue;
    575 				}
    576 				
    577 				this.set(file.path);
    578 				
    579 				return file;
    580 			} else {
    581 				return false;
    582 			}
    583 		}
    584 	},
    585 	
    586 	
    587 	checkForUnsafeLocation: Zotero.Promise.coroutine(function* (path) {
    588 		if (this._warnOnUnsafeLocation && Zotero.File.isDropboxDirectory(path)
    589 				&& Zotero.Prefs.get('warnOnUnsafeDataDir')) {
    590 			this._warnOnUnsafeLocation = false;
    591 			let check = {value: false};
    592 			let index = Services.prompt.confirmEx(
    593 				null,
    594 				Zotero.getString('general.warning'),
    595 				Zotero.getString('dataDir.unsafeLocation.existing.dropbox') + "\n\n"
    596 					+ Zotero.getString('dataDir.unsafeLocation.existing.chooseDifferent'),
    597 				Services.prompt.STD_YES_NO_BUTTONS,
    598 				null, null, null,
    599 				Zotero.getString('general.dontShowWarningAgain'),
    600 				check
    601 			);
    602 
    603 			// Yes - display dialog.
    604 			if (index == 0) {
    605 				yield this.choose(true);
    606 			}
    607 			if (check.value) {
    608 				Zotero.Prefs.set('warnOnUnsafeDataDir', false);
    609 			}
    610 		}
    611 	}),
    612 	
    613 	
    614 	isLegacy: function (dir) {
    615 		// 'zotero'
    616 		return OS.Path.basename(dir) == this.legacyDirName
    617 				// '69pmactz.default'
    618 				&& OS.Path.basename(OS.Path.dirname(dir)).match(/^[0-9a-z]{8}\..+/)
    619 				// 'Profiles'
    620 				&& OS.Path.basename(OS.Path.dirname(OS.Path.dirname(dir))) == 'Profiles';
    621 	},
    622 	
    623 	
    624 	isNewDirOnDifferentDrive: Zotero.Promise.coroutine(function* (oldDir, newDir) {
    625 		var filename = 'zotero-migration.tmp';
    626 		var tmpFile = OS.Path.join(Zotero.getTempDirectory().path, filename);
    627 		yield Zotero.File.putContentsAsync(tmpFile, ' ');
    628 		var testPath = OS.Path.normalize(OS.Path.join(newDir, '..', filename));
    629 		try {
    630 			// Attempt moving the marker with noCopy
    631 			yield OS.File.move(tmpFile, testPath, { noCopy: true });
    632 		} catch(e) {
    633 			yield OS.File.remove(tmpFile);
    634 			
    635 			Components.classes["@mozilla.org/net/osfileconstantsservice;1"].
    636 				getService(Components.interfaces.nsIOSFileConstantsService).
    637 				init();	
    638 			if (e instanceof OS.File.Error) {
    639 				if (e.unixErrno != undefined && e.unixErrno == OS.Constants.libc.EXDEV) {
    640 					return true;
    641 				}
    642 				// ERROR_NOT_SAME_DEVICE is undefined
    643 				// e.winLastError == OS.Constants.Win.ERROR_NOT_SAME_DEVICE
    644 				if (e.winLastError != undefined && e.winLastError == 17) {
    645 					return true;
    646 				}
    647 			}
    648 			throw e;
    649 		}
    650 		yield OS.File.remove(testPath);
    651 		return false;
    652 	}),
    653 	
    654 	
    655 	// TODO: Remove after 5.0 upgrades
    656 	checkForLostLegacy: async function () {
    657 		var currentDir = this.dir;
    658 		if (currentDir != this.defaultDir) return;
    659 		if (Zotero.Prefs.get('ignoreLegacyDataDir.auto') || Zotero.Prefs.get('ignoreLegacyDataDir.explicit')) return;
    660 		try {
    661 			let profilesParent = OS.Path.dirname(Zotero.Profile.getOtherAppProfilesDir());
    662 			Zotero.debug("Looking for Firefox profile in " + profilesParent);
    663 			
    664 			// get default profile
    665 			var defProfile;
    666 			try {
    667 				defProfile = await Zotero.Profile.getDefaultInProfilesDir(profilesParent);
    668 			}
    669 			catch (e) {
    670 				Zotero.logError(e);
    671 				return;
    672 			}
    673 			if (!defProfile) {
    674 				return;
    675 			}
    676 			let profileDir = defProfile[0];
    677 			Zotero.debug("Found default profile at " + profileDir);
    678 			
    679 			let dir;
    680 			let mtime;
    681 			try {
    682 				dir = OS.Path.join(profileDir, this.legacyDirName);
    683 				let dbFile = OS.Path.join(dir, this.getDatabaseFilename());
    684 				let info = await OS.File.stat(dbFile);
    685 				if (info.size < 1200000) {
    686 					Zotero.debug(`Legacy database is ${info.size} bytes -- ignoring`);
    687 					Zotero.Prefs.set('ignoreLegacyDataDir.auto', true);
    688 					return;
    689 				}
    690 				mtime = info.lastModificationDate;
    691 				if (mtime < new Date(2017, 6, 1)) {
    692 					Zotero.debug(`Legacy database was last modified on ${mtime.toString()} -- ignoring`);
    693 					Zotero.Prefs.set('ignoreLegacyDataDir.auto', true);
    694 					return;
    695 				}
    696 				Zotero.debug(`Legacy database found at ${dbFile}, last modified ${mtime}`);
    697 			}
    698 			catch (e) {
    699 				Zotero.Prefs.set('ignoreLegacyDataDir.auto', true);
    700 				if (e.becauseNoSuchFile) {
    701 					return;
    702 				}
    703 				throw e;
    704 			}
    705 			
    706 			let ps = Services.prompt;
    707 			let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    708 				+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
    709 				+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING);
    710 			let dontAskAgain = {};
    711 			let index = ps.confirmEx(null,
    712 				"Other Data Directory Found",
    713 				"Zotero found a previous data directory within your Firefox profile, "
    714 					+ `last modified on ${mtime.toLocaleDateString()}. `
    715 					+ "If items or files are missing from Zotero that were present in Zotero for Firefox, "
    716 					+ "your previous data directory may not have been properly migrated to the new default location "
    717 					+ `in ${this.defaultDir}.\n\n`
    718 					+ `Do you wish to continue using the current data directory or switch to the previous one?\n\n`
    719 					+ `If you switch, your current data directory will be moved to ${this.defaultDir + '-Old'}, `
    720 					+ `and the previous directory will be migrated to ${this.defaultDir}.`,
    721 				buttonFlags,
    722 				"Use Current Directory",
    723 				null,
    724 				"Switch to Previous Directory",
    725 				"Don\u0027t ask me again",
    726 				dontAskAgain
    727 			);
    728 			if (index == 1) {
    729 				return;
    730 			}
    731 			if (dontAskAgain.value) {
    732 				Zotero.Prefs.set('ignoreLegacyDataDir.explicit', true);
    733 			}
    734 			if (index == 0) {
    735 				return;
    736 			}
    737 			
    738 			// Switch to previous directory
    739 			this.set(dir);
    740 			// Set a marker to rename the current ~/Zotero directory
    741 			try {
    742 				await Zotero.File.putContentsAsync(OS.Path.join(this.defaultDir, 'move-to-old'), '');
    743 			}
    744 			catch (e) {
    745 				Zotero.logError(e);
    746 			}
    747 			Zotero.Utilities.Internal.quit(true);
    748 		}
    749 		catch (e) {
    750 			Zotero.logError(e);
    751 		}
    752 	},
    753 	
    754 	
    755 	/**
    756 	 * Determine if current data directory is in a legacy location
    757 	 */
    758 	canMigrate: function () {
    759 		// If (not default location) && (not useDataDir or within legacy location)
    760 		var currentDir = this.dir;
    761 		if (currentDir == this.defaultDir) {
    762 			return false;
    763 		}
    764 		
    765 		if (this.newDirOnDifferentDrive) {
    766 			return false;
    767 		}
    768 		
    769 		if (Zotero.forceDataDir) {
    770 			return false;
    771 		}
    772 		
    773 		// Legacy default or set to legacy default from other program (Standalone/Z4Fx) to share data
    774 		if (!Zotero.Prefs.get('useDataDir') || this.isLegacy(currentDir)) {
    775 			return true;
    776 		}
    777 		
    778 		return false;
    779 	},
    780 	
    781 	
    782 	reveal: function () {
    783 		return Zotero.File.reveal(this.dir);
    784 	},
    785 	
    786 	
    787 	markForMigration: function (dir, automatic = false) {
    788 		var path = OS.Path.join(dir, this.MIGRATION_MARKER);
    789 		Zotero.debug("Creating migration marker at " + path);
    790 		return Zotero.File.putContentsAsync(
    791 			path,
    792 			JSON.stringify({
    793 				sourceDir: dir,
    794 				automatic
    795 			})
    796 		);
    797 	},
    798 	
    799 	
    800 	/**
    801 	 * Migrate data directory if necessary and show any errors
    802 	 *
    803 	 * @param {String} dataDir - Current directory
    804 	 * @param {String} targetDir - Target directory, which may be the same; except in tests, this is
    805 	 *     the default data directory
    806 	 */
    807 	checkForMigration: Zotero.Promise.coroutine(function* (dataDir, newDir) {
    808 		if (!this.canMigrate(dataDir)) {
    809 			return false;
    810 		}
    811 		
    812 		let migrationMarker = OS.Path.join(dataDir, this.MIGRATION_MARKER);
    813 		try {
    814 			var exists = yield OS.File.exists(migrationMarker)
    815 		}
    816 		catch (e) {
    817 			Zotero.logError(e);
    818 		}
    819 		let automatic = false;
    820 		if (!exists) {
    821 			automatic = true;
    822 			
    823 			// Skip automatic migration if there's a non-empty directory at the new location
    824 			// TODO: Notify user
    825 			if ((yield OS.File.exists(newDir)) && !(yield Zotero.File.directoryIsEmpty(newDir))) {
    826 				Zotero.debug(`${newDir} exists and is non-empty -- skipping migration`);
    827 				return false;
    828 			}
    829 		}
    830 		
    831 		// Skip migration if new dir on different drive and prompt
    832 		try {
    833 			if (yield this.isNewDirOnDifferentDrive(dataDir, newDir)) {
    834 				Zotero.debug(`New dataDir ${newDir} is on a different drive from ${dataDir} -- skipping migration`);
    835 				Zotero.DataDirectory.newDirOnDifferentDrive = true;
    836 				
    837 				let error = Zotero.getString(`dataDir.migration.failure.full.automatic.newDirOnDifferentDrive`, Zotero.clientName)
    838 					+ "\n\n"
    839 					+ Zotero.getString(`dataDir.migration.failure.full.automatic.text2`, Zotero.appName);
    840 				return this.fullMigrationFailurePrompt(dataDir, newDir, error);
    841 			}
    842 		}
    843 		catch (e) {
    844 			Zotero.logError("Error checking whether data directory is on different drive "
    845 				+ "-- skipping migration:\n\n" + e);
    846 			return false;
    847 		}
    848 		
    849 		// Check for an existing pipe from other running versions of Zotero pointing at the same data
    850 		// directory, and skip migration if found
    851 		try {
    852 			let foundPipe = yield Zotero.IPC.pipeExists();
    853 			if (foundPipe) {
    854 				Zotero.debug("Found existing pipe -- skipping migration");
    855 				
    856 				if (!automatic) {
    857 					let ps = Services.prompt;
    858 					let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    859 						+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
    860 					let index = ps.confirmEx(null,
    861 						Zotero.getString('dataDir.migration.failure.title'),
    862 						Zotero.getString('dataDir.migration.failure.full.firefoxOpen'),
    863 						buttonFlags,
    864 						Zotero.getString('general.tryAgain'),
    865 						Zotero.getString('general.tryLater'),
    866 						null, null, {}
    867 					);
    868 					
    869 					if (index == 0) {
    870 						return this.checkForMigration(newDir, newDir);
    871 					}
    872 				}
    873 				
    874 				return false;
    875 			}
    876 		}
    877 		catch (e) {
    878 			Zotero.logError("Error checking for pipe -- skipping migration:\n\n" + e);
    879 			return false;
    880 		}
    881 		
    882 		// If there are other profiles pointing to the old directory, make sure we can edit the prefs.js
    883 		// file before doing anything, or else we risk orphaning a 4.0 installation
    884 		try {
    885 			let otherProfiles = yield Zotero.Profile.findOtherProfilesUsingDataDirectory(dataDir);
    886 			// 'touch' each prefs.js file to make sure we can access it
    887 			for (let dir of otherProfiles) {
    888 				let prefs = OS.Path.join(dir, "prefs.js");
    889 				yield OS.File.setDates(prefs);
    890 			}
    891 		}
    892 		catch (e) {
    893 			Zotero.logError(e);
    894 			Zotero.logError("Error checking other profiles -- skipping migration");
    895 			// TODO: After 5.0 has been out a while, remove this and let migration continue even if
    896 			// other profile directories can't be altered, with the assumption that they'll be running
    897 			// 5.0 already and will be pick up the new data directory automatically.
    898 			return false;
    899 		}
    900 		
    901 		if (automatic) {
    902 			yield this.markForMigration(dataDir, true);
    903 		}
    904 		
    905 		let sourceDir;
    906 		let oldDir;
    907 		let partial = false;
    908 		
    909 		// Check whether this is an automatic or manual migration
    910 		let contents;
    911 		try {
    912 			contents = yield Zotero.File.getContentsAsync(migrationMarker);
    913 			({ sourceDir, automatic } = JSON.parse(contents));
    914 		}
    915 		catch (e) {
    916 			if (contents !== undefined) {
    917 				Zotero.debug(contents, 1);
    918 			}
    919 			Zotero.logError(e);
    920 			return false;
    921 		}
    922 		
    923 		// Not set to the default directory, so use current as old directory
    924 		if (dataDir != newDir) {
    925 			oldDir = dataDir;
    926 		}
    927 		// Unfinished migration -- already using new directory, so get path to previous
    928 		// directory from the migration marker
    929 		else {
    930 			oldDir = sourceDir;
    931 			partial = true;
    932 		}
    933 		
    934 		// Not yet used
    935 		let progressHandler = function (progress, progressMax) {
    936 			this.updateZoteroPaneProgressMeter(Math.round(progress / progressMax));
    937 		}.bind(this);
    938 		
    939 		let errors;
    940 		let mode = automatic ? 'automatic' : 'manual';
    941 		// This can seemingly fail due to a race condition building the Standalone window,
    942 		// so just ignore it if it does
    943 		try {
    944 			Zotero.showZoteroPaneProgressMeter(
    945 				Zotero.getString("dataDir.migration.inProgress"),
    946 				false,
    947 				null,
    948 				// Don't show message in a popup in Standalone if pane isn't ready
    949 				Zotero.isStandalone
    950 			);
    951 		}
    952 		catch (e) {
    953 			Zotero.logError(e);
    954 		}
    955 		try {
    956 			errors = yield this.migrate(oldDir, newDir, partial, progressHandler);
    957 		}
    958 		catch (e) {
    959 			// Complete failure (failed to create new directory, copy marker, or move database)
    960 			Zotero.debug("Migration failed", 1);
    961 			Zotero.logError(e);
    962 			
    963 			let error = Zotero.getString(`dataDir.migration.failure.full.${mode}.text1`, Zotero.clientName)
    964 				+ "\n\n"
    965 				+ e
    966 				+ "\n\n"
    967 				+ Zotero.getString(`dataDir.migration.failure.full.${mode}.text2`, Zotero.appName);
    968 			yield this.fullMigrationFailurePrompt(oldDir, newDir, error);
    969 			
    970 			// Clear status line from progress meter
    971 			try {
    972 				Zotero.showZoteroPaneProgressMeter("", false, null, Zotero.isStandalone);
    973 			}
    974 			catch (e) {
    975 				Zotero.logError(e);
    976 			}
    977 			return;
    978 		}
    979 		
    980 		// Set data directory again
    981 		Zotero.debug("Using new data directory " + newDir);
    982 		this._cache(newDir);
    983 		// Tell Zotero for Firefox in connector mode to reload and find the new data directory
    984 		if (Zotero.isStandalone) {
    985 			Zotero.IPC.broadcast('reinit');
    986 		}
    987 		
    988 		// At least the database was copied, but other things failed
    989 		if (errors.length) {
    990 			let ps = Services.prompt;
    991 			let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
    992 				+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING)
    993 				+ (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING);
    994 			let index = ps.confirmEx(null,
    995 				Zotero.getString('dataDir.migration.failure.title'),
    996 				Zotero.getString(`dataDir.migration.failure.partial.${mode}.text`,
    997 						[ZOTERO_CONFIG.CLIENT_NAME, Zotero.appName])
    998 					+ "\n\n"
    999 					+ Zotero.getString('dataDir.migration.failure.partial.old', oldDir)
   1000 					+ "\n\n"
   1001 					+ Zotero.getString('dataDir.migration.failure.partial.new', newDir),
   1002 				buttonFlags,
   1003 				Zotero.getString('general.tryAgain'),
   1004 				Zotero.getString('general.tryLater'),
   1005 				Zotero.getString('dataDir.migration.failure.partial.showDirectoriesAndQuit', Zotero.appName),
   1006 				null, {}
   1007 			);
   1008 			
   1009 			if (index == 0) {
   1010 				return this.checkForMigration(newDir, newDir);
   1011 			}
   1012 			// Focus the first file/folder in the old directory
   1013 			else if (index == 2) {
   1014 				try {
   1015 					let it = new OS.File.DirectoryIterator(oldDir);
   1016 					let entry;
   1017 					try {
   1018 						entry = yield it.next();
   1019 					}
   1020 					catch (e) {
   1021 						if (e != StopIteration) {
   1022 							throw e;
   1023 						}
   1024 					}
   1025 					finally {
   1026 						it.close();
   1027 					}
   1028 					if (entry) {
   1029 						yield Zotero.File.reveal(entry.path);
   1030 					}
   1031 					// Focus the database file in the new directory
   1032 					yield Zotero.File.reveal(OS.Path.join(newDir, this.getDatabaseFilename()));
   1033 				}
   1034 				catch (e) {
   1035 					Zotero.logError(e);
   1036 				}
   1037 				
   1038 				Zotero.skipLoading = true;
   1039 				Zotero.Utilities.Internal.quitZotero();
   1040 				return;
   1041 			}
   1042 		}
   1043 	}),
   1044 	
   1045 	
   1046 	fullMigrationFailurePrompt: Zotero.Promise.coroutine(function* (oldDir, newDir, error) {
   1047 		let ps = Services.prompt;
   1048 		let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
   1049 			+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
   1050 		let index = ps.confirmEx(null,
   1051 			Zotero.getString('dataDir.migration.failure.title'),
   1052 			error + "\n\n"
   1053 				+ Zotero.getString('dataDir.migration.failure.full.current', oldDir)
   1054 				+ "\n\n"
   1055 				+ Zotero.getString('dataDir.migration.failure.full.recommended', newDir),
   1056 			buttonFlags,
   1057 			Zotero.getString('dataDir.migration.failure.full.showCurrentDirectoryAndQuit', Zotero.appName),
   1058 			Zotero.getString('general.notNow'),
   1059 			null, null, {}
   1060 		);
   1061 		if (index == 0) {
   1062 			yield Zotero.File.reveal(oldDir);
   1063 			Zotero.skipLoading = true;
   1064 			Zotero.Utilities.Internal.quitZotero();
   1065 		}
   1066 	}),
   1067 	
   1068 	
   1069 	/**
   1070 	 * Recursively moves data directory from one location to another and updates the data directory
   1071 	 * setting in this profile and any profiles pointing to the old location
   1072 	 *
   1073 	 * If moving the database file fails, an error is thrown.
   1074 	 * Otherwise, an array of errors is returned.
   1075 	 *
   1076 	 * @param {String} oldDir
   1077 	 * @param {String} newDir
   1078 	 * @return {Error[]}
   1079 	 */
   1080 	migrate: Zotero.Promise.coroutine(function* (oldDir, newDir, partial) {
   1081 		var dbName = this.getDatabaseFilename();
   1082 		var errors = [];
   1083 		
   1084 		function addError(e) {
   1085 			errors.push(e);
   1086 			Zotero.logError(e);
   1087 		}
   1088 		
   1089 		if (!(yield OS.File.exists(oldDir))) {
   1090 			Zotero.debug(`Old directory ${oldDir} doesn't exist -- nothing to migrate`);
   1091 			try {
   1092 				let newMigrationMarker = OS.Path.join(newDir, this.MIGRATION_MARKER);
   1093 				Zotero.debug("Removing " + newMigrationMarker);
   1094 				yield OS.File.remove(newMigrationMarker);
   1095 			}
   1096 			catch (e) {
   1097 				Zotero.logError(e);
   1098 			}
   1099 			return [];
   1100 		}
   1101 		
   1102 		if (partial) {
   1103 			Zotero.debug(`Continuing data directory migration from ${oldDir} to ${newDir}`);
   1104 		}
   1105 		else {
   1106 			Zotero.debug(`Migrating data directory from ${oldDir} to ${newDir}`);
   1107 		}
   1108 		
   1109 		// Create the new directory
   1110 		if (!partial) {
   1111 			yield OS.File.makeDir(
   1112 				newDir,
   1113 				{
   1114 					ignoreExisting: false,
   1115 					unixMode: 0o755
   1116 				}
   1117 			);
   1118 		}
   1119 		
   1120 		// Copy marker
   1121 		let oldMarkerFile = OS.Path.join(oldDir, this.MIGRATION_MARKER);
   1122 		// Marker won't exist on subsequent attempts after partial failure
   1123 		if (yield OS.File.exists(oldMarkerFile)) {
   1124 			yield OS.File.copy(oldMarkerFile, OS.Path.join(newDir, this.MIGRATION_MARKER));
   1125 		}
   1126 		
   1127 		// Update the data directory setting first so that a failure immediately after the move won't
   1128 		// leave the database stranded
   1129 		this.set(newDir);
   1130 		
   1131 		// Move database
   1132 		if (!partial) {
   1133 			Zotero.debug("Moving " + dbName);
   1134 			try {
   1135 				yield OS.File.move(OS.Path.join(oldDir, dbName), OS.Path.join(newDir, dbName));
   1136 			}
   1137 			// If moving the database failed, revert to the old data directory and clear marker files
   1138 			catch (e) {
   1139 				if (this.isLegacy(oldDir)) {
   1140 					Zotero.Prefs.clear('dataDir');
   1141 					Zotero.Prefs.clear('useDataDir');
   1142 				}
   1143 				else {
   1144 					this.set(oldDir);
   1145 				}
   1146 				try {
   1147 					yield OS.File.remove(oldMarkerFile, { ignoreAbsent: true });
   1148 				}
   1149 				catch (e) {
   1150 					Zotero.logError(e);
   1151 				}
   1152 				try {
   1153 					yield OS.File.remove(OS.Path.join(newDir, this.MIGRATION_MARKER));
   1154 					yield OS.File.removeEmptyDir(newDir);
   1155 				}
   1156 				catch (e) {
   1157 					Zotero.logError(e);
   1158 				}
   1159 				throw e;
   1160 			}
   1161 		}
   1162 		
   1163 		// Once the database has been moved, we can clear the migration marker from the old directory.
   1164 		// If the migration is interrupted after this, it can be continued later based on the migration
   1165 		// marker in the new directory.
   1166 		try {
   1167 			yield OS.File.remove(OS.Path.join(oldDir, this.MIGRATION_MARKER));
   1168 		}
   1169 		catch (e) {
   1170 			addError(e);
   1171 		}
   1172 		
   1173 		errors = errors.concat(yield Zotero.File.moveDirectory(
   1174 			oldDir,
   1175 			newDir,
   1176 			{
   1177 				allowExistingTarget: true,
   1178 				// Don't overwrite root files (except for hidden files like .DS_Store)
   1179 				noOverwrite: path => {
   1180 					return OS.Path.dirname(path) == oldDir && !OS.Path.basename(path).startsWith('.')
   1181 				},
   1182 			}
   1183 		));
   1184 		
   1185 		if (errors.length) {
   1186 			Zotero.logError("Not all files were transferred from " + oldDir + " to " + newDir);
   1187 		}
   1188 		else {
   1189 			try {
   1190 				let newMigrationMarker = OS.Path.join(newDir, this.MIGRATION_MARKER);
   1191 				Zotero.debug("Removing " + newMigrationMarker);
   1192 				yield OS.File.remove(newMigrationMarker);
   1193 				
   1194 				Zotero.debug("Migration successful");
   1195 			}
   1196 			catch (e) {
   1197 				addError(e);
   1198 			}
   1199 		}
   1200 		
   1201 		// Update setting in other profiles that point to this data directory
   1202 		try {
   1203 			let otherProfiles = yield Zotero.Profile.findOtherProfilesUsingDataDirectory(oldDir);
   1204 			for (let dir of otherProfiles) {
   1205 				try {
   1206 					yield Zotero.Profile.updateProfileDataDirectory(dir, oldDir, newDir);
   1207 				}
   1208 				catch (e) {
   1209 					Zotero.logError("Error updating " + OS.Path.join(dir.path, "prefs.js"));
   1210 					Zotero.logError(e);
   1211 				}
   1212 			}
   1213 		}
   1214 		catch (e) {
   1215 			Zotero.logError("Error updating other profiles to point to new location");
   1216 		}
   1217 		
   1218 		return errors;
   1219 	}),
   1220 	
   1221 	
   1222 	getDatabaseFilename: function (name) {
   1223 		return (name || ZOTERO_CONFIG.ID) + '.sqlite';
   1224 	},
   1225 	
   1226 	getDatabase: function (name, ext) {
   1227 		name = this.getDatabaseFilename(name);
   1228 		ext = ext ? '.' + ext : '';
   1229 		
   1230 		return OS.Path.join(this.dir, name + ext);
   1231 	}
   1232 };