2425 lines
65 KiB
JavaScript
2425 lines
65 KiB
JavaScript
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_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;
|
|
|
|
//
|
|
// Public properties
|
|
//
|
|
|
|
|
|
this.__defineGetter__("syncInProgress", function () _syncInProgress);
|
|
|
|
this.compressionTracker = {
|
|
compressed: 0,
|
|
uncompressed: 0,
|
|
get ratio() {
|
|
return Math.round(
|
|
(Zotero.Sync.Storage.compressionTracker.uncompressed -
|
|
Zotero.Sync.Storage.compressionTracker.compressed) /
|
|
Zotero.Sync.Storage.compressionTracker.uncompressed * 100);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Private properties
|
|
//
|
|
var _syncInProgress;
|
|
var _changesMade;
|
|
|
|
var _session;
|
|
|
|
var _callbacks = {
|
|
onSuccess: function () {},
|
|
onSkip: function () {},
|
|
onStop: function () {},
|
|
onError: function () {},
|
|
onWarning: function () {}
|
|
};
|
|
|
|
//
|
|
// Public methods
|
|
//
|
|
this.sync = function (module, callbacks) {
|
|
for (var func in callbacks) {
|
|
_callbacks[func] = callbacks[func];
|
|
}
|
|
|
|
_session = new Zotero.Sync.Storage.Session(module, {
|
|
onChangesMade: function () {
|
|
_changesMade = true;
|
|
},
|
|
onError: _error
|
|
});
|
|
|
|
if (!_session.enabled) {
|
|
Zotero.debug(_session.name + " file sync is not enabled");
|
|
_callbacks.onSkip();
|
|
return;
|
|
}
|
|
if (!_session.initFromPrefs()) {
|
|
Zotero.debug(_session.name + " module not initialized");
|
|
_callbacks.onSkip();
|
|
return;
|
|
}
|
|
|
|
if (!_session.active) {
|
|
Zotero.debug(_session.name + " file sync is not active");
|
|
|
|
var callback = 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 = _session.checkServerCallback(uri, status, lastWin, true);
|
|
if (success) {
|
|
Zotero.debug(_session.name + " file sync is successfully set up");
|
|
Zotero.Sync.Storage.sync(module, callbacks);
|
|
}
|
|
else {
|
|
Zotero.debug(_session.name + " verification failed");
|
|
_callbacks.onError(_session.name + " verification failed. Verify your "
|
|
+ "WebDAV settings in the Sync pane of the Zotero preferences.");
|
|
}
|
|
}
|
|
|
|
_session.checkServer(callback);
|
|
return;
|
|
}
|
|
|
|
if (_syncInProgress) {
|
|
_error("File sync operation already in progress");
|
|
}
|
|
|
|
Zotero.debug("Beginning " + _session.name + " file sync");
|
|
_syncInProgress = true;
|
|
_changesMade = false;
|
|
|
|
Zotero.Sync.Storage.checkForUpdatedFiles(null, null, _session.includeUserFiles, _session.includeGroupFiles);
|
|
|
|
var lastSyncCheckCallback = function (lastSyncTime) {
|
|
var downloadFiles = true;
|
|
|
|
var sql = "SELECT COUNT(*) FROM itemAttachments WHERE syncState=?";
|
|
var force = !!Zotero.DB.valueQuery(sql, Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD);
|
|
|
|
if (!force && lastSyncTime) {
|
|
var sql = "SELECT version FROM version WHERE schema='storage_" + module + "'";
|
|
var version = Zotero.DB.valueQuery(sql);
|
|
if (version == lastSyncTime) {
|
|
Zotero.debug("Last " + _session.name + " sync time hasn't changed -- skipping file download step");
|
|
downloadFiles = false;
|
|
}
|
|
}
|
|
|
|
var activeDown = downloadFiles ? Zotero.Sync.Storage.downloadFiles() : false;
|
|
var activeUp = Zotero.Sync.Storage.uploadFiles();
|
|
if (!activeDown && !activeUp) {
|
|
_syncInProgress = false;
|
|
_callbacks.onSkip();
|
|
}
|
|
};
|
|
|
|
_session.getLastSyncTime(lastSyncCheckCallback);
|
|
}
|
|
|
|
|
|
/**
|
|
* @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:
|
|
_error("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 Unix timestamp,
|
|
* 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) {
|
|
_error("Item " + itemID
|
|
+ " not found in Zotero.Sync.Storage.getSyncedModificationTime()");
|
|
}
|
|
return mtime;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Integer} itemID
|
|
* @param {Integer} mtime File modification time as
|
|
* Unix timestamp
|
|
* @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) {
|
|
_error("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;
|
|
Zotero.debug('================');
|
|
Zotero.debug(fileHash);
|
|
Zotero.debug(syncHash);
|
|
if (fileHash && fileHash == syncHash) {
|
|
Zotero.debug("Mod time didn't match but hash did for " + file.leafName + " -- ignoring");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Scans local files and marks any that have changed as 0 for uploading
|
|
* and any that are missing as 1 for downloading
|
|
*
|
|
* Also marks missing files for downloading
|
|
*
|
|
* @param {Integer[]} [itemIDs] An optional set of item ids to check
|
|
* @param {Object} [itemModTimes] Item mod times indexed by item ids
|
|
* appearing in itemIDs; if set,
|
|
* items with stored mod times
|
|
* that differ from the provided
|
|
* time but file mod times
|
|
* matching the stored time will
|
|
* be marked for download
|
|
* @param {Boolean} [includePersonalItems=false]
|
|
* @param {Boolean} [includeGroupItems=false]
|
|
* @return {Boolean} TRUE if any items changed state,
|
|
* FALSE otherwise
|
|
*/
|
|
this.checkForUpdatedFiles = function (itemIDs, itemModTimes, includeUserFiles, includeGroupFiles) {
|
|
var funcName = "Zotero.Sync.Storage.checkForUpdatedFiles()";
|
|
|
|
Zotero.debug("Checking for locally changed attachment files");
|
|
// check for current ops?
|
|
|
|
if (itemIDs) {
|
|
if (includeUserFiles || includeGroupFiles) {
|
|
_error("includeUserFiles and includeGroupFiles are not allowed when itemIDs is set in " + funcName);
|
|
}
|
|
}
|
|
else {
|
|
if (!includeUserFiles && !includeGroupFiles) {
|
|
_error("At least one of includeUserFiles or includeGroupFiles must be set in " + funcName);
|
|
}
|
|
}
|
|
|
|
if (itemModTimes && !itemIDs) {
|
|
_error("itemModTimes can only be set if itemIDs is an array in " + funcName);
|
|
}
|
|
|
|
var changed = false;
|
|
|
|
if (!itemIDs) {
|
|
itemIDs = [];
|
|
}
|
|
|
|
// Can only handle 999 bound parameters at a time
|
|
var numIDs = itemIDs.length;
|
|
var maxIDs = 990;
|
|
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 (?,?)";
|
|
if (includeUserFiles && !includeGroupFiles) {
|
|
sql += " AND libraryID IS NULL";
|
|
}
|
|
else if (!includeUserFiles && includeGroupFiles) {
|
|
sql += " AND libraryID IS NOT NULL";
|
|
}
|
|
var params = [
|
|
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 (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);
|
|
|
|
if (!rows) {
|
|
Zotero.debug("No to-upload or in-sync files found");
|
|
Zotero.DB.commitTransaction();
|
|
return changed;
|
|
}
|
|
|
|
// Index data by item id
|
|
var itemIDs = [];
|
|
var attachmentData = {};
|
|
for each(var 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
|
|
};
|
|
}
|
|
if (itemIDs.length == 0) {
|
|
Zotero.DB.commitTransaction();
|
|
return changed;
|
|
}
|
|
|
|
rows = undefined;
|
|
|
|
var updatedStates = {};
|
|
var items = Zotero.Items.get(itemIDs);
|
|
for each(var item in items) {
|
|
var file = item.getFile(attachmentData[item.id]);
|
|
if (!file) {
|
|
Zotero.debug("Marking attachment " + item.id + " as missing");
|
|
updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
|
|
continue;
|
|
}
|
|
|
|
var fileModTime = Math.round(file.lastModifiedTime / 1000);
|
|
|
|
//Zotero.debug("Stored mtime is " + attachmentData[item.id].mtime);
|
|
//Zotero.debug("File mtime is " + fileModTime);
|
|
|
|
// Download-marking mode
|
|
if (itemModTimes) {
|
|
Zotero.debug("Item mod time is " + itemModTimes[item.id]);
|
|
|
|
// Ignore attachments whose storage mod times haven't changed
|
|
if (row.storageModTime == itemModTimes[id]) {
|
|
Zotero.debug("Storage mod time (" + row.storageModTime + ") "
|
|
+ "hasn't changed for attachment " + id);
|
|
continue;
|
|
}
|
|
|
|
Zotero.debug("Marking attachment " + item.id + " for download");
|
|
updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
|
|
|
|
continue;
|
|
}
|
|
|
|
// If stored time matches file, it hasn't changed
|
|
if (attachmentData[item.id].mtime == fileModTime) {
|
|
continue;
|
|
}
|
|
|
|
// If file is already marked for upload, skip
|
|
if (attachmentData[item.id].state == Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD) {
|
|
continue;
|
|
}
|
|
|
|
// If file hash matches stored hash, only the mod time changed, so skip
|
|
var f = item.getFile();
|
|
if (f) {
|
|
Zotero.debug(f.path);
|
|
}
|
|
else {
|
|
Zotero.debug("File missing before getting hash");
|
|
}
|
|
var fileHash = item.attachmentHash;
|
|
if (attachmentData[item.id].hash && attachmentData[item.id].hash == fileHash) {
|
|
Zotero.debug("Mod time didn't match but hash did for " + file.leafName + " -- ignoring");
|
|
continue;
|
|
}
|
|
|
|
Zotero.debug("Marking attachment " + item.id + " as changed ("
|
|
+ attachmentData[item.id].mtime + " != " + fileModTime + ")");
|
|
updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD;
|
|
}
|
|
|
|
for (var itemID in updatedStates) {
|
|
var sql = "UPDATE itemAttachments SET syncState=? WHERE itemID=?";
|
|
Zotero.DB.query(
|
|
sql,
|
|
[
|
|
updatedStates[itemID],
|
|
itemID
|
|
]
|
|
);
|
|
changed = true;
|
|
}
|
|
|
|
if (!changed) {
|
|
Zotero.debug("No synced files have changed locally");
|
|
}
|
|
|
|
//throw ('foo');
|
|
|
|
Zotero.DB.commitTransaction();
|
|
return changed;
|
|
}
|
|
|
|
|
|
/**
|
|
* Starts download of all attachments marked for download
|
|
*
|
|
* @return {Boolean}
|
|
*/
|
|
this.downloadFiles = function () {
|
|
if (!_syncInProgress) {
|
|
_syncInProgress = true;
|
|
}
|
|
|
|
var downloadFileIDs = _getFilesToDownload(_session.includeUserFiles, _session.includeGroupFiles);
|
|
if (!downloadFileIDs) {
|
|
Zotero.debug("No files to download");
|
|
return false;
|
|
}
|
|
|
|
// Check for active operations?
|
|
var queue = Zotero.Sync.Storage.QueueManager.get('download');
|
|
if (queue.isRunning()) {
|
|
throw ("Download queue already running in "
|
|
+ "Zotero.Sync.Storage.downloadFiles()");
|
|
}
|
|
queue.reset();
|
|
|
|
for each(var itemID in downloadFileIDs) {
|
|
var item = Zotero.Items.get(itemID);
|
|
if (Zotero.Sync.Storage.getSyncState(itemID) !=
|
|
Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD
|
|
&& this.isFileModified(itemID)) {
|
|
Zotero.debug("File for attachment " + itemID + " has been modified");
|
|
this.setSyncState(itemID, this.SYNC_STATE_TO_UPLOAD);
|
|
continue;
|
|
}
|
|
|
|
var request = new Zotero.Sync.Storage.Request(
|
|
item.libraryID + '/' + item.key, function (request) { _session.downloadFile(request); }
|
|
);
|
|
queue.addRequest(request);
|
|
}
|
|
|
|
// Start downloads
|
|
queue.start();
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Extract a downloaded file and update the database metadata
|
|
*
|
|
* This is called from Zotero.Sync.Server.StreamListener.onStopRequest()
|
|
*
|
|
* @return {Object} data Properties 'request', 'item', 'compressed', 'syncModTime', 'syncHash'
|
|
*/
|
|
this.processDownload = function (data) {
|
|
var funcName = "Zotero.Sync.Storage.processDownload()";
|
|
|
|
if (!data) {
|
|
_error("|data| not set in " + funcName);
|
|
}
|
|
|
|
if (!data.item) {
|
|
_error("|data.item| not set in " + funcName);
|
|
}
|
|
|
|
if (!data.syncModTime) {
|
|
_error("|data.syncModTime| not set in " + funcName);
|
|
}
|
|
|
|
if (!data.compressed && !data.syncHash) {
|
|
_error("|data.storageHash| 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 = _processZipDownload(item);
|
|
}
|
|
else {
|
|
var newFile = _processDownload(item);
|
|
}
|
|
|
|
// If |updated| is a file, it was renamed, so set item filename to that
|
|
// and mark for updated
|
|
var file = item.getFile();
|
|
if (newFile && file.leafName != newFile.leafName) {
|
|
item.relinkAttachmentFile(newFile);
|
|
file = item.getFile();
|
|
// TODO: use an integer counter instead of mod time for change detection
|
|
var useCurrentModTime = true;
|
|
}
|
|
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;
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
var syncState = Zotero.Sync.Storage.getSyncState(item.id);
|
|
|
|
|
|
var updateItem = syncState != 1;
|
|
var updateItem = false;
|
|
|
|
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);
|
|
Zotero.Sync.Storage.resyncOnFinish = true;
|
|
}
|
|
else {
|
|
file.lastModifiedTime = syncModTime * 1000;
|
|
|
|
// Only save hash if file isn't compressed
|
|
if (!data.compressed) {
|
|
Zotero.Sync.Storage.setSyncedHash(item.id, syncHash, false);
|
|
}
|
|
Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
|
|
}
|
|
|
|
Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
|
|
Zotero.DB.commitTransaction();
|
|
_changesMade = true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Start upload of all attachments marked for upload
|
|
*
|
|
* @return {Boolean}
|
|
*/
|
|
this.uploadFiles = function () {
|
|
if (!_syncInProgress) {
|
|
_syncInProgress = true;
|
|
}
|
|
|
|
var uploadFileIDs = _getFilesToUpload(_session.includeUserFiles, _session.includeGroupFiles);
|
|
if (!uploadFileIDs) {
|
|
Zotero.debug("No files to upload");
|
|
return false;
|
|
}
|
|
|
|
// Check for active operations?
|
|
var queue = Zotero.Sync.Storage.QueueManager.get('upload');
|
|
if (queue.isRunning()) {
|
|
throw ("Upload queue already running in "
|
|
+ "Zotero.Sync.Storage.uploadFiles()");
|
|
}
|
|
queue.reset();
|
|
|
|
Zotero.debug(uploadFileIDs.length + " file(s) to upload");
|
|
|
|
for each(var itemID in uploadFileIDs) {
|
|
var item = Zotero.Items.get(itemID);
|
|
|
|
var request = new Zotero.Sync.Storage.Request(
|
|
item.libraryID + '/' + item.key, function (request) { _session.uploadFile(request); }
|
|
);
|
|
request.progressMax = Zotero.Attachments.getTotalFileSize(item, true);
|
|
queue.addRequest(request);
|
|
}
|
|
|
|
// Start uploads
|
|
queue.start();
|
|
return true;
|
|
}
|
|
|
|
|
|
this.checkServer = function (module, callback) {
|
|
_session = new Zotero.Sync.Storage.Session(
|
|
module,
|
|
{
|
|
onError: function (e) {
|
|
Zotero.debug(e, 1);
|
|
callback(null, null, e);
|
|
throw (e);
|
|
}
|
|
}
|
|
);
|
|
_session.initFromPrefs();
|
|
_session.checkServer(callback);
|
|
}
|
|
|
|
|
|
this.checkServerCallback = function (uri, status, window, skipSuccessMessage, e) {
|
|
return _session.checkServerCallback(uri, status, window, skipSuccessMessage, e);
|
|
}
|
|
|
|
|
|
this.purgeDeletedStorageFiles = function (module, callback) {
|
|
_session = new Zotero.Sync.Storage.Session(module, { onError: _error });
|
|
if (!_session.initFromPrefs()) {
|
|
_error("Module not initialized");
|
|
}
|
|
_session.purgeDeletedStorageFiles(callback);
|
|
}
|
|
|
|
|
|
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=?";
|
|
if (includeUserFiles && !includeGroupFiles) {
|
|
sql += " WHERE itemID IN (SELECT itemID FROM items WHERE libraryID IS NULL)";
|
|
}
|
|
else if (!includeUserFiles && includeGroupFiles) {
|
|
sql += " WHERE itemID IN (SELECT itemID FROM items WHERE libraryID IS NOT NULL)";
|
|
}
|
|
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('/');
|
|
if (libraryID == "null") {
|
|
libraryID = null;
|
|
}
|
|
return Zotero.Items.getByLibraryAndKey(libraryID, key);
|
|
}
|
|
|
|
|
|
//
|
|
// Private methods
|
|
//
|
|
function _processDownload(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.id);
|
|
if (!parentDir.exists()) {
|
|
Zotero.Attachments.createDirectoryForItem(item.id);
|
|
}
|
|
|
|
_deleteExistingAttachmentFiles(item);
|
|
|
|
var file = item.getFile(null, true);
|
|
if (!file) {
|
|
throw ("Empty path for item " + item.key + " in " + funcName);
|
|
}
|
|
var newName = file.leafName;
|
|
var returnFile = null
|
|
|
|
Zotero.debug("Moving download file " + tempFile.leafName + " into attachment directory");
|
|
try {
|
|
tempFile.moveTo(parentDir, newName);
|
|
}
|
|
catch (e) {
|
|
var destFile = parentDir.clone();
|
|
destFile.append(newName);
|
|
|
|
var windowsLength = false;
|
|
var nameLength = false;
|
|
|
|
// Windows API only allows paths of 260 characters
|
|
if (e.name == "NS_ERROR_FILE_NOT_FOUND" && destFile.path.length > 255) {
|
|
windowsLength = true;
|
|
}
|
|
// ext3/ext4/HFS+ have a filename length limit of ~254 bytes
|
|
//
|
|
// These filenames will almost always be ASCII ad files,
|
|
// but allow an extra 10 bytes anyway
|
|
else if (e.name == "NS_ERROR_FAILURE" && destFile.leafName.length >= 244) {
|
|
nameLength = true;
|
|
}
|
|
// ecrypt (on Ubuntu, at least) can result in a lower limit --
|
|
// not much we can do about this, but log a warning
|
|
else if (e.name == "NS_ERROR_FAILURE" && Zotero.isLinux && destFile.leafName.length > 130) {
|
|
var msg = "Error creating file '" + destFile.leafName + "' "
|
|
+ "(Are you using filesystem encryption such as ecrypt "
|
|
+ "that results in a filename length limit below 255 bytes?)";
|
|
Components.utils.reportError(msg);
|
|
}
|
|
|
|
if (windowsLength || nameLength) {
|
|
// Preserve extension
|
|
var matches = destFile.leafName.match(/\.[a-z0-9]{0,8}$/);
|
|
var ext = matches ? matches[0] : "";
|
|
|
|
if (windowsLength) {
|
|
var pathLength = destFile.path.length - destFile.leafName.length;
|
|
var newLength = 255 - pathLength;
|
|
// Require 40 available characters in path -- this is arbitrary,
|
|
// but otherwise filenames are going to end up being cut off
|
|
if (newLength < 40) {
|
|
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.";
|
|
throw (msg);
|
|
}
|
|
}
|
|
else {
|
|
var newLength = 254;
|
|
}
|
|
|
|
// Shorten file if it's too long -- we don't relink it, but this should
|
|
// be pretty rare and probably only occurs on extraneous files with
|
|
// gibberish for filenames
|
|
var newName = destFile.leafName.substr(0, newLength - (ext.length + 1)) + ext;
|
|
var msg = "Shortening filename to '" + newName + "'";
|
|
Zotero.debug(msg, 2);
|
|
Components.utils.reportError(msg);
|
|
|
|
tempFile.moveTo(parentDir, newName);
|
|
|
|
destFile = parentDir.clone();
|
|
destFile.append(newName);
|
|
|
|
// processDownload() needs to know that we're renaming the file
|
|
returnFile = destFile;
|
|
}
|
|
else {
|
|
throw(e);
|
|
}
|
|
}
|
|
|
|
return returnFile;
|
|
}
|
|
|
|
|
|
function _processZipDownload(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();
|
|
zipFile.remove(false);
|
|
|
|
// TODO: Remove prop file to trigger reuploading, in case it was an upload error?
|
|
|
|
return false;
|
|
}
|
|
|
|
var parentDir = Zotero.Attachments.getStorageDirectory(item.id);
|
|
if (!parentDir.exists()) {
|
|
Zotero.Attachments.createDirectoryForItem(item.id);
|
|
}
|
|
|
|
try {
|
|
_deleteExistingAttachmentFiles(item);
|
|
}
|
|
catch (e) {
|
|
zipReader.close();
|
|
throw (e);
|
|
}
|
|
|
|
var returnFile = null;
|
|
|
|
var entries = zipReader.findEntries(null);
|
|
while (entries.hasMore()) {
|
|
var entryName = entries.getNext();
|
|
var b64re = /%ZB64$/;
|
|
if (entryName.match(b64re)) {
|
|
var fileName = Zotero.Utilities.Base64.decode(
|
|
entryName.replace(b64re, '')
|
|
);
|
|
}
|
|
else {
|
|
var fileName = entryName;
|
|
}
|
|
|
|
if (fileName.indexOf('.') == 0) {
|
|
Zotero.debug("Skipping " + fileName);
|
|
continue;
|
|
}
|
|
|
|
// Make sure the new filename is valid, in case an invalid character
|
|
// for this OS somehow make it into the ZIP (e.g., from before we checked
|
|
// for them or if a user manually renamed and relinked a file on another OS)
|
|
fileName = Zotero.File.getValidFileName(fileName);
|
|
|
|
Zotero.debug("Extracting " + fileName);
|
|
var destFile = parentDir.clone();
|
|
destFile.QueryInterface(Components.interfaces.nsILocalFile);
|
|
destFile.setRelativeDescriptor(parentDir, fileName);
|
|
if (destFile.exists()) {
|
|
var msg = "ZIP entry '" + fileName + "' " + "already exists";
|
|
Zotero.debug(msg, 2);
|
|
Components.utils.reportError(msg + " in " + funcName);
|
|
continue;
|
|
}
|
|
try {
|
|
destFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
|
|
}
|
|
catch (e) {
|
|
var windowsLength = false;
|
|
var nameLength = false;
|
|
|
|
// Windows API only allows paths of 260 characters
|
|
if (e.name == "NS_ERROR_FILE_NOT_FOUND" && destFile.path.length > 255) {
|
|
windowsLength = true;
|
|
}
|
|
// ext3/ext4/HFS+ have a filename length limit of ~254 bytes
|
|
//
|
|
// These filenames will almost always be ASCII ad files,
|
|
// but allow an extra 10 bytes anyway
|
|
else if (e.name == "NS_ERROR_FAILURE" && destFile.leafName.length >= 244) {
|
|
nameLength = true;
|
|
}
|
|
// ecrypt (on Ubuntu, at least) can result in a lower limit --
|
|
// not much we can do about this, but log a warning
|
|
else if (e.name == "NS_ERROR_FAILURE" && Zotero.isLinux && destFile.leafName.length > 130) {
|
|
var msg = "Error creating file '" + destFile.leafName + "' "
|
|
+ "(Are you using filesystem encryption such as ecrypt "
|
|
+ "that results in a filename length limit below 255 bytes?)";
|
|
Components.utils.reportError(msg);
|
|
}
|
|
|
|
if (windowsLength || nameLength) {
|
|
// Is this the main attachment file?
|
|
var primaryFile = item.getFile(null, true).leafName == destFile.leafName;
|
|
|
|
// Preserve extension
|
|
var matches = destFile.leafName.match(/\.[a-z0-9]{0,8}$/);
|
|
var ext = matches ? matches[0] : "";
|
|
|
|
if (windowsLength) {
|
|
var pathLength = destFile.path.length - destFile.leafName.length;
|
|
var newLength = 255 - pathLength;
|
|
// Require 40 available characters in path -- this is arbitrary,
|
|
// but otherwise filenames are going to end up being cut off
|
|
if (newLength < 40) {
|
|
zipReader.close();
|
|
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.";
|
|
throw (msg);
|
|
}
|
|
}
|
|
else {
|
|
var newLength = 254;
|
|
}
|
|
|
|
// Shorten file if it's too long -- we don't relink it, but this should
|
|
// be pretty rare and probably only occurs on extraneous files with
|
|
// gibberish for filenames
|
|
//
|
|
// Shortened file could already exist if there was another file with a
|
|
// similar name that was also longer than the limit, so we do this in a
|
|
// loop, adding numbers if necessary
|
|
var step = 0;
|
|
do {
|
|
if (step == 0) {
|
|
var newName = destFile.leafName.substr(0, newLength - ext.length) + ext;
|
|
}
|
|
else {
|
|
var newName = destFile.leafName.substr(0, newLength - ext.length) + "-" + step + ext;
|
|
}
|
|
destFile.leafName = newName;
|
|
step++;
|
|
}
|
|
while (destFile.exists());
|
|
|
|
var msg = "Shortening filename to '" + newName + "'";
|
|
Zotero.debug(msg, 2);
|
|
Components.utils.reportError(msg);
|
|
|
|
destFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
|
|
|
|
// If we're renaming the main file, processDownload() needs to know
|
|
if (primaryFile) {
|
|
returnFile = destFile;
|
|
}
|
|
}
|
|
else {
|
|
zipReader.close();
|
|
throw(e);
|
|
}
|
|
}
|
|
zipReader.extract(entryName, destFile);
|
|
|
|
var origPath = destFile.path;
|
|
var origFileName = destFile.leafName;
|
|
destFile.normalize();
|
|
if (origPath != destFile.path) {
|
|
var msg = "ZIP file " + zipFile.leafName + " contained symlink '"
|
|
+ origFileName + "'";
|
|
Zotero.debug(msg, 1);
|
|
Components.utils.reportError(msg + " in " + funcName);
|
|
continue;
|
|
}
|
|
destFile.permissions = 0644;
|
|
}
|
|
zipReader.close();
|
|
zipFile.remove(false);
|
|
|
|
return returnFile;
|
|
}
|
|
|
|
|
|
function _deleteExistingAttachmentFiles(item) {
|
|
var funcName = "Zotero.Sync.Storage._deleteExistingAttachmentFiles()";
|
|
|
|
var parentDir = Zotero.Attachments.getStorageDirectory(item.id);
|
|
|
|
// Delete existing files
|
|
var otherFiles = parentDir.directoryEntries;
|
|
while (otherFiles.hasMoreElements()) {
|
|
var file = otherFiles.getNext();
|
|
file.QueryInterface(Components.interfaces.nsIFile);
|
|
if (file.leafName[0] == '.') {
|
|
continue;
|
|
}
|
|
|
|
// Firefox (as of 3.0.1) can't detect symlinks (at least on OS X),
|
|
// so use pre/post-normalized path to check
|
|
var origPath = file.path;
|
|
var origFileName = file.leafName;
|
|
file.normalize();
|
|
if (origPath != file.path) {
|
|
var msg = "Not deleting symlink '" + origFileName + "'";
|
|
Zotero.debug(msg, 2);
|
|
Components.utils.reportError(msg + " in " + funcName);
|
|
continue;
|
|
}
|
|
// This should be redundant with above check, but let's do it anyway
|
|
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;
|
|
}
|
|
|
|
if (file.isFile()) {
|
|
Zotero.debug("Deleting existing file " + file.leafName);
|
|
try {
|
|
file.remove(false);
|
|
}
|
|
catch (e) {
|
|
if (e.name == 'NS_ERROR_FILE_ACCESS_DENIED') {
|
|
Zotero.debug(e);
|
|
|
|
// TODO: localize
|
|
var msg = "The file '" + file.leafName + "' is in use and cannot "
|
|
+ "be updated. Please close the file or restart your computer "
|
|
+ "and try syncing again.";
|
|
throw (msg);
|
|
}
|
|
|
|
throw (e);
|
|
}
|
|
}
|
|
else if (file.isDirectory()) {
|
|
Zotero.debug("Deleting existing directory " + file.leafName);
|
|
file.remove(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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.getStorageDirectoryByKey(item.key);
|
|
|
|
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');
|
|
tmpFile.remove(null);
|
|
request.finish();
|
|
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;
|
|
}
|
|
catch (e) {
|
|
request.error(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.indexOf('.') == 0) {
|
|
Zotero.debug('Skipping file ' + fileName);
|
|
continue;
|
|
}
|
|
|
|
//Zotero.debug("Adding file " + fileName);
|
|
|
|
fileName = Zotero.Utilities.Base64.encode(fileName) + "%ZB64";
|
|
zipWriter.addEntryFile(
|
|
fileName,
|
|
Components.interfaces.nsIZipWriter.COMPRESSION_DEFAULT,
|
|
file,
|
|
true
|
|
);
|
|
fileList.push(fileName);
|
|
}
|
|
return fileList;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Get files marked as ready to upload
|
|
*
|
|
* @inner
|
|
* @return {Number[]} Array of attachment itemIDs
|
|
*/
|
|
function _getFilesToDownload(includeUserFiles, includeGroupFiles) {
|
|
if (!includeUserFiles && !includeGroupFiles) {
|
|
_error("At least one of includeUserFiles or includeGroupFiles must be set "
|
|
+ "in Zotero.Sync.Storage._getFilesToDownload()");
|
|
}
|
|
|
|
var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) "
|
|
+ "WHERE syncState IN (?,?)";
|
|
if (includeUserFiles && !includeGroupFiles) {
|
|
sql += " AND libraryID IS NULL";
|
|
}
|
|
else if (!includeUserFiles && includeGroupFiles) {
|
|
sql += " AND libraryID IS NOT NULL";
|
|
}
|
|
return Zotero.DB.columnQuery(sql,
|
|
[
|
|
Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD,
|
|
Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD
|
|
]
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get files marked as ready to upload
|
|
*
|
|
* @inner
|
|
* @return {Number[]} Array of attachment itemIDs
|
|
*/
|
|
function _getFilesToUpload(includeUserFiles, includeGroupFiles) {
|
|
if (!includeUserFiles && !includeGroupFiles) {
|
|
_error("At least one of includeUserFiles or includeGroupFiles must be set "
|
|
+ "in Zotero.Sync.Storage._getFilesToUpload()");
|
|
}
|
|
|
|
var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) "
|
|
+ "WHERE syncState IN (?,?) AND linkMode IN (?,?)";
|
|
if (includeUserFiles && !includeGroupFiles) {
|
|
sql += " AND libraryID IS NULL";
|
|
}
|
|
else if (!includeUserFiles && includeGroupFiles) {
|
|
sql += " AND libraryID IS NOT NULL";
|
|
}
|
|
return Zotero.DB.columnQuery(sql,
|
|
[
|
|
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
|
|
]
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* @inner
|
|
* @param {Integer} [days=pref:e.z.sync.storage.deleteDelayDays]
|
|
* Number of days old files have to be
|
|
* @return {String[]|FALSE} Array of keys, or FALSE if none
|
|
*/
|
|
this.getDeletedFiles = function (days) {
|
|
if (!days) {
|
|
days = Zotero.Prefs.get("sync.storage.deleteDelayDays");
|
|
}
|
|
|
|
var ts = Zotero.Date.getUnixTimestamp();
|
|
ts = ts - (86400 * days);
|
|
|
|
var sql = "SELECT key FROM storageDeleteLog WHERE timestamp<?";
|
|
return Zotero.DB.columnQuery(sql, ts);
|
|
}
|
|
|
|
|
|
this.finish = function (cancelled, skipSuccessFile) {
|
|
if (!_syncInProgress) {
|
|
throw ("Sync not in progress in Zotero.Sync.Storage.finish()");
|
|
}
|
|
|
|
// Upload success file when done
|
|
if (!this.resyncOnFinish && !skipSuccessFile) {
|
|
// If we finished successfully and didn't upload any files, save the
|
|
// last sync time locally rather than setting a new one on the server,
|
|
// since we don't want other clients to check for new files
|
|
var uploadQueue = Zotero.Sync.Storage.QueueManager.get('upload');
|
|
var useLastSyncTime = !cancelled && uploadQueue.totalRequests == 0;
|
|
|
|
_session.setLastSyncTime(function () {
|
|
Zotero.Sync.Storage.finish(cancelled, true);
|
|
}, useLastSyncTime);
|
|
return;
|
|
}
|
|
|
|
Zotero.debug(_session.name + " sync is complete");
|
|
_syncInProgress = false;
|
|
|
|
if (this.resyncOnFinish) {
|
|
Zotero.debug("Force-resyncing items in conflict");
|
|
this.resyncOnFinish = false;
|
|
this.sync(_session.module, _callbacks);
|
|
return;
|
|
}
|
|
|
|
_session = null;
|
|
|
|
if (!_changesMade) {
|
|
Zotero.debug("No changes made during storage sync");
|
|
}
|
|
|
|
if (cancelled) {
|
|
_callbacks.onStop();
|
|
return;
|
|
}
|
|
|
|
if (!_changesMade) {
|
|
_callbacks.onSkip();
|
|
return;
|
|
}
|
|
|
|
_callbacks.onSuccess();
|
|
}
|
|
|
|
|
|
//
|
|
// Stop requests, log error, and
|
|
//
|
|
function _error(e) {
|
|
if (_syncInProgress) {
|
|
Zotero.Sync.Storage.QueueManager.cancel(true);
|
|
_syncInProgress = false;
|
|
_session = null;
|
|
}
|
|
|
|
Zotero.DB.rollbackAllTransactions();
|
|
|
|
Zotero.debug(e, 1);
|
|
|
|
// If we get a quota error, log and continue
|
|
if (e.error && e.error == Zotero.Error.ERROR_ZFS_OVER_QUOTA && _callbacks.onWarning) {
|
|
_callbacks.onWarning(e);
|
|
_callbacks.onSuccess();
|
|
}
|
|
else if (e.error && e.error == Zotero.Error.ERROR_ZFS_FILE_EDITING_DENIED) {
|
|
setTimeout(function () {
|
|
var group = Zotero.Groups.get(e.data.groupID);
|
|
|
|
var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
|
|
.createInstance(Components.interfaces.nsIPrompt);
|
|
var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING)
|
|
+ (pr.BUTTON_POS_1) * (pr.BUTTON_TITLE_CANCEL)
|
|
+ pr.BUTTON_DELAY_ENABLE;
|
|
var index = pr.confirmEx(
|
|
Zotero.getString('general.warning'),
|
|
// TODO: localize
|
|
"You no longer have file editing access to the Zotero group '" + group.name + "', "
|
|
+ "and files you've added or edited cannot be synced to the server.\n\n"
|
|
+ "If you continue, your copy of the group will be reset to its state "
|
|
+ "on the server, and local modifications to items and files will be lost.\n\n"
|
|
+ "If you would like a chance to copy changed items and files elsewhere, "
|
|
+ "cancel the sync now.",
|
|
buttonFlags,
|
|
"Reset Group and Sync",
|
|
null, null, null, {}
|
|
);
|
|
|
|
if (index == 0) {
|
|
group.erase();
|
|
Zotero.Sync.Server.resetClient();
|
|
Zotero.Sync.Storage.resetAllSyncStates();
|
|
Zotero.Sync.Runner.sync();
|
|
return;
|
|
}
|
|
}, 1);
|
|
_callbacks.onError(e);
|
|
}
|
|
else if (_callbacks.onError) {
|
|
_callbacks.onError(e);
|
|
}
|
|
else {
|
|
throw (e);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
Zotero.Sync.Storage.QueueManager = new function () {
|
|
var _queues = {};
|
|
var _conflicts = [];
|
|
|
|
|
|
/**
|
|
* Retrieving a queue, creating a new one if necessary
|
|
*
|
|
* @param {String} queueName
|
|
*/
|
|
this.get = function (queueName) {
|
|
// Initialize the queue if it doesn't exist yet
|
|
if (!_queues[queueName]) {
|
|
var queue = new Zotero.Sync.Storage.Queue(queueName);
|
|
switch (queueName) {
|
|
case 'download':
|
|
queue.maxConcurrentRequests =
|
|
Zotero.Prefs.get('sync.storage.maxDownloads')
|
|
break;
|
|
|
|
case 'upload':
|
|
queue.maxConcurrentRequests =
|
|
Zotero.Prefs.get('sync.storage.maxUploads')
|
|
break;
|
|
|
|
default:
|
|
throw ("Invalid queue '" + queueName + "' in Zotero.Sync.Storage.QueueManager.get()");
|
|
}
|
|
_queues[queueName] = queue;
|
|
}
|
|
|
|
return _queues[queueName];
|
|
}
|
|
|
|
|
|
/**
|
|
* Stop all queues
|
|
*
|
|
* @param {Boolean} [skipStorageFinish=false] Don't call Zotero.Sync.Storage.finish()
|
|
* when done (used when we stopped because of
|
|
* an error)
|
|
*/
|
|
this.cancel = function (skipStorageFinish) {
|
|
this._cancelled = true;
|
|
if (skipStorageFinish) {
|
|
this._skipStorageFinish = true;
|
|
}
|
|
for each(var queue in _queues) {
|
|
if (!queue.isFinished() && !queue.isStopping()) {
|
|
queue.stop();
|
|
}
|
|
}
|
|
_conflicts = [];
|
|
}
|
|
|
|
|
|
/**
|
|
* Tell the storage system that we're finished
|
|
*/
|
|
this.finish = function () {
|
|
if (_conflicts.length) {
|
|
var data = _reconcileConflicts();
|
|
if (data) {
|
|
_processMergeData(data);
|
|
}
|
|
_conflicts = [];
|
|
}
|
|
|
|
if (this._skipStorageFinish) {
|
|
this._cancelled = false;
|
|
this._skipStorageFinish = false;
|
|
return;
|
|
}
|
|
|
|
Zotero.Sync.Storage.finish(this._cancelled);
|
|
this._cancelled = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculate the current progress values and trigger a display update
|
|
*
|
|
* Also detects when all queues have finished and ends sync progress
|
|
*/
|
|
this.updateProgress = function () {
|
|
var activeRequests = 0;
|
|
var allFinished = true;
|
|
for each(var queue in _queues) {
|
|
// Finished or never started
|
|
if (queue.isFinished() || (!queue.isRunning() && !queue.isStopping())) {
|
|
continue;
|
|
}
|
|
allFinished = false;
|
|
activeRequests += queue.activeRequests;
|
|
}
|
|
if (activeRequests == 0) {
|
|
this.updateProgressMeters(0);
|
|
if (allFinished) {
|
|
this.finish();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Percentage
|
|
var percentageSum = 0;
|
|
var numQueues = 0;
|
|
for each(var queue in _queues) {
|
|
percentageSum += queue.percentage;
|
|
numQueues++;
|
|
}
|
|
var percentage = Math.round(percentageSum / numQueues);
|
|
//Zotero.debug("Total percentage is " + percentage);
|
|
|
|
// Remaining KB
|
|
var downloadStatus = _queues.download ?
|
|
_getQueueStatus(_queues.download) : 0;
|
|
var uploadStatus = _queues.upload ?
|
|
_getQueueStatus(_queues.upload) : 0;
|
|
|
|
this.updateProgressMeters(
|
|
activeRequests, percentage, downloadStatus, uploadStatus
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* Cycle through windows, updating progress meters with new values
|
|
*/
|
|
this.updateProgressMeters = function (activeRequests, percentage, downloadStatus, uploadStatus) {
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
var enumerator = wm.getEnumerator("navigator:browser");
|
|
while (enumerator.hasMoreElements()) {
|
|
var win = enumerator.getNext();
|
|
var doc = win.document;
|
|
|
|
//
|
|
// TODO: Move to overlay.js?
|
|
//
|
|
var box = doc.getElementById("zotero-tb-sync-progress-box");
|
|
var meter = doc.getElementById("zotero-tb-sync-progress");
|
|
|
|
if (activeRequests == 0) {
|
|
box.hidden = true;
|
|
continue;
|
|
}
|
|
|
|
meter.setAttribute("value", percentage);
|
|
box.hidden = false;
|
|
|
|
var tooltip = doc.
|
|
getElementById("zotero-tb-sync-progress-tooltip-progress");
|
|
tooltip.setAttribute("value", percentage + "%");
|
|
|
|
var tooltip = doc.
|
|
getElementById("zotero-tb-sync-progress-tooltip-downloads");
|
|
tooltip.setAttribute("value", downloadStatus);
|
|
|
|
var tooltip = doc.
|
|
getElementById("zotero-tb-sync-progress-tooltip-uploads");
|
|
tooltip.setAttribute("value", uploadStatus);
|
|
}
|
|
}
|
|
|
|
|
|
this.addConflict = function (requestName, localData, remoteData) {
|
|
Zotero.debug('===========');
|
|
Zotero.debug(localData);
|
|
Zotero.debug(remoteData);
|
|
|
|
_conflicts.push({
|
|
name: requestName,
|
|
localData: localData,
|
|
remoteData: remoteData
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a status string for a queue
|
|
*
|
|
* @param {Zotero.Sync.Storage.Queue} queue
|
|
* @return {String}
|
|
*/
|
|
function _getQueueStatus(queue) {
|
|
var remaining = queue.remaining;
|
|
var unfinishedRequests = queue.unfinishedRequests;
|
|
|
|
if (!unfinishedRequests) {
|
|
return Zotero.getString('sync.storage.none')
|
|
}
|
|
|
|
var kbRemaining = Zotero.getString(
|
|
'sync.storage.kbRemaining',
|
|
Zotero.Utilities.prototype.numberFormat(remaining / 1024, 0)
|
|
);
|
|
var totalRequests = queue.totalRequests;
|
|
// TODO: localize
|
|
/*
|
|
var filesRemaining = Zotero.getString(
|
|
'sync.storage.filesRemaining',
|
|
[totalRequests - unfinishedRequests, totalRequests]
|
|
);
|
|
*/
|
|
var filesRemaining = (totalRequests - unfinishedRequests)
|
|
+ "/" + totalRequests + " files";
|
|
var status = Zotero.localeJoin([kbRemaining, '(' + filesRemaining + ')']);
|
|
return status;
|
|
}
|
|
|
|
|
|
function _reconcileConflicts() {
|
|
var objectPairs = [];
|
|
for each(var conflict in _conflicts) {
|
|
var item = Zotero.Sync.Storage.getItemFromRequestName(conflict.name);
|
|
var item1 = item.clone(false, false, true);
|
|
item1.setField('dateModified',
|
|
Zotero.Date.dateToSQL(new Date(conflict.localData.modTime * 1000), true));
|
|
var item2 = item.clone(false, false, true);
|
|
item2.setField('dateModified',
|
|
Zotero.Date.dateToSQL(new Date(conflict.remoteData.modTime * 1000), true));
|
|
objectPairs.push([item1, item2]);
|
|
}
|
|
|
|
var io = {
|
|
dataIn: {
|
|
type: 'storagefile',
|
|
captions: [
|
|
// TODO: localize
|
|
'Local File',
|
|
'Remote File',
|
|
'Saved File'
|
|
],
|
|
objects: objectPairs
|
|
}
|
|
};
|
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
var lastWin = wm.getMostRecentWindow("navigator:browser");
|
|
lastWin.openDialog('chrome://zotero/content/merge.xul', '', 'chrome,modal,centerscreen', io);
|
|
|
|
if (!io.dataOut) {
|
|
return false;
|
|
}
|
|
|
|
// Since we're only putting cloned items into the merge window,
|
|
// we have to manually set the ids
|
|
for (var i=0; i<_conflicts.length; i++) {
|
|
io.dataOut[i].id = Zotero.Sync.Storage.getItemFromRequestName(_conflicts[i].name).id;
|
|
}
|
|
|
|
return io.dataOut;
|
|
}
|
|
|
|
|
|
function _processMergeData(data) {
|
|
if (!data.length) {
|
|
return false;
|
|
}
|
|
|
|
Zotero.Sync.Storage.resyncOnFinish = true;
|
|
|
|
for each(var mergeItem in data) {
|
|
var itemID = mergeItem.id;
|
|
var dateModified = mergeItem.ref.getField('dateModified');
|
|
// Local
|
|
if (dateModified == mergeItem.left.getField('dateModified')) {
|
|
Zotero.Sync.Storage.setSyncState(
|
|
itemID, Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD
|
|
);
|
|
}
|
|
// Remote
|
|
else {
|
|
Zotero.Sync.Storage.setSyncState(
|
|
itemID, Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Queue for storage sync transfer requests
|
|
*
|
|
* @param {String} name Queue name (e.g., 'download' or 'upload')
|
|
*/
|
|
Zotero.Sync.Storage.Queue = function (name) {
|
|
Zotero.debug("Initializing " + name + " queue");
|
|
|
|
//
|
|
// Public properties
|
|
//
|
|
this.name = name;
|
|
this.__defineGetter__('Name', function () {
|
|
return this.name[0].toUpperCase() + this.name.substr(1);
|
|
});
|
|
this.maxConcurrentRequests = 1;
|
|
|
|
this.__defineGetter__('running', function () _running);
|
|
this.__defineGetter__('stopping', function () _stopping);
|
|
this.activeRequests = 0;
|
|
this.__defineGetter__('finishedRequests', function () {
|
|
return _finishedReqs;
|
|
});
|
|
this.__defineSetter__('finishedRequests', function (val) {
|
|
Zotero.debug("Finished requests: " + val);
|
|
Zotero.debug("Total requests: " + this.totalRequests);
|
|
|
|
_finishedReqs = val;
|
|
|
|
if (val == 0) {
|
|
return;
|
|
}
|
|
|
|
// Last request
|
|
if (val == this.totalRequests) {
|
|
Zotero.debug(this.Name + " queue is done");
|
|
// DEBUG info
|
|
Zotero.debug("Active requests: " + this.activeRequests);
|
|
if (this._errors) {
|
|
Zotero.debug("Errors:");
|
|
Zotero.debug(this._errors);
|
|
}
|
|
|
|
if (this.activeRequests) {
|
|
throw (this.Name + " queue can't be finished if there "
|
|
+ "are active requests in Zotero.Sync.Storage.finishedRequests");
|
|
}
|
|
|
|
this._running = false;
|
|
this._stopping = false;
|
|
this._finished = true;
|
|
return;
|
|
}
|
|
|
|
if (this.isStopping() || this.isFinished()) {
|
|
return;
|
|
}
|
|
this.advance();
|
|
});
|
|
this.totalRequests = 0;
|
|
|
|
this.__defineGetter__('unfinishedRequests', function () {
|
|
return this.totalRequests - this.finishedRequests;
|
|
});
|
|
this.__defineGetter__('queuedRequests', function () {
|
|
return this.unfinishedRequests - this.activeRequests;
|
|
});
|
|
this.__defineGetter__('remaining', function () {
|
|
var remaining = 0;
|
|
for each(var request in this._requests) {
|
|
remaining += request.remaining;
|
|
}
|
|
return remaining;
|
|
});
|
|
this.__defineGetter__('percentage', function () {
|
|
if (this.totalRequests == 0) {
|
|
return 0;
|
|
}
|
|
|
|
var completedRequests = 0;
|
|
for each(var request in this._requests) {
|
|
completedRequests += request.percentage / 100;
|
|
}
|
|
return Math.round((completedRequests / this.totalRequests) * 100);
|
|
});
|
|
|
|
|
|
//
|
|
// Private properties
|
|
//
|
|
this._requests = {};
|
|
this._running = false;
|
|
this._errors = [];
|
|
this._stopping = false;
|
|
this._finished = false;
|
|
|
|
var _finishedReqs = 0;
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Queue.prototype.isRunning = function () {
|
|
return this._running;
|
|
}
|
|
|
|
Zotero.Sync.Storage.Queue.prototype.isStopping = function () {
|
|
return this._stopping;
|
|
}
|
|
|
|
Zotero.Sync.Storage.Queue.prototype.isFinished = function () {
|
|
return this._finished;
|
|
}
|
|
|
|
/**
|
|
* Add a request to this queue
|
|
*
|
|
* @param {Zotero.Sync.Storage.Request} request
|
|
*/
|
|
Zotero.Sync.Storage.Queue.prototype.addRequest = function (request) {
|
|
if (this.isRunning()) {
|
|
throw ("Can't add request after queue started");
|
|
}
|
|
if (this.isFinished()) {
|
|
throw ("Can't add request after queue finished");
|
|
}
|
|
|
|
request.queue = this;
|
|
var name = request.name;
|
|
Zotero.debug("Queuing " + this.name + " request '" + name + "'");
|
|
|
|
if (this._requests[name]) {
|
|
throw (this.name + " request '" + name + "' already exists in "
|
|
+ "Zotero.Sync.Storage.Queue.addRequest()");
|
|
}
|
|
|
|
this._requests[name] = request;
|
|
this.totalRequests++;
|
|
}
|
|
|
|
|
|
/**
|
|
* Starts this queue
|
|
*/
|
|
Zotero.Sync.Storage.Queue.prototype.start = function () {
|
|
if (this._running) {
|
|
throw (this.Name + " queue is already running in "
|
|
+ "Zotero.Sync.Storage.Queue.start()");
|
|
}
|
|
|
|
if (!this.queuedRequests) {
|
|
Zotero.debug("No requests to start in " + this.name + " queue");
|
|
return;
|
|
}
|
|
|
|
this._running = true;
|
|
this.advance();
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Queue.prototype.logError = function (msg) {
|
|
Zotero.debug(msg, 1);
|
|
Components.utils.reportError(msg);
|
|
// TODO: necessary?
|
|
this._errors.push(msg);
|
|
}
|
|
|
|
|
|
/**
|
|
* Start another request in this queue if there's an available slot
|
|
*/
|
|
Zotero.Sync.Storage.Queue.prototype.advance = function () {
|
|
if (this._stopping) {
|
|
Zotero.debug(this.Name + " queue is being stopped in "
|
|
+ "Zotero.Sync.Storage.Queue.advance()", 2);
|
|
return;
|
|
}
|
|
if (this._finished) {
|
|
Zotero.debug(this.Name + " queue already finished "
|
|
+ "Zotero.Sync.Storage.Queue.advance()", 2);
|
|
return;
|
|
}
|
|
|
|
if (!this.queuedRequests) {
|
|
Zotero.debug("No remaining requests in " + this.name + " queue ("
|
|
+ this.activeRequests + " active, "
|
|
+ this.finishedRequests + " finished)");
|
|
return;
|
|
}
|
|
|
|
if (this.activeRequests >= this.maxConcurrentRequests) {
|
|
Zotero.debug(this.Name + " queue is busy ("
|
|
+ this.activeRequests + "/" + this.maxConcurrentRequests + ")");
|
|
return;
|
|
}
|
|
|
|
for each(var request in this._requests) {
|
|
if (!request.isRunning() && !request.isFinished()) {
|
|
request.start();
|
|
|
|
var self = this;
|
|
|
|
// Wait a second and then try starting another
|
|
setTimeout(function () {
|
|
if (self.isStopping() || self.isFinished()) {
|
|
return;
|
|
}
|
|
self.advance();
|
|
}, 1000);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Queue.prototype.updateProgress = function () {
|
|
Zotero.Sync.Storage.QueueManager.updateProgress();
|
|
}
|
|
|
|
|
|
/**
|
|
* Stops all requests in this queue
|
|
*/
|
|
Zotero.Sync.Storage.Queue.prototype.stop = function () {
|
|
if (this._stopping) {
|
|
Zotero.debug("Already stopping " + this.name + " queue");
|
|
return;
|
|
}
|
|
if (this._finished) {
|
|
Zotero.debug(this.Name + " queue is already finished");
|
|
return;
|
|
}
|
|
|
|
// If no requests, finish manually
|
|
if (this.activeRequests == 0) {
|
|
this._finishedRequests = this._finishedRequests;
|
|
return;
|
|
}
|
|
|
|
this._stopping = true;
|
|
for each(var request in this._requests) {
|
|
if (!request.isFinished()) {
|
|
request.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Clears queue state data
|
|
*/
|
|
Zotero.Sync.Storage.Queue.prototype.reset = function () {
|
|
Zotero.debug("Resetting " + this.name + " queue");
|
|
|
|
if (this._running) {
|
|
throw ("Can't reset running queue in Zotero.Sync.Storage.Queue.reset()");
|
|
}
|
|
if (this._stopping) {
|
|
throw ("Can't reset stopping queue in Zotero.Sync.Storage.Queue.reset()");
|
|
}
|
|
|
|
this._finished = false;
|
|
this._requests = {};
|
|
this._errors = [];
|
|
this.activeRequests = 0;
|
|
this.finishedRequests = 0;
|
|
this.totalRequests = 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Updates multiplier applied to estimated sizes
|
|
*
|
|
* Also updates progress meter
|
|
*/
|
|
/*
|
|
function _updateSizeMultiplier(mult) {
|
|
var previousMult = _requestSizeMultiplier;
|
|
_requestSizeMultiplier = mult;
|
|
for (var queue in _requests) {
|
|
for (var name in _requests[queue]) {
|
|
var r = _requests[queue][name];
|
|
if (r.progressMax > 0 || !r.size) {
|
|
continue;
|
|
}
|
|
// Remove previous estimated size and add new one
|
|
_totalProgressMax[queue] += Math.round(r.size * previousMult) * -1
|
|
+ Math.round(r.size * mult);
|
|
}
|
|
}
|
|
_updateProgressMeter();
|
|
}
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
* Transfer request for storage sync
|
|
*
|
|
* @param {String} name Identifier for request (e.g., "[libraryID]/[key]")
|
|
* @param {Function} onStart Callback when request is started
|
|
*/
|
|
Zotero.Sync.Storage.Request = function (name, onStart) {
|
|
Zotero.debug("Initializing request '" + name + "'");
|
|
|
|
this.name = name;
|
|
this.channel = null;
|
|
this.queue = null;
|
|
this.progress = 0;
|
|
this.progressMax = 0;
|
|
|
|
this._running = false;
|
|
this._onStart = onStart;
|
|
this._percentage = 0;
|
|
this._remaining = null;
|
|
this._finished = false;
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.__defineGetter__('percentage', function () {
|
|
if (this.progressMax == 0) {
|
|
return 0;
|
|
}
|
|
|
|
var percentage = Math.round((this.progress / this.progressMax) * 100);
|
|
if (percentage < this._percentage) {
|
|
Zotero.debug(percentage + " is less than last percentage of "
|
|
+ this._percentage + " for request '" + this.name + "'", 2);
|
|
Zotero.debug(this.progress);
|
|
Zotero.debug(this.progressMax);
|
|
percentage = this._percentage;
|
|
}
|
|
else if (percentage > 100) {
|
|
Zotero.debug(percentage + " is greater than 100 for "
|
|
+ this.name + " request", 2);
|
|
Zotero.debug(this.progress);
|
|
Zotero.debug(this.progressMax);
|
|
percentage = 100;
|
|
}
|
|
else {
|
|
this._percentage = percentage;
|
|
}
|
|
//Zotero.debug("Request '" + this.name + "' percentage is " + percentage);
|
|
return percentage;
|
|
});
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.__defineGetter__('remaining', function () {
|
|
if (!this.progressMax) {
|
|
//Zotero.debug("Remaining not yet available for request '" + this.name + "'");
|
|
return 0;
|
|
}
|
|
|
|
var remaining = this.progressMax - this.progress;
|
|
if (this._remaining === null) {
|
|
this._remaining = remaining;
|
|
}
|
|
else if (remaining > this._remaining) {
|
|
Zotero.debug(remaining + " is greater than the last remaining amount of "
|
|
+ this._remaining + " for request " + this.name);
|
|
remaining = this._remaining;
|
|
}
|
|
else if (remaining < 0) {
|
|
Zotero.debug(remaining + " is less than 0 for request " + this.name);
|
|
}
|
|
else {
|
|
this._remaining = remaining;
|
|
}
|
|
//Zotero.debug("Request '" + this.name + "' remaining is " + remaining);
|
|
return remaining;
|
|
});
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.setChannel = function (channel) {
|
|
this.channel = channel;
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.start = function () {
|
|
if (!this.queue) {
|
|
throw ("Request '" + this.name + "' must be added to a queue before starting");
|
|
}
|
|
|
|
if (this._running) {
|
|
throw ("Request '" + this.name + "' already running in "
|
|
+ "Zotero.Sync.Storage.Request.start()");
|
|
}
|
|
|
|
Zotero.debug("Starting " + this.queue.name + " request '" + this.name + "'");
|
|
this._running = true;
|
|
this.queue.activeRequests++;
|
|
this._onStart(this);
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.isRunning = function () {
|
|
return this._running;
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.isFinished = function () {
|
|
return this._finished;
|
|
}
|
|
|
|
|
|
/**
|
|
* Update counters for given request
|
|
*
|
|
* Also updates progress meter
|
|
*
|
|
* @param {Integer} progress Progress so far
|
|
* (usually bytes transferred)
|
|
* @param {Integer} progressMax Max progress value for this request
|
|
* (usually total bytes)
|
|
*/
|
|
Zotero.Sync.Storage.Request.prototype.onProgress = function (channel, progress, progressMax) {
|
|
if (!this._running) {
|
|
throw ("Trying to update finished request " + this.name + " in "
|
|
+ "Zotero.Sync.Storage.Request.onProgress()");
|
|
}
|
|
|
|
if (!this.channel) {
|
|
this.channel = channel;
|
|
}
|
|
|
|
// Workaround for invalid progress values (possibly related to
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=451991 and fixed in 3.1)
|
|
if (progress < this.progress) {
|
|
Zotero.debug("Invalid progress for request '"
|
|
+ this.name + "' (" + progress + " < " + this.progress + ")");
|
|
return;
|
|
}
|
|
|
|
if (progressMax != this.progressMax) {
|
|
Zotero.debug("progressMax has changed from " + this.progressMax
|
|
+ " to " + progressMax + " for request '" + this.name + "'", 2);
|
|
}
|
|
|
|
this.progress = progress;
|
|
this.progressMax = progressMax;
|
|
this.queue.updateProgress();
|
|
}
|
|
|
|
|
|
Zotero.Sync.Storage.Request.prototype.error = function (msg) {
|
|
msg = typeof msg == 'object' ? msg.message : msg;
|
|
|
|
this.queue.logError(msg);
|
|
|
|
// DEBUG: ever need to stop channel?
|
|
this.finish();
|
|
}
|
|
|
|
|
|
/**
|
|
* Stop the request's underlying network request, if there is one
|
|
*/
|
|
Zotero.Sync.Storage.Request.prototype.stop = function () {
|
|
var finishNow = false;
|
|
try {
|
|
// If upload already finished, finish() will never be called otherwise
|
|
if (this.channel) {
|
|
this.channel.QueryInterface(Components.interfaces.nsIHttpChannel);
|
|
// Throws error if request not finished
|
|
this.channel.requestSucceeded;
|
|
Zotero.debug("Channel is no longer running for request " + this.name);
|
|
Zotero.debug(this.channel.requestSucceeded);
|
|
finishNow = true;
|
|
}
|
|
else {
|
|
Zotero.debug("No channel to stop for request " + this.name);
|
|
}
|
|
}
|
|
catch (e) { Zotero.debug(e); }
|
|
|
|
if (!this._running || !this.channel || finishNow) {
|
|
this.finish();
|
|
return;
|
|
}
|
|
|
|
Zotero.debug("Stopping request '" + this.name + "'");
|
|
this.channel.cancel(0x804b0002); // NS_BINDING_ABORTED
|
|
}
|
|
|
|
|
|
/**
|
|
* Mark request as finished and notify queue that it's done
|
|
*/
|
|
Zotero.Sync.Storage.Request.prototype.finish = function () {
|
|
if (this._finished) {
|
|
throw ("Request '" + this.name + "' is already finished");
|
|
}
|
|
|
|
Zotero.debug("Finishing " + this.queue.name + " request '" + this.name + "'");
|
|
|
|
this._finished = true;
|
|
var active = this._running;
|
|
this._running = false;
|
|
|
|
if (active) {
|
|
this.queue.activeRequests--;
|
|
}
|
|
// mechanism for failures?
|
|
this.queue.finishedRequests++;
|
|
this.queue.updateProgress();
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* 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);
|
|
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: "
|
|
+ Zotero.Sync.Storage.compressionTracker.ratio + "%");
|
|
|
|
this._callback(this._data);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Stream listener that can handle both download and upload requests
|
|
*
|
|
* Possible properties of data object:
|
|
* - onStart: f(request)
|
|
* - onProgress: f(request, progress, progressMax)
|
|
* - onStop: f(request, status, response, data)
|
|
* - onCancel: f(request, status, data)
|
|
* - streams: array of streams to close on completion
|
|
* - Other values to pass to onStop()
|
|
*/
|
|
Zotero.Sync.Storage.StreamListener = function (data) {
|
|
this._data = data;
|
|
}
|
|
|
|
Zotero.Sync.Storage.StreamListener.prototype = {
|
|
_channel: null,
|
|
|
|
// nsIProgressEventSink
|
|
onProgress: function (request, context, progress, progressMax) {
|
|
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=451991
|
|
// (fixed in Fx3.1)
|
|
if (progress > progressMax) {
|
|
progress = progressMax;
|
|
}
|
|
//Zotero.debug("onProgress with " + progress + "/" + progressMax);
|
|
this._onProgress(request, progress, progressMax);
|
|
},
|
|
|
|
onStatus: function (request, context, status, statusArg) {
|
|
//Zotero.debug('onStatus');
|
|
},
|
|
|
|
// nsIRequestObserver
|
|
// Note: For uploads, this isn't called until data is done uploading
|
|
onStartRequest: function (request, context) {
|
|
Zotero.debug('onStartRequest');
|
|
this._response = "";
|
|
|
|
this._onStart(request);
|
|
},
|
|
|
|
onStopRequest: function (request, context, status) {
|
|
Zotero.debug('onStopRequest');
|
|
|
|
switch (status) {
|
|
case 0:
|
|
case 0x804b0002: // NS_BINDING_ABORTED
|
|
this._onDone(request, status);
|
|
break;
|
|
|
|
default:
|
|
throw ("Unexpected request status " + status
|
|
+ " in Zotero.Sync.Storage.StreamListener.onStopRequest()");
|
|
}
|
|
},
|
|
|
|
// nsIWebProgressListener
|
|
onProgressChange: function (wp, request, curSelfProgress,
|
|
maxSelfProgress, curTotalProgress, maxTotalProgress) {
|
|
//Zotero.debug("onProgressChange with " + curTotalProgress + "/" + maxTotalProgress);
|
|
|
|
// onProgress gets called too, so this isn't necessary
|
|
//this._onProgress(request, curTotalProgress, maxTotalProgress);
|
|
},
|
|
|
|
onStateChange: function (wp, request, stateFlags, status) {
|
|
Zotero.debug("onStateChange");
|
|
|
|
if ((stateFlags & Components.interfaces.nsIWebProgressListener.STATE_START)
|
|
&& (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_NETWORK)) {
|
|
this._onStart(request);
|
|
}
|
|
else if ((stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
|
|
&& (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_NETWORK)) {
|
|
this._onDone(request, status);
|
|
}
|
|
},
|
|
|
|
onStatusChange: function (progress, request, status, message) {
|
|
Zotero.debug("onStatusChange with '" + message + "'");
|
|
},
|
|
onLocationChange: function () {},
|
|
onSecurityChange: function () {},
|
|
|
|
// nsIStreamListener
|
|
onDataAvailable: function (request, context, stream, sourceOffset, length) {
|
|
Zotero.debug('onDataAvailable');
|
|
var scriptableInputStream =
|
|
Components.classes["@mozilla.org/scriptableinputstream;1"]
|
|
.createInstance(Components.interfaces.nsIScriptableInputStream);
|
|
scriptableInputStream.init(stream);
|
|
|
|
this._response += scriptableInputStream.read(length);
|
|
},
|
|
|
|
// nsIChannelEventSink
|
|
onChannelRedirect: function (oldChannel, newChannel, flags) {
|
|
Zotero.debug('onRedirect');
|
|
|
|
// if redirecting, store the new channel
|
|
this._channel = newChannel;
|
|
},
|
|
|
|
// nsIHttpEventSink
|
|
onRedirect: function (oldChannel, newChannel) {
|
|
Zotero.debug('onRedirect');
|
|
},
|
|
|
|
|
|
//
|
|
// Private methods
|
|
//
|
|
_onStart: function (request) {
|
|
//Zotero.debug('Starting request');
|
|
if (this._data && this._data.onStart) {
|
|
var data = this._getPassData();
|
|
this._data.onStart(request, data);
|
|
}
|
|
},
|
|
|
|
_onProgress: function (request, progress, progressMax) {
|
|
if (this._data && this._data.onProgress) {
|
|
this._data.onProgress(request, progress, progressMax);
|
|
}
|
|
},
|
|
|
|
_onDone: function (request, status) {
|
|
var cancelled = status == 0x804b0002; // NS_BINDING_ABORTED
|
|
|
|
if (!cancelled && request instanceof Components.interfaces.nsIHttpChannel) {
|
|
request.QueryInterface(Components.interfaces.nsIHttpChannel);
|
|
status = request.responseStatus;
|
|
request.QueryInterface(Components.interfaces.nsIRequest);
|
|
}
|
|
|
|
if (this._data.streams) {
|
|
for each(var stream in this._data.streams) {
|
|
stream.close();
|
|
}
|
|
}
|
|
|
|
var data = this._getPassData();
|
|
|
|
if (cancelled) {
|
|
if (this._data.onCancel) {
|
|
this._data.onCancel(request, status, data);
|
|
}
|
|
}
|
|
else {
|
|
if (this._data.onStop) {
|
|
this._data.onStop(request, status, this._response, data);
|
|
}
|
|
}
|
|
|
|
this._channel = null;
|
|
},
|
|
|
|
_getPassData: function () {
|
|
// Make copy of data without callbacks to pass along
|
|
var passData = {};
|
|
for (var i in this._data) {
|
|
switch (i) {
|
|
case "onStart":
|
|
case "onProgress":
|
|
case "onStop":
|
|
case "onCancel":
|
|
continue;
|
|
}
|
|
passData[i] = this._data[i];
|
|
}
|
|
return passData;
|
|
},
|
|
|
|
// nsIInterfaceRequestor
|
|
getInterface: function (iid) {
|
|
try {
|
|
return this.QueryInterface(iid);
|
|
}
|
|
catch (e) {
|
|
throw Components.results.NS_NOINTERFACE;
|
|
}
|
|
},
|
|
|
|
QueryInterface: function(iid) {
|
|
if (iid.equals(Components.interfaces.nsISupports) ||
|
|
iid.equals(Components.interfaces.nsIInterfaceRequestor) ||
|
|
iid.equals(Components.interfaces.nsIChannelEventSink) ||
|
|
iid.equals(Components.interfaces.nsIProgressEventSink) ||
|
|
iid.equals(Components.interfaces.nsIHttpEventSink) ||
|
|
iid.equals(Components.interfaces.nsIStreamListener) ||
|
|
iid.equals(Components.interfaces.nsIWebProgressListener)) {
|
|
return this;
|
|
}
|
|
throw Components.results.NS_NOINTERFACE;
|
|
},
|
|
|
|
_safeSpec: function (uri) {
|
|
return uri.scheme + '://' + uri.username + ':********@'
|
|
+ uri.hostPort + uri.path
|
|
},
|
|
};
|
|
|