zotero/chrome/content/zotero/xpcom/storage.js
2015-07-18 07:09:53 -04:00

1951 lines
56 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.Sync.Storage = new function () {
//
// Constants
//
this.SYNC_STATE_TO_UPLOAD = 0;
this.SYNC_STATE_TO_DOWNLOAD = 1;
this.SYNC_STATE_IN_SYNC = 2;
this.SYNC_STATE_FORCE_UPLOAD = 3;
this.SYNC_STATE_FORCE_DOWNLOAD = 4;
this.SUCCESS = 1;
this.ERROR_NO_URL = -1;
this.ERROR_NO_USERNAME = -2;
this.ERROR_NO_PASSWORD = -3;
this.ERROR_OFFLINE = -4;
this.ERROR_UNREACHABLE = -5;
this.ERROR_SERVER_ERROR = -6;
this.ERROR_NOT_DAV = -7;
this.ERROR_BAD_REQUEST = -8;
this.ERROR_AUTH_FAILED = -9;
this.ERROR_FORBIDDEN = -10;
this.ERROR_PARENT_DIR_NOT_FOUND = -11;
this.ERROR_ZOTERO_DIR_NOT_FOUND = -12;
this.ERROR_ZOTERO_DIR_NOT_WRITABLE = -13;
this.ERROR_NOT_ALLOWED = -14;
this.ERROR_UNKNOWN = -15;
this.ERROR_FILE_MISSING_AFTER_UPLOAD = -16;
this.ERROR_NONEXISTENT_FILE_NOT_MISSING = -17;
// TEMP
this.__defineGetter__("defaultError", function () Zotero.getString('sync.storage.error.default', Zotero.appName));
this.__defineGetter__("defaultErrorRestart", function () Zotero.getString('sync.storage.error.defaultRestart', Zotero.appName));
//
// Public properties
//
this.__defineGetter__("syncInProgress", function () _syncInProgress);
this.__defineGetter__("updatesInProgress", function () _updatesInProgress);
this.compressionTracker = {
compressed: 0,
uncompressed: 0,
get ratio() {
return (Zotero.Sync.Storage.compressionTracker.uncompressed -
Zotero.Sync.Storage.compressionTracker.compressed) /
Zotero.Sync.Storage.compressionTracker.uncompressed;
}
}
Zotero.Notifier.registerObserver(this, ['file']);
//
// Private properties
//
var _maxCheckAgeInSeconds = 10800; // maximum age for upload modification check (3 hours)
var _syncInProgress;
var _updatesInProgress;
var _itemDownloadPercentages = {};
var _uploadCheckFiles = [];
var _lastFullFileCheck = {};
this.sync = function (options) {
if (options.libraries) {
Zotero.debug("Starting file sync for libraries " + options.libraries);
}
else {
Zotero.debug("Starting file sync");
}
var self = this;
var libraryModes = {};
var librarySyncTimes = {};
// Get personal library file sync mode
return Zotero.Promise.try(function () {
// TODO: Make sure modes are active
if (options.libraries && options.libraries.indexOf(0) == -1) {
return;
}
if (Zotero.Sync.Storage.ZFS.includeUserFiles) {
libraryModes[0] = Zotero.Sync.Storage.ZFS;
}
else if (Zotero.Sync.Storage.WebDAV.includeUserFiles) {
libraryModes[0] = Zotero.Sync.Storage.WebDAV;
}
})
.then(function () {
// Get group library file sync modes
if (Zotero.Sync.Storage.ZFS.includeGroupFiles) {
var groups = Zotero.Groups.getAll();
for each(var group in groups) {
if (options.libraries && options.libraries.indexOf(group.libraryID) == -1) {
continue;
}
// TODO: if library file syncing enabled
libraryModes[group.libraryID] = Zotero.Sync.Storage.ZFS;
}
}
// Cache auth credentials for each mode
var modes = [];
var promises = [];
for each(var mode in libraryModes) {
if (modes.indexOf(mode) == -1) {
modes.push(mode);
// Try to verify WebDAV server first if it hasn't been
if (mode == Zotero.Sync.Storage.WebDAV
&& !Zotero.Sync.Storage.WebDAV.verified) {
Zotero.debug("WebDAV file sync is not active");
var promise = Zotero.Sync.Storage.checkServerPromise(Zotero.Sync.Storage.WebDAV)
.then(function () {
return mode.cacheCredentials();
});
}
else {
var promise = mode.cacheCredentials();
}
promises.push(Zotero.Promise.allSettled([mode, promise]));
}
}
return Zotero.Promise.all(promises)
// Get library last-sync times
.then(function (cacheCredentialsPromises) {
var promises = [];
// Mark WebDAV verification failure as user library error.
// We ignore credentials-caching errors for ZFS and let the
// later requests fail.
cacheCredentialsPromises.forEach(function (results) {
let mode = results[0].value;
if (mode == Zotero.Sync.Storage.WebDAV) {
if (results[1].state == "rejected") {
promises.push(Zotero.Promise.allSettled(
[0, Zotero.Promise.reject(results[1].reason)]
));
// Skip further syncing of user library
delete libraryModes[0];
}
}
});
for (var libraryID in libraryModes) {
libraryID = parseInt(libraryID);
// Get the last sync time for each library
if (self.downloadOnSync(libraryID)) {
promises.push(Zotero.Promise.allSettled(
[libraryID, libraryModes[libraryID].getLastSyncTime(libraryID)]
));
}
// If download-as-needed, we don't need the last sync time
else {
promises.push(Zotero.Promise.allSettled([libraryID, null]));
}
}
return Zotero.Promise.all(promises);
});
})
.then(function (promises) {
if (!promises.length) {
Zotero.debug("No libraries are active for file sync");
return [];
}
var libraryQueues = [];
// Get the libraries we have sync times for
promises.forEach(function (results) {
let libraryID = results[0].value;
let lastSyncTime = results[1].value;
if (results[1].state == "fulfilled") {
librarySyncTimes[libraryID] = lastSyncTime;
}
else {
Zotero.debug(lastSyncTime.reason);
Components.utils.reportError(lastSyncTime.reason);
// Pass rejected promise through
libraryQueues.push(results);
}
});
// Check for updated files to upload in each library
var promises = [];
for (let libraryID in librarySyncTimes) {
let promise;
libraryID = parseInt(libraryID);
if (!Zotero.Libraries.isFilesEditable(libraryID)) {
Zotero.debug("No file editing access -- skipping file "
+ "modification check for library " + libraryID);
continue;
}
// If this is a background sync, it's not the first sync of
// the session, the library has had at least one full check
// this session, and it's been less than _maxCheckAgeInSeconds
// since the last full check of this library, check only files
// that were previously modified or opened recently
else if (options.background
&& !options.firstInSession
&& _lastFullFileCheck[libraryID]
&& (_lastFullFileCheck[libraryID] + (_maxCheckAgeInSeconds * 1000))
> new Date().getTime()) {
let itemIDs = _getFilesToCheck(libraryID);
promise = self.checkForUpdatedFiles(libraryID, itemIDs);
}
// Otherwise check all files in the library
else {
_lastFullFileCheck[libraryID] = new Date().getTime();
promise = self.checkForUpdatedFiles(libraryID);
}
promises.push(promise);
}
return Zotero.Promise.all(promises)
.then(function () {
// Queue files to download and upload from each library
for (let libraryID in librarySyncTimes) {
libraryID = parseInt(libraryID);
var downloadAll = self.downloadOnSync(libraryID);
// Forced downloads happen even in on-demand mode
var sql = "SELECT COUNT(*) FROM items "
+ "JOIN itemAttachments USING (itemID) "
+ "WHERE libraryID=? AND syncState=?";
var downloadForced = !!Zotero.DB.valueQuery(
sql,
[libraryID, Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD]
);
// If we don't have any forced downloads, we can skip
// downloads if the last sync time hasn't changed
// or doesn't exist on the server (meaning there are no files)
if (downloadAll && !downloadForced) {
let lastSyncTime = librarySyncTimes[libraryID];
if (lastSyncTime) {
var version = self.getStoredLastSyncTime(
libraryModes[libraryID], libraryID
);
if (version == lastSyncTime) {
Zotero.debug("Last " + libraryModes[libraryID].name
+ " sync id hasn't changed for library "
+ libraryID + " -- skipping file downloads");
downloadAll = false;
}
}
else {
Zotero.debug("No last " + libraryModes[libraryID].name
+ " sync time for library " + libraryID
+ " -- skipping file downloads");
downloadAll = false;
}
}
if (downloadAll || downloadForced) {
for each(var itemID in _getFilesToDownload(libraryID, !downloadAll)) {
var item = Zotero.Items.get(itemID);
self.queueItem(item);
}
}
// Get files to upload
if (Zotero.Libraries.isFilesEditable(libraryID)) {
for each(var itemID in _getFilesToUpload(libraryID)) {
var item = Zotero.Items.get(itemID);
self.queueItem(item);
}
}
else {
Zotero.debug("No file editing access -- skipping file uploads for library " + libraryID);
}
}
// Start queues for each library
for (let libraryID in librarySyncTimes) {
libraryID = parseInt(libraryID);
libraryQueues.push(Zotero.Promise.allSettled(
[libraryID, Zotero.Sync.Storage.QueueManager.start(libraryID)]
));
}
// The promise is done when all libraries are done
return Zotero.Promise.all(libraryQueues);
});
})
.then(function (promises) {
Zotero.debug('Queue manager is finished');
var changedLibraries = [];
var finalPromises = [];
promises.forEach(function (results) {
var libraryID = results[0].value;
var libraryQueues = results[1].value;
if (results[1].state == "fulfilled") {
libraryQueues.forEach(function (queuePromise) {
if (queueZotero.Promise.isFulfilled()) {
let result = queueZotero.Promise.value();
Zotero.debug("File " + result.type + " sync finished "
+ "for library " + libraryID);
if (result.localChanges) {
changedLibraries.push(libraryID);
}
finalPromises.push(Zotero.Promise.allSettled([
libraryID,
libraryModes[libraryID].setLastSyncTime(
libraryID,
result.remoteChanges ? false : librarySyncTimes[libraryID]
)
]));
}
else {
let e = queueZotero.Promise.reason();
Zotero.debug("File " + e.type + " sync failed "
+ "for library " + libraryID);
finalPromises.push(Zotero.Promise.allSettled(
[libraryID, Zotero.Promise.reject(e)]
));
}
});
}
else {
Zotero.debug("File sync failed for library " + libraryID);
finalPromises.push([libraryID, libraryQueues]);
}
// If WebDAV sync enabled, purge deleted and orphaned files
if (libraryID == Zotero.Libraries.userLibraryID
&& Zotero.Sync.Storage.WebDAV.includeUserFiles) {
Zotero.Sync.Storage.WebDAV.purgeDeletedStorageFiles()
.then(function () {
return Zotero.Sync.Storage.WebDAV.purgeOrphanedStorageFiles();
})
.catch(function (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
});
}
});
Zotero.Sync.Storage.ZFS.purgeDeletedStorageFiles()
.catch(function (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
});
if (promises.length && !changedLibraries.length) {
Zotero.debug("No local changes made during file sync");
}
return Zotero.Promise.all(finalPromises)
.then(function (promises) {
var results = {
changesMade: !!changedLibraries.length,
errors: []
};
promises.forEach(function (promiseResults) {
var libraryID = promiseResults[0].value;
if (promiseResults[1].state == "rejected") {
let e = promiseResults[1].reason;
if (typeof e == 'string') {
e = new Error(e);
}
e.libraryID = libraryID;
results.errors.push(e);
}
});
return results;
});
});
}
//
// Public methods
//
this.queueItem = function (item, highPriority) {
var library = item.libraryID;
if (libraryID) {
var mode = Zotero.Sync.Storage.ZFS;
}
else {
var mode = Zotero.Sync.Storage.ZFS.includeUserFiles
? Zotero.Sync.Storage.ZFS : Zotero.Sync.Storage.WebDAV;
}
switch (Zotero.Sync.Storage.getSyncState(item.id)) {
case this.SYNC_STATE_TO_DOWNLOAD:
case this.SYNC_STATE_FORCE_DOWNLOAD:
var queue = 'download';
var callbacks = {
onStart: function (request) {
return mode.downloadFile(request);
}
};
break;
case this.SYNC_STATE_TO_UPLOAD:
case this.SYNC_STATE_FORCE_UPLOAD:
var queue = 'upload';
var callbacks = {
onStart: function (request) {
return mode.uploadFile(request);
}
};
break;
case false:
Zotero.debug("Sync state for item " + item.id + " not found", 2);
return;
}
var queue = Zotero.Sync.Storage.QueueManager.get(queue, library);
var request = new Zotero.Sync.Storage.Request(
item.libraryID + '/' + item.key, callbacks
);
if (queue.type == 'upload') {
try {
request.setMaxSize(Zotero.Attachments.getTotalFileSize(item));
}
// If this fails, ignore it, though we might fail later
catch (e) {
// But if the file doesn't exist yet, don't try to upload it
//
// This isn't a perfect test, because the file could still be
// in the process of being downloaded. It'd be better to
// download files to a temp directory and move them into place.
if (!item.getFile()) {
Zotero.debug("File " + item.libraryKey + " not yet available to upload -- skipping");
return;
}
Components.utils.reportError(e);
Zotero.debug(e, 1);
}
}
queue.addRequest(request, highPriority);
};
this.getStoredLastSyncTime = function (mode, libraryID) {
var sql = "SELECT version FROM version WHERE schema=?";
return Zotero.DB.valueQuery(
sql, "storage_" + mode.name.toLowerCase() + "_" + libraryID
);
};
this.setStoredLastSyncTime = function (mode, libraryID, time) {
var sql = "REPLACE INTO version SET version=? WHERE schema=?";
Zotero.DB.query(
sql,
[
time,
"storage_" + mode.name.toLowerCase() + "_" + libraryID
]
);
};
/**
* @param {Integer} itemID
*/
this.getSyncState = function (itemID) {
var sql = "SELECT syncState FROM itemAttachments WHERE itemID=?";
return Zotero.DB.valueQuery(sql, itemID);
}
/**
* @param {Integer} itemID
* @param {Integer} syncState Constant from Zotero.Sync.Storage
*/
this.setSyncState = function (itemID, syncState) {
switch (syncState) {
case this.SYNC_STATE_TO_UPLOAD:
case this.SYNC_STATE_TO_DOWNLOAD:
case this.SYNC_STATE_IN_SYNC:
case this.SYNC_STATE_FORCE_UPLOAD:
case this.SYNC_STATE_FORCE_DOWNLOAD:
break;
default:
throw "Invalid sync state '" + syncState
+ "' in Zotero.Sync.Storage.setSyncState()"
}
var sql = "UPDATE itemAttachments SET syncState=? WHERE itemID=?";
return Zotero.DB.valueQuery(sql, [syncState, itemID]);
}
/**
* @param {Integer} itemID
* @return {Integer|NULL} Mod time as timestamp in ms,
* or NULL if never synced
*/
this.getSyncedModificationTime = function (itemID) {
var sql = "SELECT storageModTime FROM itemAttachments WHERE itemID=?";
var mtime = Zotero.DB.valueQuery(sql, itemID);
if (mtime === false) {
throw "Item " + itemID + " not found in "
+ "Zotero.Sync.Storage.getSyncedModificationTime()";
}
return mtime;
}
/**
* @param {Integer} itemID
* @param {Integer} mtime File modification time as
* timestamp in ms
* @param {Boolean} [updateItem=FALSE] Update dateModified field of
* attachment item
*/
this.setSyncedModificationTime = function (itemID, mtime, updateItem) {
if (mtime < 0) {
Components.utils.reportError("Invalid file mod time " + mtime
+ " in Zotero.Storage.setSyncedModificationTime()");
mtime = 0;
}
Zotero.DB.beginTransaction();
var sql = "UPDATE itemAttachments SET storageModTime=? WHERE itemID=?";
Zotero.DB.valueQuery(sql, [mtime, itemID]);
if (updateItem) {
// Update item date modified so the new mod time will be synced
var sql = "UPDATE items SET clientDateModified=? WHERE itemID=?";
Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, itemID]);
}
Zotero.DB.commitTransaction();
}
/**
* @param {Integer} itemID
* @return {String|NULL} File hash, or NULL if never synced
*/
this.getSyncedHash = function (itemID) {
var sql = "SELECT storageHash FROM itemAttachments WHERE itemID=?";
var hash = Zotero.DB.valueQuery(sql, itemID);
if (hash === false) {
throw "Item " + itemID + " not found in "
+ "Zotero.Sync.Storage.getSyncedHash()";
}
return hash;
}
/**
* @param {Integer} itemID
* @param {String} hash File hash
* @param {Boolean} [updateItem=FALSE] Update dateModified field of
* attachment item
*/
this.setSyncedHash = function (itemID, hash, updateItem) {
if (hash !== null && hash.length != 32) {
throw ("Invalid file hash '" + hash + "' in Zotero.Storage.setSyncedHash()");
}
Zotero.DB.beginTransaction();
var sql = "UPDATE itemAttachments SET storageHash=? WHERE itemID=?";
Zotero.DB.valueQuery(sql, [hash, itemID]);
if (updateItem) {
// Update item date modified so the new mod time will be synced
var sql = "UPDATE items SET clientDateModified=? WHERE itemID=?";
Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, itemID]);
}
Zotero.DB.commitTransaction();
}
/**
* Check if modification time of file on disk matches the mod time
* in the database
*
* @param {Integer} itemID
* @return {Boolean}
*/
this.isFileModified = function (itemID) {
var item = Zotero.Items.get(itemID);
var file = item.getFile();
if (!file) {
return false;
}
var fileModTime = item.attachmentModificationTime;
if (!fileModTime) {
return false;
}
var syncModTime = Zotero.Sync.Storage.getSyncedModificationTime(itemID);
if (fileModTime != syncModTime) {
var syncHash = Zotero.Sync.Storage.getSyncedHash(itemID);
if (syncHash) {
var fileHash = item.attachmentHash;
if (fileHash && fileHash == syncHash) {
Zotero.debug("Mod time didn't match (" + fileModTime + "!=" + syncModTime + ") "
+ "but hash did for " + file.leafName + " -- ignoring");
return false;
}
}
return true;
}
return false;
}
/**
* @param {Integer|'groups'} [libraryID]
*/
this.downloadAsNeeded = function (libraryID) {
// Personal library
if (!libraryID) {
return Zotero.Prefs.get('sync.storage.downloadMode.personal') == 'on-demand';
}
// Group library (groupID or 'groups')
else {
return Zotero.Prefs.get('sync.storage.downloadMode.groups') == 'on-demand';
}
}
/**
* @param {Integer|'groups'} [libraryID]
*/
this.downloadOnSync = function (libraryID) {
// Personal library
if (!libraryID) {
return Zotero.Prefs.get('sync.storage.downloadMode.personal') == 'on-sync';
}
// Group library (groupID or 'groups')
else {
return Zotero.Prefs.get('sync.storage.downloadMode.groups') == 'on-sync';
}
}
/**
* Scans local files and marks any that have changed for uploading
* and any that are missing for downloading
*
* @param {Integer} [libraryID]
* @param {Integer[]} [itemIDs]
* @param {Object} [itemModTimes] Item mod times indexed by item ids;
* items with stored mod times
* that differ from the provided
* time but file mod times
* matching the stored time will
* be marked for download
* @return {Promise} Promise resolving to TRUE if any items changed state,
* FALSE otherwise
*/
this.checkForUpdatedFiles = function (libraryID, itemIDs, itemModTimes) {
return Zotero.Promise.try(function () {
libraryID = parseInt(libraryID);
if (isNaN(libraryID)) {
libraryID = false;
}
var msg = "Checking for locally changed attachment files";
var memmgr = Components.classes["@mozilla.org/memory-reporter-manager;1"]
.getService(Components.interfaces.nsIMemoryReporterManager);
memmgr.init();
//Zotero.debug("Memory usage: " + memmgr.resident);
if (libraryID !== false) {
if (itemIDs) {
if (!itemIDs.length) {
var msg = "No files to check for local changes in library " + libraryID;
Zotero.debug(msg);
return false;
}
}
if (itemModTimes) {
throw new Error("itemModTimes is not allowed when libraryID is set");
}
msg += " in library " + libraryID;
}
else if (itemIDs) {
throw new Error("libraryID not provided");
}
else if (itemModTimes) {
if (!Object.keys(itemModTimes).length) {
return false;
}
msg += " in download-marking mode";
}
else {
throw new Error("libraryID, itemIDs, or itemModTimes must be provided");
}
Zotero.debug(msg);
var changed = false;
if (!itemIDs) {
itemIDs = Object.keys(itemModTimes ? itemModTimes : {});
}
// Can only handle a certain number of bound parameters at a time
var numIDs = itemIDs.length;
var maxIDs = Zotero.DB.MAX_BOUND_PARAMETERS - 10;
var done = 0;
var rows = [];
Zotero.DB.beginTransaction();
do {
var chunk = itemIDs.splice(0, maxIDs);
var sql = "SELECT itemID, linkMode, path, storageModTime, storageHash, syncState "
+ "FROM itemAttachments JOIN items USING (itemID) "
+ "WHERE linkMode IN (?,?) AND syncState IN (?,?)";
var params = [];
params.push(
Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
Zotero.Attachments.LINK_MODE_IMPORTED_URL,
Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD,
Zotero.Sync.Storage.SYNC_STATE_IN_SYNC
);
if (libraryID !== false) {
sql += " AND libraryID=?";
params.push(libraryID);
}
if (chunk.length) {
sql += " AND itemID IN (" + chunk.map(function () '?').join() + ")";
params = params.concat(chunk);
}
var chunkRows = Zotero.DB.query(sql, params);
if (chunkRows) {
rows = rows.concat(chunkRows);
}
done += chunk.length;
}
while (done < numIDs);
Zotero.DB.commitTransaction();
// If no files, or everything is already marked for download,
// we don't need to do anything
if (!rows.length) {
var msg = "No in-sync or to-upload files found";
if (libraryID !== false) {
msg += " in library " + libraryID;
}
Zotero.debug(msg);
return false;
}
// Index attachment data by item id
itemIDs = [];
var attachmentData = {};
for each(let row in rows) {
var id = row.itemID;
itemIDs.push(id);
attachmentData[id] = {
linkMode: row.linkMode,
path: row.path,
mtime: row.storageModTime,
hash: row.storageHash,
state: row.syncState
};
}
rows = null;
var t = new Date();
var items = Zotero.Items.get(itemIDs);
var numItems = items.length;
var updatedStates = {};
let checkItems = function () {
if (!items.length) return Zotero.Promise.resolve();
//Zotero.debug("Memory usage: " + memmgr.resident);
let item = items.shift();
let row = attachmentData[item.id];
let lk = item.libraryKey;
Zotero.debug("Checking attachment file for item " + lk);
let nsIFile = item.getFile(row, true);
if (!nsIFile) {
Zotero.debug("Marking pathless attachment " + lk + " as in-sync");
updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_IN_SYNC;
return checkItems();
}
let file = null;
return Zotero.Promise.resolve(OS.File.open(nsIFile.path))
.then(function (promisedFile) {
file = promisedFile;
return file.stat()
.then(function (info) {
//Zotero.debug("Memory usage: " + memmgr.resident);
var fmtime = info.lastModificationDate.getTime();
//Zotero.debug("File modification time for item " + lk + " is " + fmtime);
if (fmtime < 1) {
Zotero.debug("File mod time " + fmtime + " is less than 1 -- interpreting as 1", 2);
fmtime = 1;
}
// If file is already marked for upload, skip check. Even if this
// is download-marking mode (itemModTimes) and the file was
// changed remotely, conflicts are checked at upload time, so we
// don't need to worry about it here.
if (row.state == Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD) {
return;
}
//Zotero.debug("Stored mtime is " + row.mtime);
//Zotero.debug("File mtime is " + fmtime);
// Download-marking mode
if (itemModTimes) {
Zotero.debug("Remote mod time for item " + lk + " is " + itemModTimes[item.id]);
// Ignore attachments whose stored mod times haven't changed
if (row.storageModTime == itemModTimes[item.id]) {
Zotero.debug("Storage mod time (" + row.storageModTime + ") "
+ "hasn't changed for item " + lk);
return;
}
Zotero.debug("Marking attachment " + lk + " for download "
+ "(stored mtime: " + itemModTimes[item.id] + ")");
updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD;
}
var mtime = row.mtime;
// If stored time matches file, it hasn't changed locally
if (mtime == fmtime) {
return;
}
// Allow floored timestamps for filesystems that don't support
// millisecond precision (e.g., HFS+)
if (Math.floor(mtime / 1000) * 1000 == fmtime || Math.floor(fmtime / 1000) * 1000 == mtime) {
Zotero.debug("File mod times are within one-second precision "
+ "(" + fmtime + " ≅ " + mtime + ") for " + file.leafName
+ " for item " + lk + " -- ignoring");
return;
}
// Allow timestamp to be exactly one hour off to get around
// time zone issues -- there may be a proper way to fix this
if (Math.abs(fmtime - mtime) == 3600000
// And check with one-second precision as well
|| Math.abs(fmtime - Math.floor(mtime / 1000) * 1000) == 3600000
|| Math.abs(Math.floor(fmtime / 1000) * 1000 - mtime) == 3600000) {
Zotero.debug("File mod time (" + fmtime + ") is exactly one "
+ "hour off remote file (" + mtime + ") for item " + lk
+ "-- assuming time zone issue and skipping upload");
return;
}
// If file hash matches stored hash, only the mod time changed, so skip
return Zotero.Utilities.Internal.md5Async(file)
.then(function (fileHash) {
if (row.hash && row.hash == fileHash) {
// We have to close the file before modifying it from the main
// thread (at least on Windows, where assigning lastModifiedTime
// throws an NS_ERROR_FILE_IS_LOCKED otherwise)
return Zotero.Promise.resolve(file.close())
.then(function () {
Zotero.debug("Mod time didn't match (" + fmtime + "!=" + mtime + ") "
+ "but hash did for " + nsIFile.leafName + " for item " + lk
+ " -- updating file mod time");
try {
nsIFile.lastModifiedTime = row.mtime;
}
catch (e) {
Zotero.File.checkFileAccessError(e, nsIFile, 'update');
}
});
}
// Mark file for upload
Zotero.debug("Marking attachment " + lk + " as changed "
+ "(" + mtime + " != " + fmtime + ")");
updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD;
});
});
})
.finally(function () {
if (file) {
//Zotero.debug("Closing file for item " + lk);
file.close();
}
})
.catch(function (e) {
if (e instanceof OS.File.Error &&
(e.becauseNoSuchFile
// This can happen if a path is too long on Windows,
// e.g. a file is being accessed on a VM through a share
// (and probably in other cases).
|| (e.winLastError && e.winLastError == 3)
// Handle long filenames on OS X/Linux
|| (e.unixErrno && e.unixErrno == 63))) {
Zotero.debug("Marking attachment " + lk + " as missing");
updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
return;
}
if (e instanceof OS.File.Error) {
if (e.becauseClosed) {
Zotero.debug("File was closed", 2);
}
Zotero.debug(e);
Zotero.debug(e.toString());
throw new Error("Error for operation '" + e.operation + "' for " + nsIFile.path);
}
throw e;
})
.then(function () {
return checkItems();
});
};
return checkItems()
.then(function () {
for (let itemID in updatedStates) {
Zotero.Sync.Storage.setSyncState(itemID, updatedStates[itemID]);
changed = true;
}
if (!changed) {
Zotero.debug("No synced files have changed locally");
}
let msg = "Checked " + numItems + " files in ";
if (libraryID !== false) {
msg += "library " + libraryID + " in ";
}
msg += (new Date() - t) + "ms";
Zotero.debug(msg);
return changed;
});
});
};
/**
* Download a single file
*
* If no queue is active, start one. Otherwise, add to existing queue.
*/
this.downloadFile = function (item, requestCallbacks) {
var itemID = item.id;
var mode = getModeFromLibrary(item.libraryID);
// TODO: verify WebDAV on-demand?
if (!mode || !mode.verified) {
Zotero.debug("File syncing is not active for item's library -- skipping download");
return false;
}
if (!item.isImportedAttachment()) {
throw new Error("Not an imported attachment");
}
if (item.getFile()) {
Zotero.debug("File already exists -- replacing");
}
// TODO: start sync icon in cacheCredentials
return Zotero.Promise.try(function () {
return mode.cacheCredentials();
})
.then(function () {
// TODO: start sync icon
var library = item.libraryID;
var queue = Zotero.Sync.Storage.QueueManager.get(
'download', library
);
if (!requestCallbacks) {
requestCallbacks = {};
}
var onStart = function (request) {
return mode.downloadFile(request);
};
requestCallbacks.onStart = requestCallbacks.onStart
? [onStart, requestCallbacks.onStart]
: onStart;
var request = new Zotero.Sync.Storage.Request(
library + '/' + item.key, requestCallbacks
);
queue.addRequest(request, true);
queue.start();
return request.promise;
});
}
/**
* Extract a downloaded file and update the database metadata
*
* This is called from Zotero.Sync.Server.StreamListener.onStopRequest()
*
* @return {Promise<Object>} data - Promise for object with properties 'request', 'item',
* 'compressed', 'syncModTime', 'syncHash'
*/
this.processDownload = Zotero.Promise.coroutine(function* (data) {
var funcName = "Zotero.Sync.Storage.processDownload()";
if (!data) {
throw "'data' not set in " + funcName;
}
if (!data.item) {
throw "'data.item' not set in " + funcName;
}
if (!data.syncModTime) {
throw "'data.syncModTime' not set in " + funcName;
}
if (!data.compressed && !data.syncHash) {
throw "'data.syncHash' is required if 'data.compressed' is false in " + funcName;
}
var item = data.item;
var syncModTime = data.syncModTime;
var syncHash = data.syncHash;
// TODO: Test file hash
if (data.compressed) {
var newFile = yield _processZipDownload(item);
}
else {
var newFile = yield _processDownload(item);
}
// If |newFile| is set, the file was renamed, so set item filename to that
// and mark for updated
var file = item.getFile();
if (newFile && file.leafName != newFile.leafName) {
// Bypass library access check
_updatesInProgress = true;
// If library isn't editable but filename was changed, update
// database without updating the item's mod time, which would result
// in a library access error
if (!Zotero.Items.isEditable(item)) {
Zotero.debug("File renamed without library access -- "
+ "updating itemAttachments path", 3);
item.relinkAttachmentFile(newFile, true);
var useCurrentModTime = false;
}
else {
item.relinkAttachmentFile(newFile);
// TODO: use an integer counter instead of mod time for change detection
var useCurrentModTime = true;
}
file = item.getFile();
_updatesInProgress = false;
}
else {
var useCurrentModTime = false;
}
if (!file) {
// This can happen if an HTML snapshot filename was changed and synced
// elsewhere but the renamed file wasn't synced, so the ZIP doesn't
// contain a file with the known name
var missingFile = item.getFile(null, true);
Components.utils.reportError("File '" + missingFile.leafName
+ "' not found after processing download "
+ item.libraryID + "/" + item.key + " in " + funcName);
return false;
}
Zotero.DB.beginTransaction();
//var syncState = Zotero.Sync.Storage.getSyncState(item.id);
//var updateItem = syncState != this.SYNC_STATE_TO_DOWNLOAD;
var updateItem = false;
try {
if (useCurrentModTime) {
file.lastModifiedTime = new Date();
// Reset hash and sync state
Zotero.Sync.Storage.setSyncedHash(item.id, null);
Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD);
this.queueItem(item);
}
else {
file.lastModifiedTime = syncModTime;
// If hash not provided (e.g., WebDAV), calculate it now
if (!syncHash) {
syncHash = item.attachmentHash;
}
Zotero.Sync.Storage.setSyncedHash(item.id, syncHash);
Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
}
}
catch (e) {
Zotero.File.checkFileAccessError(e, file, 'update');
}
Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
Zotero.DB.commitTransaction();
return true;
});
this.checkServerPromise = function (mode) {
return mode.checkServer()
.spread(function (uri, status) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
var success = mode.checkServerCallback(uri, status, lastWin, true);
if (!success) {
Zotero.debug(mode.name + " verification failed");
var e = new Zotero.Error(
Zotero.getString('sync.storage.error.verificationFailed', mode.name),
0,
{
dialogButtonText: Zotero.getString('sync.openSyncPreferences'),
dialogButtonCallback: function () {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync');
}
}
);
throw e;
}
})
.then(function () {
Zotero.debug(mode.name + " file sync is successfully set up");
Zotero.Prefs.set("sync.storage.verified", true);
});
}
this.getItemDownloadImageNumber = function (item) {
var numImages = 64;
var lk = item.libraryID + "/" + item.key;
if (typeof _itemDownloadPercentages[lk] == 'undefined') {
return false;
}
var percentage = _itemDownloadPercentages[lk];
return Math.round(percentage / 100 * (numImages - 1)) + 1;
}
this.setItemDownloadPercentage = function (libraryKey, percentage) {
Zotero.debug("Setting image download percentage to " + percentage
+ " for item " + libraryKey);
if (percentage !== false) {
_itemDownloadPercentages[libraryKey] = percentage;
}
else {
delete _itemDownloadPercentages[libraryKey];
}
var libraryID, key;
[libraryID, key] = libraryKey.split("/");
var item = Zotero.Items.getByLibraryAndKey(libraryID, key);
// TODO: yield or switch to queue
Zotero.Notifier.trigger('redraw', 'item', item.id, { column: "hasAttachment" });
var parent = item.parentItemKey;
if (parent) {
var parentItem = Zotero.Items.getByLibraryAndKey(libraryID, parent);
var parentLibraryKey = libraryID + "/" + parentItem.key;
if (percentage !== false) {
_itemDownloadPercentages[parentLibraryKey] = percentage;
}
else {
delete _itemDownloadPercentages[parentLibraryKey];
}
Zotero.Notifier.trigger('redraw', 'item', parentItem.id, { column: "hasAttachment" });
}
}
this.resetAllSyncStates = function (syncState, includeUserFiles, includeGroupFiles) {
if (!includeUserFiles && !includeGroupFiles) {
includeUserFiles = true;
includeGroupFiles = true;
}
if (!syncState) {
syncState = this.SYNC_STATE_TO_UPLOAD;
}
switch (syncState) {
case this.SYNC_STATE_TO_UPLOAD:
case this.SYNC_STATE_TO_DOWNLOAD:
case this.SYNC_STATE_IN_SYNC:
break;
default:
throw ("Invalid sync state '" + syncState + "' in "
+ "Zotero.Sync.Storage.resetAllSyncStates()");
}
//var sql = "UPDATE itemAttachments SET syncState=?, storageModTime=NULL, storageHash=NULL";
var sql = "UPDATE itemAttachments SET syncState=?";
var params = [syncState];
if (includeUserFiles && !includeGroupFiles) {
sql += " WHERE itemID IN (SELECT itemID FROM items WHERE libraryID = ?)";
params.push(Zotero.Libraries.userLibraryID);
}
else if (!includeUserFiles && includeGroupFiles) {
sql += " WHERE itemID IN (SELECT itemID FROM items WHERE libraryID != ?)";
params.push(Zotero.Libraries.userLibraryID);
}
Zotero.DB.query(sql, [syncState]);
var sql = "DELETE FROM version WHERE schema LIKE 'storage_%'";
Zotero.DB.query(sql);
}
this.getItemFromRequestName = function (name) {
var [libraryID, key] = name.split('/');
return Zotero.Items.getByLibraryAndKey(libraryID, key);
}
this.notify = function(event, type, ids, extraData) {
if (event == 'open' && type == 'file') {
let timestamp = new Date().getTime();
for each(let id in ids) {
_uploadCheckFiles.push({
itemID: id,
timestamp: timestamp
});
}
}
}
//
// Private methods
//
function getModeFromLibrary(libraryID) {
if (libraryID === undefined) {
throw new Error("libraryID not provided");
}
// Personal library
if (!libraryID) {
if (Zotero.Sync.Storage.ZFS.includeUserFiles) {
return Zotero.Sync.Storage.ZFS;
}
if (Zotero.Sync.Storage.WebDAV.includeUserFiles) {
return Zotero.Sync.Storage.WebDAV;
}
return false;
}
// Group library
else {
if (Zotero.Sync.Storage.ZFS.includeGroupFiles) {
return Zotero.Sync.Storage.ZFS;
}
return false;
}
}
var _processDownload = Zotero.Promise.coroutine(function* (item) {
var funcName = "Zotero.Sync.Storage._processDownload()";
var tempFile = Zotero.getTempDirectory();
tempFile.append(item.key + '.tmp');
if (!tempFile.exists()) {
Zotero.debug(tempFile.path);
throw ("Downloaded file not found in " + funcName);
}
var parentDir = Zotero.Attachments.getStorageDirectory(item);
if (!parentDir.exists()) {
yield Zotero.Attachments.createDirectoryForItem(item);
}
_deleteExistingAttachmentFiles(item);
var file = item.getFile(null, true);
if (!file) {
throw ("Empty path for item " + item.key + " in " + funcName);
}
// Don't save Windows aliases
if (file.leafName.endsWith('.lnk')) {
return false;
}
var fileName = file.leafName;
var renamed = false;
// Make sure the new filename is valid, in case an invalid character made it over
// (e.g., from before we checked for them)
var filteredName = Zotero.File.getValidFileName(fileName);
if (filteredName != fileName) {
Zotero.debug("Filtering filename '" + fileName + "' to '" + filteredName + "'");
fileName = filteredName;
file.leafName = fileName;
renamed = true;
}
Zotero.debug("Moving download file " + tempFile.leafName + " into attachment directory as '" + fileName + "'");
try {
var destFile = parentDir.clone();
destFile.append(fileName);
Zotero.File.createShortened(destFile, Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
}
catch (e) {
Zotero.File.checkFileAccessError(e, destFile, 'create');
}
if (destFile.leafName != fileName) {
Zotero.debug("Changed filename '" + fileName + "' to '" + destFile.leafName + "'");
// Abort if Windows path limitation would cause filenames to be overly truncated
if (Zotero.isWin && destFile.leafName.length < 40) {
try {
destFile.remove(false);
}
catch (e) {}
// TODO: localize
var msg = "Due to a Windows path length limitation, your Zotero data directory "
+ "is too deep in the filesystem for syncing to work reliably. "
+ "Please relocate your Zotero data to a higher directory.";
Zotero.debug(msg, 1);
throw new Error(msg);
}
renamed = true;
}
try {
tempFile.moveTo(parentDir, destFile.leafName);
}
catch (e) {
try {
destFile.remove(false);
}
catch (e) {}
Zotero.File.checkFileAccessError(e, destFile, 'create');
}
var returnFile = null;
// processDownload() needs to know that we're renaming the file
if (renamed) {
returnFile = destFile.clone();
}
return returnFile;
});
var _processZipDownload = Zotero.Promise.coroutine(function* (item) {
var funcName = "Zotero.Sync.Storage._processDownloadedZip()";
var zipFile = Zotero.getTempDirectory();
zipFile.append(item.key + '.zip.tmp');
if (!zipFile.exists()) {
throw ("Downloaded ZIP file not found in " + funcName);
}
var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"].
createInstance(Components.interfaces.nsIZipReader);
try {
zipReader.open(zipFile);
zipReader.test(null);
Zotero.debug("ZIP file is OK");
}
catch (e) {
Zotero.debug(zipFile.leafName + " is not a valid ZIP file", 2);
zipReader.close();
try {
zipFile.remove(false);
}
catch (e) {
Zotero.File.checkFileAccessError(e, zipFile, 'delete');
}
// TODO: Remove prop file to trigger reuploading, in case it was an upload error?
return false;
}
var parentDir = Zotero.Attachments.getStorageDirectory(item);
if (!parentDir.exists()) {
yield Zotero.Attachments.createDirectoryForItem(item);
}
try {
_deleteExistingAttachmentFiles(item);
}
catch (e) {
zipReader.close();
throw (e);
}
var returnFile = null;
var count = 0;
var entries = zipReader.findEntries(null);
while (entries.hasMore()) {
count++;
var entryName = entries.getNext();
var b64re = /%ZB64$/;
if (entryName.match(b64re)) {
var fileName = Zotero.Utilities.Internal.Base64.decode(
entryName.replace(b64re, '')
);
}
else {
var fileName = entryName;
}
if (fileName.startsWith('.zotero')) {
Zotero.debug("Skipping " + fileName);
continue;
}
Zotero.debug("Extracting " + fileName);
var primaryFile = false;
var filtered = false;
var renamed = false;
// Get the old filename
var itemFileName = item.getFilename();
// Make sure the new filename is valid, in case an invalid character
// somehow make it into the ZIP (e.g., from before we checked for them)
//
// Do this before trying to use the relative descriptor, since otherwise
// it might fail silently and select the parent directory
var filteredName = Zotero.File.getValidFileName(fileName);
if (filteredName != fileName) {
Zotero.debug("Filtering filename '" + fileName + "' to '" + filteredName + "'");
fileName = filteredName;
filtered = true;
}
// Name in ZIP is a relative descriptor, so file has to be reconstructed
// using setRelativeDescriptor()
var destFile = parentDir.clone();
destFile.QueryInterface(Components.interfaces.nsILocalFile);
destFile.setRelativeDescriptor(parentDir, fileName);
fileName = destFile.leafName;
// If only one file in zip and it doesn't match the known filename,
// take our chances and use that name
if (count == 1 && !entries.hasMore() && itemFileName) {
// May not be necessary, but let's be safe
itemFileName = Zotero.File.getValidFileName(itemFileName);
if (itemFileName != fileName) {
Zotero.debug("Renaming single file '" + fileName + "' in ZIP to known filename '" + itemFileName + "'", 2);
Components.utils.reportError("Renaming single file '" + fileName + "' in ZIP to known filename '" + itemFileName + "'");
fileName = itemFileName;
destFile.leafName = fileName;
renamed = true;
}
}
var primaryFile = itemFileName == fileName;
if (primaryFile && filtered) {
renamed = true;
}
if (destFile.exists()) {
var msg = "ZIP entry '" + fileName + "' " + "already exists";
Zotero.debug(msg, 2);
Components.utils.reportError(msg + " in " + funcName);
continue;
}
try {
Zotero.File.createShortened(destFile, Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
}
catch (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
zipReader.close();
Zotero.File.checkFileAccessError(e, destFile, 'create');
}
if (destFile.leafName != fileName) {
Zotero.debug("Changed filename '" + fileName + "' to '" + destFile.leafName + "'");
// Abort if Windows path limitation would cause filenames to be overly truncated
if (Zotero.isWin && destFile.leafName.length < 40) {
try {
destFile.remove(false);
}
catch (e) {}
zipReader.close();
// TODO: localize
var msg = "Due to a Windows path length limitation, your Zotero data directory "
+ "is too deep in the filesystem for syncing to work reliably. "
+ "Please relocate your Zotero data to a higher directory.";
Zotero.debug(msg, 1);
throw new Error(msg);
}
if (primaryFile) {
renamed = true;
}
}
try {
zipReader.extract(entryName, destFile);
}
catch (e) {
try {
destFile.remove(false);
}
catch (e) {}
// For advertising junk files, ignore a bug on Windows where
// destFile.create() works but zipReader.extract() doesn't
// when the path length is close to 255.
if (destFile.leafName.match(/[a-zA-Z0-9+=]{130,}/)) {
var msg = "Ignoring error extracting '" + destFile.path + "'";
Zotero.debug(msg, 2);
Zotero.debug(e, 2);
Components.utils.reportError(msg + " in " + funcName);
continue;
}
zipReader.close();
Zotero.File.checkFileAccessError(e, destFile, 'create');
}
destFile.permissions = 0644;
// If we're renaming the main file, processDownload() needs to know
if (renamed) {
returnFile = destFile;
}
}
zipReader.close();
zipFile.remove(false);
return returnFile;
});
function _deleteExistingAttachmentFiles(item) {
var funcName = "Zotero.Sync.Storage._deleteExistingAttachmentFiles()";
var parentDir = Zotero.Attachments.getStorageDirectory(item);
// Delete existing files
var otherFiles = parentDir.directoryEntries;
otherFiles.QueryInterface(Components.interfaces.nsIDirectoryEnumerator);
var filesToDelete = [];
var file;
while (file = otherFiles.nextFile) {
if (file.leafName.startsWith('.zotero')) {
continue;
}
// Check symlink awareness, just to be safe
if (!parentDir.contains(file, false)) {
var msg = "Storage directory doesn't contain '" + file.leafName + "'";
Zotero.debug(msg, 2);
Components.utils.reportError(msg + " in " + funcName);
continue;
}
filesToDelete.push(file);
}
otherFiles.close();
// Do deletes outside of the enumerator to avoid an access error on Windows
for each(var file in filesToDelete) {
try {
if (file.isFile()) {
Zotero.debug("Deleting existing file " + file.leafName);
file.remove(false);
}
else if (file.isDirectory()) {
Zotero.debug("Deleting existing directory " + file.leafName);
file.remove(true);
}
}
catch (e) {
Zotero.File.checkFileAccessError(e, file, 'delete');
}
}
}
/**
* Create zip file of attachment directory
*
* @param {Zotero.Sync.Storage.Request} request
* @param {Function} callback
* @return {Boolean} TRUE if zip process started,
* FALSE if storage was empty
*/
this.createUploadFile = function (request, callback) {
var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
Zotero.debug("Creating zip file for item " + item.libraryID + "/" + item.key);
try {
switch (item.attachmentLinkMode) {
case Zotero.Attachments.LINK_MODE_LINKED_FILE:
case Zotero.Attachments.LINK_MODE_LINKED_URL:
throw (new Error(
"Upload file must be an imported snapshot or file in "
+ "Zotero.Sync.Storage.createUploadFile()"
));
}
var dir = Zotero.Attachments.getStorageDirectory(item);
var tmpFile = Zotero.getTempDirectory();
tmpFile.append(item.key + '.zip');
var zw = Components.classes["@mozilla.org/zipwriter;1"]
.createInstance(Components.interfaces.nsIZipWriter);
zw.open(tmpFile, 0x04 | 0x08 | 0x20); // open rw, create, truncate
var fileList = _zipDirectory(dir, dir, zw);
if (fileList.length == 0) {
Zotero.debug('No files to add -- removing zip file');
zw.close();
tmpFile.remove(null);
return false;
}
Zotero.debug('Creating ' + tmpFile.leafName + ' with ' + fileList.length + ' file(s)');
var observer = new Zotero.Sync.Storage.ZipWriterObserver(
zw, callback, { request: request, files: fileList }
);
zw.processQueue(observer, null);
return true;
}
// DEBUG: Do we want to catch this?
catch (e) {
Zotero.debug(e, 1);
Components.utils.reportError(e);
return false;
}
}
function _zipDirectory(rootDir, dir, zipWriter) {
var fileList = [];
dir = dir.directoryEntries;
while (dir.hasMoreElements()) {
var file = dir.getNext();
file.QueryInterface(Components.interfaces.nsILocalFile);
if (file.isDirectory()) {
//Zotero.debug("Recursing into directory " + file.leafName);
fileList.concat(_zipDirectory(rootDir, file, zipWriter));
continue;
}
var fileName = file.getRelativeDescriptor(rootDir);
if (fileName.startsWith('.zotero')) {
Zotero.debug('Skipping file ' + fileName);
continue;
}
//Zotero.debug("Adding file " + fileName);
zipWriter.addEntryFile(
fileName,
Components.interfaces.nsIZipWriter.COMPRESSION_DEFAULT,
file,
true
);
fileList.push(fileName);
}
return fileList;
}
/**
* Get files marked as ready to download
*
* @inner
* @return {Number[]} Array of attachment itemIDs
*/
function _getFilesToDownload(libraryID, forcedOnly) {
var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) "
+ "WHERE libraryID=? AND syncState IN (?";
var params = [libraryID, Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD];
if (!forcedOnly) {
sql += ",?";
params.push(Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD);
}
sql += ") "
// Skip attachments with empty path, which can't be saved, and files with .zotero*
// paths, which have somehow ended up in some users' libraries
+ "AND path!='' AND path NOT LIKE 'storage:.zotero%'";
var itemIDs = Zotero.DB.columnQuery(sql, params);
if (!itemIDs) {
return [];
}
return itemIDs;
}
/**
* Get files marked as ready to upload
*
* @inner
* @return {Number[]} Array of attachment itemIDs
*/
function _getFilesToUpload(libraryID) {
var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) "
+ "WHERE syncState IN (?,?) AND linkMode IN (?,?)";
var params = [
Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD,
Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD,
Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
Zotero.Attachments.LINK_MODE_IMPORTED_URL
];
if (typeof libraryID != 'undefined') {
sql += " AND libraryID=?";
params.push(libraryID);
}
else {
throw new Error("libraryID not specified");
}
var itemIDs = Zotero.DB.columnQuery(sql, params);
if (!itemIDs) {
return [];
}
return itemIDs;
}
/**
* Get files to check for local modifications for uploading
*
* This includes files previously modified and files opened externally
* via Zotero within _maxCheckAgeInSeconds.
*/
function _getFilesToCheck(libraryID) {
var minTime = new Date().getTime() - (_maxCheckAgeInSeconds * 1000);
// Get files by modification time
var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) "
+ "WHERE libraryID=? AND linkMode IN (?,?) AND syncState IN (?) AND "
+ "storageModTime>=?";
var params = [
libraryID,
Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
Zotero.Attachments.LINK_MODE_IMPORTED_URL,
Zotero.Sync.Storage.SYNC_STATE_IN_SYNC,
minTime
];
var itemIDs = Zotero.DB.columnQuery(sql, params) || [];
// Get files by open time
_uploadCheckFiles.filter(function (x) x.timestamp >= minTime);
itemIDs = itemIDs.concat([x.itemID for each(x in _uploadCheckFiles)])
return Zotero.Utilities.arrayUnique(itemIDs);
}
/**
* @inner
* @return {String[]|FALSE} Array of keys, or FALSE if none
*/
this.getDeletedFiles = function () {
var sql = "SELECT key FROM storageDeleteLog";
return Zotero.DB.columnQuery(sql);
}
function error(e) {
if (_syncInProgress) {
Zotero.Sync.Storage.QueueManager.cancel(true);
_syncInProgress = false;
}
Zotero.DB.rollbackAllTransactions();
if (e) {
Zotero.debug(e, 1);
}
else {
e = Zotero.Sync.Storage.defaultError;
}
if (e.error && e.error == Zotero.Error.ERROR_ZFS_FILE_EDITING_DENIED) {
setTimeout(function () {
var group = Zotero.Groups.get(e.data.groupID);
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)
+ ps.BUTTON_DELAY_ENABLE;
var index = ps.confirmEx(
null,
Zotero.getString('general.warning'),
Zotero.getString('sync.storage.error.fileEditingAccessLost', group.name) + "\n\n"
+ Zotero.getString('sync.error.groupWillBeReset') + "\n\n"
+ Zotero.getString('sync.error.copyChangedItems'),
buttonFlags,
Zotero.getString('sync.resetGroupAndSync'),
null, null, null, {}
);
if (index == 0) {
// TODO: transaction
group.erase();
Zotero.Sync.Server.resetClient();
Zotero.Sync.Storage.resetAllSyncStates();
Zotero.Sync.Runner.sync();
return;
}
}, 1);
}
}
}
/**
* Request observer for zip writing
*
* Implements nsIRequestObserver
*
* @param {nsIZipWriter} zipWriter
* @param {Function} callback
* @param {Object} data
*/
Zotero.Sync.Storage.ZipWriterObserver = function (zipWriter, callback, data) {
this._zipWriter = zipWriter;
this._callback = callback;
this._data = data;
}
Zotero.Sync.Storage.ZipWriterObserver.prototype = {
onStartRequest: function () {},
onStopRequest: function(req, context, status) {
var zipFileName = this._zipWriter.file.leafName;
var originalSize = 0;
for each(var fileName in this._data.files) {
var entry = this._zipWriter.getEntry(fileName);
if (!entry) {
var msg = "ZIP entry '" + fileName + "' not found for request '" + this._data.request.name + "'";
Components.utils.reportError(msg);
Zotero.debug(msg, 1);
this._zipWriter.close();
this._callback(false);
return;
}
originalSize += entry.realSize;
}
delete this._data.files;
this._zipWriter.close();
Zotero.debug("Zip of " + zipFileName + " finished with status " + status
+ " (original " + Math.round(originalSize / 1024) + "KB, "
+ "compressed " + Math.round(this._zipWriter.file.fileSize / 1024) + "KB, "
+ Math.round(
((originalSize - this._zipWriter.file.fileSize) / originalSize) * 100
) + "% reduction)");
Zotero.Sync.Storage.compressionTracker.compressed += this._zipWriter.file.fileSize;
Zotero.Sync.Storage.compressionTracker.uncompressed += originalSize;
Zotero.debug("Average compression so far: "
+ Math.round(Zotero.Sync.Storage.compressionTracker.ratio * 100) + "%");
this._callback(this._data);
}
}