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 };