/)) {
note = Zotero.Utilities.htmlSpecialChars(note);
- note = '
'
+ note = Zotero.Notes.notePrefix + '
'
+ note.replace(/\n/g, '
')
.replace(/\t/g, ' ')
.replace(/ /g, ' ')
- + '
';
+ + '' + Zotero.Notes.noteSuffix;
note = note.replace(/
\s*<\/p>/g, '
');
var sql = "UPDATE itemNotes SET note=? WHERE itemID=?";
Zotero.DB.query(sql, [note, this.id]);
@@ -2410,7 +2478,6 @@ Zotero.Item.prototype.getNote = function() {
}
this._noteText = note ? note : '';
-
return this._noteText;
}
@@ -2441,6 +2508,7 @@ Zotero.Item.prototype.setNote = function(text) {
this._previousData = this.serialize();
}
+ this._hasNote = text !== '';
this._noteText = text;
this._noteTitle = Zotero.Notes.noteToTitle(text);
this._changedNote = true;
@@ -2547,6 +2615,14 @@ Zotero.Item.prototype.isWebAttachment = function() {
}
+Zotero.Item.prototype.isFileAttachment = function() {
+ if (!this.isAttachment()) {
+ return false;
+ }
+ return this.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL;
+}
+
+
/**
* Returns number of child attachments of item
*
@@ -2594,6 +2670,18 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
};
}
+ // Update file existence state of this item
+ // and best attachment state of parent item
+ var self = this;
+ var updateAttachmentStates = function (exists) {
+ self._fileExists = exists;
+
+ if (self.isTopLevelItem()) {
+ return;
+ }
+ Zotero.Items.get(self.getSource()).updateBestAttachmentState();
+ };
+
// No associated files for linked URLs
if (row.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
return false;
@@ -2601,6 +2689,7 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
if (!row.path) {
Zotero.debug("Attachment path is empty", 2);
+ updateAttachmentStates(false);
return false;
}
@@ -2648,6 +2737,7 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
}
catch (e) {
Zotero.debug('Invalid persistent descriptor', 2);
+ updateAttachmentStates(false);
return false;
}
}
@@ -2674,6 +2764,7 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
}
catch (e) {
Zotero.debug('Invalid relative descriptor', 2);
+ updateAttachmentStates(false);
return false;
}
}
@@ -2681,9 +2772,11 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
if (!skipExistsCheck && !file.exists()) {
Zotero.debug("Attachment file '" + file.path + "' not found", 2);
+ updateAttachmentStates(false);
return false;
}
+ updateAttachmentStates(true);
return file;
}
@@ -2706,6 +2799,19 @@ Zotero.Item.prototype.getFilename = function () {
}
+/**
+ * Cached check for file existence, used for items view
+ *
+ * This is updated only initially and on subsequent getFile() calls.
+ */
+Zotero.Item.prototype.__defineGetter__('fileExists', function () {
+ if (this._fileExists === null) {
+ this.getFile();
+ }
+ return this._fileExists;
+});
+
+
/*
* Rename file associated with an attachment
*
@@ -3304,7 +3410,7 @@ Zotero.Item.prototype.getBestSnapshot = function () {
}
-/*
+/**
* Looks for attachment in the following order: oldest PDF attachment matching parent URL,
* oldest non-PDF attachment matching parent URL, oldest PDF attachment not matching URL,
* old non-PDF attachment not matching URL
@@ -3315,15 +3421,36 @@ Zotero.Item.prototype.getBestAttachment = function() {
if (!this.isRegularItem()) {
throw ("getBestAttachment() can only be called on regular items");
}
- return this.getBestAttachments()[0];
+ var attachments = this.getBestAttachments();
+ return attachments ? attachments[0] : false;
}
-/*
+/**
+ * Returned cached state of best attachment for use in items view
+ *
+ * @return {Integer} 0 (none), 1 (present), -1 (missing)
+ */
+Zotero.Item.prototype.getBestAttachmentState = function () {
+ if (this._bestAttachmentState !== null) {
+ return this._bestAttachmentState;
+ }
+ var itemID = this.getBestAttachment();
+ this._bestAttachmentState = itemID ? (Zotero.Items.get(itemID).fileExists ? 1 : -1) : 0;
+ return this._bestAttachmentState;
+}
+
+
+Zotero.Item.prototype.updateBestAttachmentState = function () {
+ this._bestAttachmentState = null;
+}
+
+
+/**
* Looks for attachment in the following order: oldest PDF attachment matching parent URL,
* oldest PDF attachment not matching parent URL, oldest non-PDF attachment matching parent URL,
* old non-PDF attachment not matching parent URL
*
- * @return {Array} itemIDs for attachments
+ * @return {Array|FALSE} itemIDs for attachments, or FALSE if none
*/
Zotero.Item.prototype.getBestAttachments = function() {
if (!this.isRegularItem()) {
diff --git a/chrome/content/zotero/xpcom/data/notes.js b/chrome/content/zotero/xpcom/data/notes.js
index 481d449f5d..88a2599c33 100644
--- a/chrome/content/zotero/xpcom/data/notes.js
+++ b/chrome/content/zotero/xpcom/data/notes.js
@@ -28,6 +28,9 @@ Zotero.Notes = new function() {
this.noteToTitle = noteToTitle;
this.__defineGetter__("MAX_TITLE_LENGTH", function() { return 80; });
+ this.__defineGetter__("defaultNote", function () '
');
+ this.__defineGetter__("notePrefix", function () '
');
+ this.__defineGetter__("noteSuffix", function () '
');
/**
* Return first line (or first MAX_LENGTH characters) of note content
diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js
index a312e5fa4a..8a6173e0b7 100644
--- a/chrome/content/zotero/xpcom/itemTreeView.js
+++ b/chrome/content/zotero/xpcom/itemTreeView.js
@@ -245,8 +245,14 @@ Zotero.ItemTreeView.prototype._refreshGenerator = function()
for (var i=0; i
0) {
- val = c;
- }
+ // Image only
+ if (column.id === "zotero-items-column-hasAttachment" || column.id === "zotero-items-column-hasNote") {
+ return;
}
else if(column.id == "zotero-items-column-type")
{
@@ -791,6 +793,47 @@ Zotero.ItemTreeView.prototype.getImageSrc = function(row, col)
{
return this._getItemAtRow(row).ref.getImageSrc();
}
+ else if (col.id == 'zotero-items-column-hasAttachment') {
+ if (this._itemGroup.isTrash()) return false;
+
+ var treerow = this._getItemAtRow(row);
+ if (treerow.level === 0) {
+ if (treerow.ref.isRegularItem()) {
+ switch (treerow.ref.getBestAttachmentState()) {
+ case 1:
+ return "chrome://zotero/skin/bullet_blue.png";
+
+ case -1:
+ return "chrome://zotero/skin/bullet_blue_empty.png";
+
+ default:
+ return "";
+ }
+ }
+ }
+
+ if (treerow.ref.isFileAttachment()) {
+ if (treerow.ref.fileExists) {
+ return "chrome://zotero/skin/bullet_blue.png";
+ }
+ else {
+ return "chrome://zotero/skin/bullet_blue_empty.png";
+ }
+ }
+ }
+ else if (col.id == 'zotero-items-column-hasNote') {
+ if (this._itemGroup.isTrash()) return false;
+
+ var treerow = this._getItemAtRow(row);
+ if (treerow.level === 0 && treerow.ref.isRegularItem() && treerow.ref.numNotes(false, true)) {
+ return "chrome://zotero/skin/bullet_yellow.png";
+ }
+ else if (treerow.ref.isAttachment()) {
+ if (treerow.ref.hasNote()) {
+ return "chrome://zotero/skin/bullet_yellow.png";
+ }
+ }
+ }
}
Zotero.ItemTreeView.prototype.isContainer = function(row)
@@ -805,13 +848,16 @@ Zotero.ItemTreeView.prototype.isContainerOpen = function(row)
Zotero.ItemTreeView.prototype.isContainerEmpty = function(row)
{
- if(this._sourcesOnly) {
+ if (this._sourcesOnly) {
return true;
- } else {
- var includeTrashed = this._itemGroup.isTrash();
- return (this._getItemAtRow(row).numNotes(includeTrashed) == 0
- && this._getItemAtRow(row).numAttachments(includeTrashed) == 0);
}
+
+ var item = this._getItemAtRow(row).ref;
+ if (!item.isRegularItem()) {
+ return false;
+ }
+ var includeTrashed = this._itemGroup.isTrash();
+ return item.numNotes(includeTrashed) === 0 && item.numAttachments(includeTrashed) == 0;
}
Zotero.ItemTreeView.prototype.getLevel = function(row)
@@ -1010,25 +1056,51 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
// Get the display field for a row (which might be a placeholder title)
var getField;
- if (columnField == 'title') {
- getField = function (row) {
- var field;
- var type = row.ref.itemTypeID;
- switch (type) {
- case 8: // letter
- case 10: // interview
- case 17: // case
- field = row.ref.getDisplayTitle();
- break;
-
- default:
- field = row.getField(columnField, unformatted);
- }
- // Ignore some leading and trailing characters when sorting
- return Zotero.Items.getSortTitle(field);
- }
- } else {
- getField = function(row) row.getField(columnField, unformatted);
+ switch (columnField) {
+ case 'title':
+ getField = function (row) {
+ var field;
+ var type = row.ref.itemTypeID;
+ switch (type) {
+ case 8: // letter
+ case 10: // interview
+ case 17: // case
+ field = row.ref.getDisplayTitle();
+ break;
+
+ default:
+ field = row.getField(columnField, unformatted);
+ }
+ // Ignore some leading and trailing characters when sorting
+ return Zotero.Items.getSortTitle(field);
+ };
+ break;
+
+ case 'hasAttachment':
+ getField = function (row) {
+ if (!row.ref.isRegularItem()) {
+ return 0;
+ }
+ var state = row.ref.getBestAttachmentState();
+ // Make sort order present, missing, empty when ascending
+ if (state === -1) {
+ state = 2;
+ }
+ return state * -1;
+ };
+ break;
+
+ case 'hasNote':
+ getField = function (row) {
+ if (!row.ref.isRegularItem()) {
+ return 0;
+ }
+ return row.ref.numNotes(false, true) ? 1 : 0;
+ };
+ break;
+
+ default:
+ getField = function (row) row.getField(columnField, unformatted);
}
var includeTrashed = this._itemGroup.isTrash();
@@ -1074,13 +1146,6 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
}
break;
- case 'numChildren':
- cmp = b.numChildren(includeTrashed) - a.numChildren(includeTrashed);
- if (cmp) {
- return cmp;
- }
- break;
-
default:
if (fieldA == undefined) {
fieldA = getField(a);
@@ -2613,17 +2678,16 @@ Zotero.ItemTreeView.prototype.onDragExit = function (event) {
Zotero.ItemTreeView.prototype.isSeparator = function(row) { return false; }
Zotero.ItemTreeView.prototype.getRowProperties = function(row, prop) {
- if (!this.selection.isSelected(row)) {
- return;
- }
-
- var itemID = this._getItemAtRow(row).ref.id;
+ var treeRow = this._getItemAtRow(row);
+ var itemID = treeRow.ref.id;
// Set background color for selected items with colored tags
- if (color = Zotero.Tags.getItemColor(itemID)) {
- var aServ = Components.classes["@mozilla.org/atom-service;1"].
- getService(Components.interfaces.nsIAtomService);
- prop.AppendElement(aServ.getAtom("color" + color.substr(1)));
+ if (this.selection.isSelected(row)) {
+ if (color = Zotero.Tags.getItemColor(itemID)) {
+ var aServ = Components.classes["@mozilla.org/atom-service;1"].
+ getService(Components.interfaces.nsIAtomService);
+ prop.AppendElement(aServ.getAtom("color" + color.substr(1)));
+ }
}
}
Zotero.ItemTreeView.prototype.getColumnProperties = function(col, prop) { }
@@ -2663,27 +2727,3 @@ Zotero.ItemTreeView.TreeRow.prototype.getField = function(field, unformatted)
{
return this.ref.getField(field, unformatted, true);
}
-
-Zotero.ItemTreeView.TreeRow.prototype.numChildren = function(includeTrashed)
-{
- if(this.ref.isRegularItem())
- return this.ref.numChildren(includeTrashed);
- else
- return 0;
-}
-
-Zotero.ItemTreeView.TreeRow.prototype.numNotes = function(includeTrashed)
-{
- if(this.ref.isRegularItem())
- return this.ref.numNotes(includeTrashed);
- else
- return 0;
-}
-
-Zotero.ItemTreeView.TreeRow.prototype.numAttachments = function(includeTrashed)
-{
- if(this.ref.isRegularItem())
- return this.ref.numAttachments(includeTrashed);
- else
- return 0;
-}
diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js
index 4d14a1868b..b46be25b7d 100644
--- a/chrome/content/zotero/xpcom/schema.js
+++ b/chrome/content/zotero/xpcom/schema.js
@@ -225,6 +225,7 @@ Zotero.Schema = new function(){
finally {
Zotero.UnresponsiveScriptIndicator.enable();
}
+
return up1 || up2 || up3 || up4;
}
diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js
index b1142072b1..e80e22fdfa 100644
--- a/chrome/content/zotero/xpcom/storage.js
+++ b/chrome/content/zotero/xpcom/storage.js
@@ -81,120 +81,133 @@ Zotero.Sync.Storage = new function () {
var _syncInProgress;
var _updatesInProgress;
var _changesMade;
+ var _resyncOnFinish;
- 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];
- }
+ this.sync = function (moduleName, observer) {
+ var module = getModuleFromName(moduleName);
- _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 (!observer) {
+ throw new Error("Observer not provided");
}
+ registerDefaultObserver(moduleName);
+ Zotero.Sync.Storage.EventManager.registerObserver(observer, true, moduleName);
- 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");
-
- var e = new Zotero.Error(
- Zotero.getString('sync.storage.error.verificationFailed', _session.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');
- }
- }
- );
- _callbacks.onError(e);
- }
+ if (!module.active) {
+ if (!module.enabled) {
+ Zotero.debug(module.name + " file sync is not enabled");
+ Zotero.Sync.Storage.EventManager.skip();
+ return;
}
- _session.checkServer(callback);
+ Zotero.debug(module.name + " file sync is not active");
+
+ // Try to verify server now if it hasn't been
+ if (!module.verified) {
+ module.checkServer(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 = module.checkServerCallback(uri, status, lastWin, true);
+ if (success) {
+ Zotero.debug(module.name + " file sync is successfully set up");
+ Zotero.Sync.Storage.sync(module.name);
+ }
+ else {
+ Zotero.debug(module.name + " verification failed");
+
+ var e = new Zotero.Error(
+ Zotero.getString('sync.storage.error.verificationFailed', module.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');
+ }
+ }
+ );
+ Zotero.Sync.Storage.EventManager.error(e, true);
+ }
+ });
+ }
+
+ return;
+ }
+
+ if ((!module.includeUserFiles || !Zotero.Sync.Storage.downloadOnSync())
+ && (!module.includeGroupFiles || !Zotero.Sync.Storage.downloadOnSync('groups'))) {
+ Zotero.debug("No libraries are enabled for on-sync downloading");
+ Zotero.Sync.Storage.EventManager.skip();
return;
}
if (_syncInProgress) {
- _error("File sync operation already in progress");
+ Zotero.Sync.Storage.EventManager.error(
+ "File sync operation already in progress"
+ );
}
- Zotero.debug("Beginning " + _session.name + " file sync");
+ Zotero.debug("Beginning " + module.name + " file sync");
_syncInProgress = true;
_changesMade = false;
try {
- Zotero.Sync.Storage.checkForUpdatedFiles(null, null, _session.includeUserFiles, _session.includeGroupFiles);
+ Zotero.Sync.Storage.checkForUpdatedFiles(
+ null,
+ null,
+ module.includeUserFiles && Zotero.Sync.Storage.downloadOnSync(),
+ module.includeGroupFiles && Zotero.Sync.Storage.downloadOnSync('groups')
+ );
}
catch (e) {
- _syncInProgress = false;
- throw (e);
+ Zotero.Sync.Storage.EventManager.error(e);
}
- var lastSyncCheckCallback = function (lastSyncTime) {
- var downloadFiles = true;
+ var self = this;
+
+ module.getLastSyncTime(function (lastSyncTime) {
+ // Register the observers again to make sure they're active when we
+ // start the queues. (They'll only be registered once.) Observers are
+ // cleared when all queues finish, so without this another sync
+ // process (e.g., on-demand download) could finish and clear all
+ // observers while getLastSyncTime() is running.
+ registerDefaultObserver(moduleName);
+ Zotero.Sync.Storage.EventManager.registerObserver(observer, true, moduleName);
+
+ var download = 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 sql = "SELECT version FROM version WHERE schema='storage_" + moduleName + "'";
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;
+ Zotero.debug("Last " + module.name + " sync time hasn't changed -- skipping file download step");
+ download = false;
}
}
- var activeDown = downloadFiles ? Zotero.Sync.Storage.downloadFiles() : false;
- var activeUp = Zotero.Sync.Storage.uploadFiles();
- if (!activeDown && !activeUp) {
- _syncInProgress = false;
- _callbacks.onSkip();
+ try {
+ var activeDown = download ? _downloadFiles(module) : false;
+ var activeUp = _uploadFiles(module);
}
- };
-
- _session.getLastSyncTime(lastSyncCheckCallback);
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+
+ if (!activeDown && !activeUp) {
+ Zotero.Sync.Storage.EventManager.skip();
+ return;
+ }
+ });
}
@@ -221,8 +234,10 @@ Zotero.Sync.Storage = new function () {
break;
default:
- _error("Invalid sync state '" + syncState
- + "' in Zotero.Sync.Storage.setSyncState()");
+ Zotero.Sync.Storage.EventManager.error(
+ "Invalid sync state '" + syncState
+ + "' in Zotero.Sync.Storage.setSyncState()"
+ );
}
var sql = "UPDATE itemAttachments SET syncState=? WHERE itemID=?";
@@ -239,8 +254,9 @@ Zotero.Sync.Storage = new function () {
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()");
+ Zotero.Sync.Storage.EventManager.error(
+ "Item " + itemID + " not found in Zotero.Sync.Storage.getSyncedModificationTime()"
+ );
}
return mtime;
}
@@ -283,8 +299,9 @@ Zotero.Sync.Storage = new function () {
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()");
+ Zotero.Sync.Storage.EventManager.error(
+ "Item " + itemID + " not found in Zotero.Sync.Storage.getSyncedHash()"
+ );
}
return hash;
}
@@ -353,6 +370,37 @@ Zotero.Sync.Storage = new function () {
}
+ /**
+ * @param {NULL|Integer|'groups'} [libraryID]
+ */
+ this.downloadAsNeeded = function (libraryID) {
+ // Personal library
+ if (libraryID == null) {
+ 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 {NULL|Integer|'groups'} [libraryID]
+ */
+ this.downloadOnSync = function (libraryID) {
+ // Personal library
+ if (libraryID == null) {
+ 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 as 0 for uploading
* and any that are missing as 1 for downloading
@@ -373,24 +421,22 @@ Zotero.Sync.Storage = new function () {
* 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);
+ throw new Error("includeUserFiles and includeGroupFiles are not allowed when itemIDs");
}
}
else {
if (!includeUserFiles && !includeGroupFiles) {
- _error("At least one of includeUserFiles or includeGroupFiles must be set in " + funcName);
+ return false;
}
}
if (itemModTimes && !itemIDs) {
- _error("itemModTimes can only be set if itemIDs is an array in " + funcName);
+ throw new Error("itemModTimes can only be set if itemIDs is an array");
}
var changed = false;
@@ -569,47 +615,95 @@ Zotero.Sync.Storage = new function () {
/**
- * Starts download of all attachments marked for download
+ * Download a single file
*
- * @return {Boolean}
+ * If no queue is active, start one. Otherwise, add to existing queue.
*/
- this.downloadFiles = function () {
- if (!_syncInProgress) {
- _syncInProgress = true;
- }
+ this.downloadFile = function (item, requestCallbacks) {
+ var itemID = item.id;
+ var module = getModuleFromLibrary(item.libraryID);
- var downloadFileIDs = _getFilesToDownload(_session.includeUserFiles, _session.includeGroupFiles);
- if (!downloadFileIDs) {
- Zotero.debug("No files to download");
+ if (!module || !module.active) {
+ Zotero.debug("File syncing is not active for item's library -- skipping 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()");
+ if (!item.isImportedAttachment()) {
+ throw new Error("Not an imported attachment");
}
- 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;
+ if (item.getFile()) {
+ Zotero.debug("File already exists -- replacing");
+ }
+
+ var setup = function () {
+ Zotero.Sync.Storage.EventManager.registerObserver({
+ onSuccess: function () _syncInProgress = false,
+
+ onSkip: function () _syncInProgress = false,
+
+ onStop: function () _syncInProgress = false,
+
+ onError: function (e) {
+ Zotero.Sync.Runner.setSyncIcon('error', e);
+ error(e);
+ requestCallbacks.onStop();
+ }
+ }, false, "downloadFile");
+
+ try {
+ var queue = Zotero.Sync.Storage.QueueManager.get('download');
+
+ var isRunning = queue.isRunning();
+ if (!isRunning) {
+ _syncInProgress = true;
+
+ // Reset the sync icon
+ Zotero.Sync.Runner.setSyncIcon();
+ }
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
}
- var request = new Zotero.Sync.Storage.Request(
- item.libraryID + '/' + item.key, function (request) { _session.downloadFile(request); }
- );
- queue.addRequest(request);
- }
+ return isRunning;
+ };
+
+ var run = function () {
+ // We have to perform setup again at the same time that we add the
+ // request, because otherwise a sync process could complete while
+ // cacheCredentials() is running and clear the event handlers.
+ var isRunning = setup();
+
+ try {
+ var queue = Zotero.Sync.Storage.QueueManager.get('download');
+
+ if (!requestCallbacks) {
+ requestCallbacks = {};
+ }
+ var onStart = function (request) {
+ module.downloadFile(request);
+ };
+ requestCallbacks.onStart = requestCallbacks.onStart
+ ? [onStart, requestCallbacks.onStart]
+ : onStart;
+
+ var request = new Zotero.Sync.Storage.Request(
+ item.libraryID + '/' + item.key, requestCallbacks
+ );
+
+ queue.addRequest(request, isRunning);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+ };
+
+ setup();
+ module.cacheCredentials(function () {
+ run();
+ });
- // Start downloads
- queue.start();
return true;
}
@@ -625,19 +719,19 @@ Zotero.Sync.Storage = new function () {
var funcName = "Zotero.Sync.Storage.processDownload()";
if (!data) {
- _error("|data| not set in " + funcName);
+ Zotero.Sync.Storage.EventManager.error("|data| not set in " + funcName);
}
if (!data.item) {
- _error("|data.item| not set in " + funcName);
+ Zotero.Sync.Storage.EventManager.error("|data.item| not set in " + funcName);
}
if (!data.syncModTime) {
- _error("|data.syncModTime| not set in " + funcName);
+ Zotero.Sync.Storage.EventManager.error("|data.syncModTime| not set in " + funcName);
}
if (!data.compressed && !data.syncHash) {
- _error("|data.syncHash| is required if |data.compressed| is false in " + funcName);
+ Zotero.Sync.Storage.EventManager.error("|data.syncHash| is required if |data.compressed| is false in " + funcName);
}
var item = data.item;
@@ -705,7 +799,7 @@ Zotero.Sync.Storage = new function () {
// 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;
+ _resyncOnFinish = true;
}
else {
file.lastModifiedTime = syncModTime;
@@ -727,99 +821,52 @@ Zotero.Sync.Storage = new function () {
}
- /**
- * 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);
- }
+ this.checkServer = function (moduleName, callback) {
+ var module = getModuleFromName(moduleName);
+ Zotero.Sync.Storage.EventManager.registerObserver({
+ onSuccess: function () {},
+ onError: function (e) {
+ Zotero.debug(e, 1);
+ callback(null, null, e);
+ throw (e);
}
- );
- _session.initFromPrefs();
- return _session.checkServer(callback);
+ });
+ return module.checkServer(function (uri, status) {
+ callback(uri, status, module.checkServerCallback);
+ });
}
- this.checkServerCallback = function (uri, status, window, skipSuccessMessage, e) {
- return _session.checkServerCallback(uri, status, window, skipSuccessMessage, e);
+ this.purgeDeletedStorageFiles = function (moduleName, callback) {
+ var module = getModuleFromName(moduleName);
+ if (!module.active) {
+ return;
+ }
+ Zotero.Sync.Storage.EventManager.registerObserver({
+ onError: function (e) {
+ error(e);
+ }
+ });
+ module.purgeDeletedStorageFiles(callback);
}
- this.purgeDeletedStorageFiles = function (module, callback) {
- var session = new Zotero.Sync.Storage.Session(module, { onError: _error });
- if (!this.active) {
+ this.purgeOrphanedStorageFiles = function (moduleName, callback) {
+ var module = getModuleFromName(moduleName);
+ if (!module.active) {
return;
}
- if (!session.initFromPrefs()) {
- return;
- }
- session.purgeDeletedStorageFiles(callback);
+ Zotero.Sync.Storage.EventManager.registerObserver({
+ onError: function (e) {
+ error(e);
+ }
+ });
+ module.purgeOrphanedStorageFiles(callback);
}
- this.purgeOrphanedStorageFiles = function (module, callback) {
- var session = new Zotero.Sync.Storage.Session(module, { onError: _error });
- if (!this.active) {
- return;
- }
- if (!session.initFromPrefs()) {
- return;
- }
- session.purgeOrphanedStorageFiles(callback);
- }
-
-
- this.isActive = function (module) {
- var session = new Zotero.Sync.Storage.Session(module, { onError: _error });
- if (!session.initFromPrefs()) {
- return;
- }
- return session.active;
+ this.isActive = function (moduleName) {
+ return getModuleFromName(moduleName).active;
}
@@ -871,6 +918,135 @@ Zotero.Sync.Storage = new function () {
//
// Private methods
//
+ function getModuleFromName(moduleName) {
+ return new Zotero.Sync.Storage.Module(moduleName);
+ }
+
+
+ function getModuleFromLibrary(libraryID) {
+ if (libraryID === undefined) {
+ throw new Error("libraryID not provided");
+ }
+
+ // Personal library
+ if (libraryID === null) {
+ if (!Zotero.Prefs.get('sync.storage.enabled')) {
+ Zotero.debug('disabled');
+ return false;
+ }
+
+ var protocol = Zotero.Prefs.get('sync.storage.protocol');
+ switch (protocol) {
+ case 'zotero':
+ return getModuleFromName('ZFS');
+
+ case 'webdav':
+ return getModuleFromName('WebDAV');
+
+ default:
+ throw new Error("Invalid storage protocol '" + protocol + "'");
+ }
+ }
+
+ // Group library
+ else {
+ if (!Zotero.Prefs.get('sync.storage.groups.enabled')) {
+ return false;
+ }
+
+ return getModuleFromName('ZFS');
+ }
+ }
+
+
+ /**
+ * Starts download of all attachments marked for download
+ *
+ * @return {Boolean}
+ */
+ function _downloadFiles(module) {
+ if (!_syncInProgress) {
+ _syncInProgress = true;
+ }
+
+ var downloadFileIDs = _getFilesToDownload(
+ module.includeUserFiles && Zotero.Sync.Storage.downloadOnSync(),
+ module.includeGroupFiles && Zotero.Sync.Storage.downloadOnSync('groups')
+ );
+ if (!downloadFileIDs) {
+ Zotero.debug("No files to download");
+ return false;
+ }
+
+ // Check for active operations?
+
+ var queue = Zotero.Sync.Storage.QueueManager.get('download');
+
+ 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
+ && Zotero.Sync.Storage.isFileModified(itemID)) {
+ Zotero.debug("File for attachment " + itemID + " has been modified");
+ Zotero.Sync.Storage.setSyncState(itemID, this.SYNC_STATE_TO_UPLOAD);
+ continue;
+ }
+
+ var request = new Zotero.Sync.Storage.Request(
+ item.libraryID + '/' + item.key,
+ {
+ onStart: function (request) {
+ module.downloadFile(request);
+ }
+ }
+ );
+ queue.addRequest(request);
+ }
+
+ return true;
+ }
+
+
+ /**
+ * Start upload of all attachments marked for upload
+ *
+ * @return {Boolean}
+ */
+ function _uploadFiles(module) {
+ if (!_syncInProgress) {
+ _syncInProgress = true;
+ }
+
+ var uploadFileIDs = _getFilesToUpload(module.includeUserFiles, module.includeGroupFiles);
+ if (!uploadFileIDs) {
+ Zotero.debug("No files to upload");
+ return false;
+ }
+
+ // Check for active operations?
+ var queue = Zotero.Sync.Storage.QueueManager.get('upload');
+
+ 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,
+ {
+ onStart: function (request) {
+ module.uploadFile(request);
+ }
+ }
+ );
+ request.progressMax = Zotero.Attachments.getTotalFileSize(item, true);
+ queue.addRequest(request);
+ }
+
+ return true;
+ }
+
+
function _processDownload(item) {
var funcName = "Zotero.Sync.Storage._processDownload()";
@@ -1401,8 +1577,10 @@ Zotero.Sync.Storage = new function () {
*/
function _getFilesToDownload(includeUserFiles, includeGroupFiles) {
if (!includeUserFiles && !includeGroupFiles) {
- _error("At least one of includeUserFiles or includeGroupFiles must be set "
- + "in Zotero.Sync.Storage._getFilesToDownload()");
+ Zotero.Sync.Storage.EventManager.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) "
@@ -1432,8 +1610,10 @@ Zotero.Sync.Storage = new function () {
*/
function _getFilesToUpload(includeUserFiles, includeGroupFiles) {
if (!includeUserFiles && !includeGroupFiles) {
- _error("At least one of includeUserFiles or includeGroupFiles must be set "
- + "in Zotero.Sync.Storage._getFilesToUpload()");
+ Zotero.Sync.Storage.EventManager.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) "
@@ -1474,63 +1654,67 @@ Zotero.Sync.Storage = new function () {
}
- 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;
+ function registerDefaultObserver(moduleName) {
+ var finish = function (cancelled, skipSuccessFile) {
+ // Upload success file when done
+ if (!_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', true);
+ var useLastSyncTime = !uploadQueue || (!cancelled && uploadQueue.lastTotalRequests == 0);
+
+ getModuleFromName(moduleName).setLastSyncTime(function () {
+ finish(cancelled, true);
+ }, useLastSyncTime);
+ return false;
+ }
- _session.setLastSyncTime(function () {
- Zotero.Sync.Storage.finish(cancelled, true);
- }, useLastSyncTime);
- return;
- }
+ Zotero.debug(moduleName + " sync is complete");
+
+ _syncInProgress = false;
+
+ if (_resyncOnFinish) {
+ Zotero.debug("Force-resyncing items in conflict");
+ _resyncOnFinish = false;
+ Zotero.Sync.Storage.sync(moduleName);
+ return false;
+ }
+
+ if (cancelled) {
+ Zotero.Sync.Storage.EventManager.stop();
+ }
+ else if (!_changesMade) {
+ Zotero.debug("No changes made during storage sync");
+ Zotero.Sync.Storage.EventManager.skip();
+ }
+ else {
+ Zotero.Sync.Storage.EventManager.success();
+ }
+
+ return true;
+ };
- 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();
+ Zotero.Sync.Storage.EventManager.registerObserver({
+ onSuccess: function () finish(),
+
+ onSkip: function () function () {
+ _syncInProgress = false
+ },
+
+ onStop: function () finish(true),
+
+ onError: function (e) error(e),
+
+ onChangesMade: function () _changesMade = true
+ }, false, "default");
}
- //
- // Stop requests, log error, and
- //
- function _error(e) {
+ function error(e) {
if (_syncInProgress) {
Zotero.Sync.Storage.QueueManager.cancel(true);
_syncInProgress = false;
- _session = null;
}
Zotero.DB.rollbackAllTransactions();
@@ -1542,12 +1726,7 @@ Zotero.Sync.Storage = new function () {
e = Zotero.Sync.Storage.defaultError;
}
- // 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) {
+ if (e.error && e.error == Zotero.Error.ERROR_ZFS_FILE_EDITING_DENIED) {
setTimeout(function () {
var group = Zotero.Groups.get(e.data.groupID);
@@ -1575,808 +1754,11 @@ Zotero.Sync.Storage = new function () {
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();
- if (!win.ZoteroPane) continue;
- var doc = win.ZoteroPane.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.numberFormat(remaining / 1024, 0)
- );
- var totalRequests = queue.totalRequests;
- var filesRemaining = Zotero.getString(
- 'sync.storage.filesRemaining',
- [totalRequests - unfinishedRequests, totalRequests]
- );
- 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), true));
- var item2 = item.clone(false, false, true);
- item2.setField('dateModified',
- Zotero.Date.dateToSQL(new Date(conflict.remoteData.modTime), true));
- objectPairs.push([item1, item2]);
- }
-
- var io = {
- dataIn: {
- type: 'storagefile',
- captions: [
- Zotero.getString('sync.storage.localFile'),
- Zotero.getString('sync.storage.remoteFile'),
- Zotero.getString('sync.storage.savedFile')
- ],
- 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) {
- Zotero.debug("Trying to update finished request " + this.name + " in "
- + "Zotero.Sync.Storage.Request.onProgress() "
- + "(" + progress + "/" + progressMax + ")", 2);
- return;
- }
-
- 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;
- }
- }
- catch (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
*
@@ -2429,215 +1811,3 @@ Zotero.Sync.Storage.ZipWriterObserver.prototype = {
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('onChannelRedirect');
-
- // if redirecting, store the new channel
- this._channel = newChannel;
- },
-
- asyncOnChannelRedirect: function (oldChan, newChan, flags, redirectCallback) {
- Zotero.debug('asyncOnRedirect');
-
- this.onChannelRedirect(oldChan, newChan, flags);
- redirectCallback.onRedirectVerifyCallback(0);
- },
-
- // 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
- },
-};
-
diff --git a/chrome/content/zotero/xpcom/storage/eventManager.js b/chrome/content/zotero/xpcom/storage/eventManager.js
new file mode 100644
index 0000000000..d0ac3f4606
--- /dev/null
+++ b/chrome/content/zotero/xpcom/storage/eventManager.js
@@ -0,0 +1,143 @@
+/*
+ ***** 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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+Zotero.Sync.Storage.EventManager = (function () {
+ var _observers = [];
+
+ function call(handler, data, clear) {
+ Zotero.debug("Calling storage sync " + handler + " handlers");
+
+ var observers = _observers;
+ var cont = true;
+ var handled = false;
+
+ if (clear) {
+ Zotero.Sync.Storage.EventManager.clear();
+ }
+
+ // Process most recently assigned observers first
+ for (var i = observers.length - 1; i >= 0; i--) {
+ let observer = observers[i].observer;
+ let j = i;
+ if (observer[handler]) {
+ handled = true;
+ if (observers[i].async) {
+ setTimeout(function () {
+ Zotero.debug("Calling " + handler + " handler " + j);
+ var cont = observer[handler](data);
+ if (cont === false) {
+ throw new Error("Cannot cancel events from async observer");
+ }
+ }, 0);
+ }
+ else {
+ Zotero.debug("Calling " + handler + " handler " + j);
+ var cont = observer[handler](data);
+ // If handler returns explicit false, cancel further events
+ if (cont === false) {
+ break;
+ }
+ }
+ }
+ }
+
+ if (!handled && data) {
+ var msg = "Unhandled storage sync event: " + data;
+ Zotero.debug(msg, 1);
+ if (handler == 'onError') {
+ throw new Error(msg);
+ }
+ else {
+ Components.utils.reportError(msg);
+ }
+ }
+
+ // Throw errors to stop execution
+ if (handler == 'onError') {
+ if (!data) {
+ throw new Error("Data not provided for error");
+ }
+
+ if (cont !== false) {
+ throw (data);
+ }
+ }
+ }
+
+ return {
+ registerObserver: function (observer, async, id) {
+ var pos = -1;
+
+ if (id) {
+ for (var i = 0, len = _observers.length; i < len; i++) {
+ var o = _observers[i];
+ if (o.id === id && o.async == async) {
+ pos = o;
+ break;
+ }
+ }
+ }
+
+ if (pos == -1) {
+ Zotero.debug("Registering storage sync event observer '" + id + "'");
+ _observers.push({
+ observer: observer,
+ async: !!async,
+ id: id
+ });
+ }
+ else {
+ Zotero.debug("Replacing storage sync event observer '" + id + "'");
+ _observers[pos] = {
+ observer: observer,
+ async: !!async,
+ id: id
+ };
+ }
+ },
+
+ success: function () call('onSuccess', false, true),
+ skip: function (clear) call('onSkip', false, true),
+ stop: function () call('onStop', false, true),
+ error: function (e) call('onError', e, true),
+
+ warning: function (e) call('onWarning', e),
+ changesMade: function () call('onChangesMade'),
+
+ clear: function () {
+ var queues = Zotero.Sync.Storage.QueueManager.getAll();
+ for each(var queue in queues) {
+ if (queue.isRunning()) {
+ Zotero.debug(queue[0].toUpperCase() + queue.substr(1)
+ + " queue not empty -- not clearing storage sync event observers");
+ return;
+ }
+ }
+
+ Zotero.debug("Clearing storage sync event observers");
+ _observers = [];
+ }
+ };
+}());
diff --git a/chrome/content/zotero/xpcom/storage/module.js b/chrome/content/zotero/xpcom/storage/module.js
new file mode 100644
index 0000000000..d410b8ccfb
--- /dev/null
+++ b/chrome/content/zotero/xpcom/storage/module.js
@@ -0,0 +1,198 @@
+/*
+ ***** 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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+
+Zotero.Sync.Storage.Module = function (moduleName) {
+ switch (moduleName) {
+ case 'ZFS':
+ this._module = Zotero.Sync.Storage.Module.ZFS;
+ break;
+
+ case 'WebDAV':
+ this._module = Zotero.Sync.Storage.Module.WebDAV;
+ break;
+
+ default:
+ throw ("Invalid storage session module '" + moduleName + "'");
+ }
+};
+
+Zotero.Sync.Storage.Module.prototype.__defineGetter__('name', function () this._module.name);
+Zotero.Sync.Storage.Module.prototype.__defineGetter__('includeUserFiles', function () this._module.includeUserFiles);
+Zotero.Sync.Storage.Module.prototype.__defineGetter__('includeGroupFiles', function () this._module.includeGroupFiles);
+
+Zotero.Sync.Storage.Module.prototype.__defineGetter__('enabled', function () {
+ try {
+ return this._module.enabled;
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+});
+
+Zotero.Sync.Storage.Module.prototype.__defineGetter__('verified', function () {
+ try {
+ return this._module.verified;
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+});
+
+Zotero.Sync.Storage.Module.prototype.__defineGetter__('active', function () {
+ try {
+ return this._module.enabled && this._module.initFromPrefs() && this._module.verified;
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+});
+
+Zotero.Sync.Storage.Module.prototype.__defineGetter__('username', function () {
+ try {
+ return this._module.username;
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+});
+
+Zotero.Sync.Storage.Module.prototype.__defineGetter__('password', function () {
+ try {
+ return this._module.password;
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+});
+
+Zotero.Sync.Storage.Module.prototype.__defineSetter__('password', function (val) {
+ try {
+ this._module.password = val;
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+});
+
+
+Zotero.Sync.Storage.Module.prototype.init = function () {
+ try {
+ return this._module.init();
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.initFromPrefs = function () {
+ try {
+ return this._module.initFromPrefs();
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.downloadFile = function (request) {
+ try {
+ this._module.downloadFile(request);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.uploadFile = function (request) {
+ try {
+ this._module.uploadFile(request);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.getLastSyncTime = function (callback) {
+ try {
+ this._module.getLastSyncTime(callback);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.setLastSyncTime = function (callback, useLastSyncTime) {
+ try {
+ this._module.setLastSyncTime(callback, useLastSyncTime);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.checkServer = function (callback) {
+ try {
+ return this._module.checkServer(callback);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.checkServerCallback = function (uri, status, window, skipSuccessMessage) {
+ try {
+ return this._module.checkServerCallback(uri, status, authRequired, window, skipSuccessMessage);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.cacheCredentials = function (callback) {
+ try {
+ return this._module.cacheCredentials(callback);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.purgeDeletedStorageFiles = function (callback) {
+ try {
+ this._module.purgeDeletedStorageFiles(callback);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
+
+Zotero.Sync.Storage.Module.prototype.purgeOrphanedStorageFiles = function (callback) {
+ try {
+ this._module.purgeOrphanedStorageFiles(callback);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+}
diff --git a/chrome/content/zotero/xpcom/storage/queue.js b/chrome/content/zotero/xpcom/storage/queue.js
new file mode 100644
index 0000000000..df5ff5a4b9
--- /dev/null
+++ b/chrome/content/zotero/xpcom/storage/queue.js
@@ -0,0 +1,258 @@
+/*
+ ***** 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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+/**
+ * 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.maxConcurrentRequests = 1;
+ this.activeRequests = 0;
+ this.totalRequests = 0;
+
+ // Private properties
+ this._requests = {};
+ this._highPriority = [];
+ this._running = false;
+ this._stopping = false;
+ this._finishedReqs = 0;
+ this._lastTotalRequests = 0;
+}
+
+Zotero.Sync.Storage.Queue.prototype.__defineGetter__('Name', function () {
+ return this.name[0].toUpperCase() + this.name.substr(1);
+});
+
+Zotero.Sync.Storage.Queue.prototype.__defineGetter__('running', function () this._running);
+Zotero.Sync.Storage.Queue.prototype.__defineGetter__('stopping', function () this._stopping);
+
+Zotero.Sync.Storage.Queue.prototype.__defineGetter__('unfinishedRequests', function () {
+ return this.totalRequests - this.finishedRequests;
+});
+
+Zotero.Sync.Storage.Queue.prototype.__defineGetter__('finishedRequests', function () {
+ return this._finishedReqs;
+});
+
+Zotero.Sync.Storage.Queue.prototype.__defineSetter__('finishedRequests', function (val) {
+ Zotero.debug("Finished requests: " + val);
+ Zotero.debug("Total requests: " + this.totalRequests);
+
+ this._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.activeRequests) {
+ throw new Error(this.Name + " queue can't be done if there are active requests");
+ }
+
+ this._running = false;
+ this._stopping = false;
+ this._requests = {};
+ this._highPriority = [];
+ this._finishedReqs = 0;
+ this._lastTotalRequests = this.totalRequests;
+ this.totalRequests = 0;
+
+ return;
+ }
+
+ if (this._stopping) {
+ return;
+ }
+ this.advance();
+});
+
+Zotero.Sync.Storage.Queue.prototype.__defineGetter__('lastTotalRequests', function () {
+ return this._lastTotalRequests;
+});
+
+Zotero.Sync.Storage.Queue.prototype.__defineGetter__('queuedRequests', function () {
+ return this.unfinishedRequests - this.activeRequests;
+});
+
+Zotero.Sync.Storage.Queue.prototype.__defineGetter__('remaining', function () {
+ var remaining = 0;
+ for each(var request in this._requests) {
+ remaining += request.remaining;
+ }
+ return remaining;
+});
+
+Zotero.Sync.Storage.Queue.prototype.__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);
+});
+
+
+Zotero.Sync.Storage.Queue.prototype.isRunning = function () {
+ return this._running;
+}
+
+Zotero.Sync.Storage.Queue.prototype.isStopping = function () {
+ return this._stopping;
+}
+
+
+/**
+ * Add a request to this queue
+ *
+ * @param {Zotero.Sync.Storage.Request} request
+ * @param {Boolean} highPriority Add or move request to high priority queue
+ */
+Zotero.Sync.Storage.Queue.prototype.addRequest = function (request, highPriority) {
+ request.queue = this;
+ var name = request.name;
+ Zotero.debug("Queuing " + this.name + " request '" + name + "'");
+
+ if (this._requests[name]) {
+ if (highPriority) {
+ Zotero.debug("Moving " + name + " to high-priority queue");
+ this._requests[name].importCallbacks(request);
+ this._highPriority.push(name);
+ return;
+ }
+
+ Zotero.debug("Request '" + name + "' already exists");
+ return;
+ }
+
+ this._requests[name] = request;
+ this.totalRequests++;
+
+ if (highPriority) {
+ this._highPriority.push(name);
+ }
+
+ this.advance();
+}
+
+
+/**
+ * Start another request in this queue if there's an available slot
+ */
+Zotero.Sync.Storage.Queue.prototype.advance = function () {
+ this._running = true;
+
+ if (this._stopping) {
+ Zotero.debug(this.Name + " queue is being stopped in "
+ + "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;
+ }
+
+ // Start the first unprocessed request
+
+ // Try the high-priority queue first
+ var name, request;
+ while (name = this._highPriority.shift()) {
+ request = this._requests[name];
+ if (!request.isRunning() && !request.isFinished()) {
+ request.start();
+ this.advance();
+ return;
+ }
+ }
+
+ // And then others
+ for each(request in this._requests) {
+ if (!request.isRunning() && !request.isFinished()) {
+ request.start();
+ this.advance();
+ return;
+ }
+ }
+}
+
+
+Zotero.Sync.Storage.Queue.prototype.updateProgress = function () {
+ Zotero.Sync.Storage.QueueManager.updateProgress();
+}
+
+
+Zotero.Sync.Storage.Queue.prototype.error = function (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+}
+
+
+/**
+ * Stops all requests in this queue
+ */
+Zotero.Sync.Storage.Queue.prototype.stop = function () {
+ if (!this._running) {
+ Zotero.debug(this.Name + " queue is not running");
+ return;
+ }
+ if (this._stopping) {
+ Zotero.debug("Already stopping " + this.name + " queue");
+ 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();
+ }
+ }
+}
diff --git a/chrome/content/zotero/xpcom/storage/queueManager.js b/chrome/content/zotero/xpcom/storage/queueManager.js
new file mode 100644
index 0000000000..a2732a8bb9
--- /dev/null
+++ b/chrome/content/zotero/xpcom/storage/queueManager.js
@@ -0,0 +1,314 @@
+/*
+ ***** 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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+
+Zotero.Sync.Storage.QueueManager = new function () {
+ var _queues = {};
+ var _conflicts = [];
+ var _cancelled = false;
+
+
+ /**
+ * Retrieving a queue, creating a new one if necessary
+ *
+ * @param {String} queueName
+ */
+ this.get = function (queueName, noInit) {
+ // Initialize the queue if it doesn't exist yet
+ if (!_queues[queueName]) {
+ if (noInit) {
+ return false;
+ }
+ 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];
+ }
+
+
+ this.getAll = function () {
+ var queues = [];
+ for each(var queue in _queues) {
+ queues.push(queue);
+ }
+ return queues;
+ };
+
+
+ /**
+ * 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) {
+ Zotero.debug("Stopping all storage queues");
+ _cancelled = true;
+ for each(var queue in _queues) {
+ if (queue.isRunning() && !queue.isStopping()) {
+ queue.stop();
+ }
+ }
+ }
+
+
+ this.finish = function () {
+ Zotero.debug("All storage queues are finished");
+
+ if (!_cancelled && _conflicts.length) {
+ var data = _reconcileConflicts();
+ if (data) {
+ _processMergeData(data);
+ }
+ }
+
+ try {
+ if (_cancelled) {
+ Zotero.Sync.Storage.EventManager.stop();
+ }
+ else {
+ Zotero.Sync.Storage.EventManager.success();
+ }
+ }
+ finally {
+ _cancelled = false;
+ _conflicts = [];
+ }
+ }
+
+
+ /**
+ * 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.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();
+ if (!win.ZoteroPane) continue;
+ var doc = win.ZoteroPane.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.numberFormat(remaining / 1024, 0)
+ );
+ var totalRequests = queue.totalRequests;
+ var filesRemaining = Zotero.getString(
+ 'sync.storage.filesRemaining',
+ [totalRequests - unfinishedRequests, totalRequests]
+ );
+ 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), true));
+ var item2 = item.clone(false, false, true);
+ item2.setField('dateModified',
+ Zotero.Date.dateToSQL(new Date(conflict.remoteData.modTime), true));
+ objectPairs.push([item1, item2]);
+ }
+
+ var io = {
+ dataIn: {
+ type: 'storagefile',
+ captions: [
+ Zotero.getString('sync.storage.localFile'),
+ Zotero.getString('sync.storage.remoteFile'),
+ Zotero.getString('sync.storage.savedFile')
+ ],
+ 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
+ );
+ }
+ }
+ }
+}
diff --git a/chrome/content/zotero/xpcom/storage/request.js b/chrome/content/zotero/xpcom/storage/request.js
new file mode 100644
index 0000000000..62e96f1a1d
--- /dev/null
+++ b/chrome/content/zotero/xpcom/storage/request.js
@@ -0,0 +1,288 @@
+/*
+ ***** 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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+
+/**
+ * Transfer request for storage sync
+ *
+ * @param {String} name Identifier for request (e.g., "[libraryID]/[key]")
+ * @param {Function} onStart Callback to run when request starts
+ * @param {Function} onStop Callback to run when request stops
+ */
+Zotero.Sync.Storage.Request = function (name, callbacks) {
+ Zotero.debug("Initializing request '" + name + "'");
+
+ this.callbacks = ['onStart', 'onProgress', 'onStop'];
+
+ this.name = name;
+ this.channel = null;
+ this.queue = null;
+ this.progress = 0;
+ this.progressMax = 0;
+
+ this._running = false;
+ this._percentage = 0;
+ this._remaining = null;
+ this._finished = false;
+
+ for (var func in callbacks) {
+ if (this.callbacks.indexOf(func) !== -1) {
+ // Stuff all single functions into arrays
+ this['_' + func] = typeof callbacks[func] === 'function' ? [callbacks[func]] : callbacks[func];
+ }
+ else {
+ throw new Error("Invalid handler '" + func + "'");
+ }
+ }
+}
+
+
+/**
+ * Add callbacks from another request to this request
+ */
+Zotero.Sync.Storage.Request.prototype.importCallbacks = function (request) {
+ for each(var name in this.callbacks) {
+ name = '_' + name;
+ if (request[name]) {
+ // If no handlers for this event, add them all
+ if (!this[name]) {
+ this[name] = request[name];
+ continue;
+ }
+ // Otherwise add functions that don't already exist
+ var add = true;
+ for each(var newFunc in request[name]) {
+ for each(var currentFunc in this[name]) {
+ if (newFunc.toString() === currentFunc.toString()) {
+ Zotero.debug("Callback already exists in request -- not importing");
+ add = false;
+ break;
+ }
+ }
+ if (add) {
+ this[name].push(newFunc);
+ }
+ }
+ }
+ }
+}
+
+
+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++;
+ if (this._onStart) {
+ for each(var f in this._onStart) {
+ f(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) {
+ Zotero.debug("Trying to update finished request " + this.name + " in "
+ + "Zotero.Sync.Storage.Request.onProgress() "
+ + "(" + progress + "/" + progressMax + ")", 2);
+ return;
+ }
+
+ 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();
+
+ if (this.onProgress) {
+ for each(var f in this._onProgress) {
+ f(progress, progressMax);
+ }
+ }
+}
+
+
+Zotero.Sync.Storage.Request.prototype.error = function (e) {
+ this.queue.error(e);
+}
+
+
+/**
+ * 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;
+ }
+ }
+ catch (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();
+
+ if (this._onStop) {
+ for each(var f in this._onStop) {
+ f();
+ }
+ }
+}
diff --git a/chrome/content/zotero/xpcom/storage/session.js b/chrome/content/zotero/xpcom/storage/session.js
deleted file mode 100644
index 2a54298d92..0000000000
--- a/chrome/content/zotero/xpcom/storage/session.js
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- ***** 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 .
-
- ***** END LICENSE BLOCK *****
-*/
-
-
-Zotero.Sync.Storage.Session = function (module, callbacks) {
- switch (module) {
- case 'webdav':
- this._session = new Zotero.Sync.Storage.Session.WebDAV(callbacks);
- break;
-
- case 'zfs':
- this._session = new Zotero.Sync.Storage.Session.ZFS(callbacks);
- break;
-
- default:
- throw ("Invalid storage session module '" + module + "'");
- }
-
- this.module = module;
- this.onError = callbacks.onError;
-}
-
-Zotero.Sync.Storage.Session.prototype.__defineGetter__('name', function () this._session.name);
-Zotero.Sync.Storage.Session.prototype.__defineGetter__('includeUserFiles', function () this._session.includeUserFiles);
-Zotero.Sync.Storage.Session.prototype.__defineGetter__('includeGroupFiles', function () this._session.includeGroupFiles);
-
-Zotero.Sync.Storage.Session.prototype.__defineGetter__('enabled', function () {
- try {
- return this._session.enabled;
- }
- catch (e) {
- this.onError(e);
- }
-});
-
-Zotero.Sync.Storage.Session.prototype.__defineGetter__('verified', function () {
- try {
- return this._session.verified;
- }
- catch (e) {
- this.onError(e);
- }
-});
-
-Zotero.Sync.Storage.Session.prototype.__defineGetter__('active', function () {
- try {
- return this._session.active;
- }
- catch (e) {
- this.onError(e);
- }
-});
-
-Zotero.Sync.Storage.Session.prototype.__defineGetter__('username', function () {
- try {
- return this._session.username;
- }
- catch (e) {
- this.onError(e);
- }
-});
-
-Zotero.Sync.Storage.Session.prototype.__defineGetter__('password', function () {
- try {
- return this._session.password;
- }
- catch (e) {
- this.onError(e);
- }
-});
-
-Zotero.Sync.Storage.Session.prototype.__defineSetter__('password', function (val) {
- try {
- this._session.password = val;
- }
- catch (e) {
- this.onError(e);
- }
-});
-
-
-Zotero.Sync.Storage.Session.prototype.init = function () {
- try {
- return this._session.init();
- }
- catch (e) {
- this.onError(e);
- }
-}
-
-Zotero.Sync.Storage.Session.prototype.initFromPrefs = function () {
- try {
- return this._session.initFromPrefs();
- }
- catch (e) {
- this.onError(e);
- }
-}
-
-Zotero.Sync.Storage.Session.prototype.downloadFile = function (request) {
- try {
- this._session.downloadFile(request);
- }
- catch (e) {
- this.onError(e);
- }
-}
-
-Zotero.Sync.Storage.Session.prototype.uploadFile = function (request) {
- try {
- this._session.uploadFile(request);
- }
- catch (e) {
- this.onError(e);
- }
-}
-
-Zotero.Sync.Storage.Session.prototype.getLastSyncTime = function (callback) {
- try {
- this._session.getLastSyncTime(callback);
- }
- catch (e) {
- this.onError(e);
- }
-}
-
-Zotero.Sync.Storage.Session.prototype.setLastSyncTime = function (callback, useLastSyncTime) {
- try {
- this._session.setLastSyncTime(callback, useLastSyncTime);
- }
- catch (e) {
- this.onError(e);
- }
-}
-
-Zotero.Sync.Storage.Session.prototype.checkServer = function (callback) {
- try {
- return this._session.checkServer(callback);
- }
- catch (e) {
- this.onError(e);
- }
-}
-
-Zotero.Sync.Storage.Session.prototype.checkServerCallback = function (uri, status, authRequired, window, skipSuccessMessage, error) {
- try {
- return this._session.checkServerCallback(uri, status, authRequired, window, skipSuccessMessage, error);
- }
- catch (e) {
- this.onError(e);
- }
-}
-
-Zotero.Sync.Storage.Session.prototype.purgeDeletedStorageFiles = function (callback) {
- try {
- this._session.purgeDeletedStorageFiles(callback);
- }
- catch (e) {
- this.onError(e);
- }
-}
-
-Zotero.Sync.Storage.Session.prototype.purgeOrphanedStorageFiles = function (callback) {
- try {
- this._session.purgeOrphanedStorageFiles(callback);
- }
- catch (e) {
- this.onError(e);
- }
-}
diff --git a/chrome/content/zotero/xpcom/storage/streamListener.js b/chrome/content/zotero/xpcom/storage/streamListener.js
new file mode 100644
index 0000000000..e2f51ab0a7
--- /dev/null
+++ b/chrome/content/zotero/xpcom/storage/streamListener.js
@@ -0,0 +1,235 @@
+/*
+ ***** 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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+
+/**
+ * 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._onStop(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._onStop(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('onChannelRedirect');
+
+ // if redirecting, store the new channel
+ this._channel = newChannel;
+ },
+
+ asyncOnChannelRedirect: function (oldChan, newChan, flags, redirectCallback) {
+ Zotero.debug('asyncOnRedirect');
+
+ this.onChannelRedirect(oldChan, newChan, flags);
+ redirectCallback.onRedirectVerifyCallback(0);
+ },
+
+ // 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);
+ }
+ },
+
+ _onStop: 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
+ },
+};
diff --git a/chrome/content/zotero/xpcom/storage/webdav.js b/chrome/content/zotero/xpcom/storage/webdav.js
index ff481123ed..eadad5b35c 100644
--- a/chrome/content/zotero/xpcom/storage/webdav.js
+++ b/chrome/content/zotero/xpcom/storage/webdav.js
@@ -24,996 +24,378 @@
*/
-Zotero.Sync.Storage.Session.WebDAV = function (callbacks) {
- this.onChangesMade = callbacks.onChangesMade ? callbacks.onChangesMade : function () {};
- this.onError = callbacks.onError ? function (e) {
- if (!e) {
- e = Zotero.Sync.Storage.Session.WebDAV.prototype.defaultError;
- }
- callbacks.onError(e);
- } : function () {};
+Zotero.Sync.Storage.Module.WebDAV = (function () {
+ // TEMP
+ // TODO: localize
+ var _defaultError = "A WebDAV file sync error occurred. Please try syncing again.\n\nIf you receive this message repeatedly, check your WebDAV server settings in the Sync pane of the Zotero preferences.";
+ var _defaultErrorRestart = "A WebDAV file sync error occurred. Please restart Firefox and try syncing again.\n\nIf you receive this message repeatedly, check your WebDAV server settings in the Sync pane of the Zotero preferences.";
- this._parentURI;
- this._rootURI;
- this._cachedCredentials = false;
-}
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.name = "WebDAV";
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('includeUserFiles', function () {
- return Zotero.Prefs.get("sync.storage.enabled") && Zotero.Prefs.get("sync.storage.protocol") == 'webdav';
-});
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.includeGroupItems = false;
-
-// TEMP
-// TODO: localize
-Zotero.Sync.Storage.Session.WebDAV.prototype.defaultError = "A WebDAV file sync error occurred. Please try syncing again.\n\nIf you receive this message repeatedly, check your WebDAV server settings in the Sync pane of the Zotero preferences.";
-Zotero.Sync.Storage.Session.WebDAV.prototype.defaultErrorRestart = "A WebDAV file sync error occurred. Please restart Firefox and try syncing again.\n\nIf you receive this message repeatedly, check your WebDAV server settings in the Sync pane of the Zotero preferences.";
-
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('enabled', function () {
- return this.includeUserFiles;
-});
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('verified', function () {
- return Zotero.Prefs.get("sync.storage.verified");
-});
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('active', function () {
- return this.enabled && this.verified;
-});
-
-Zotero.Sync.Storage.Session.WebDAV.prototype._loginManagerHost = 'chrome://zotero';
-Zotero.Sync.Storage.Session.WebDAV.prototype._loginManagerURL = 'Zotero Storage Server';
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('username', function () {
- return Zotero.Prefs.get('sync.storage.username');
-});
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('password', function () {
- var username = this.username;
+ var _parentURI;
+ var _rootURI;
+ var _cachedCredentials = false;
- if (!username) {
- Zotero.debug('Username not set before getting Zotero.Sync.Storage.Session.WebDAV.password');
- return '';
- }
+ var _loginManagerHost = 'chrome://zotero';
+ var _loginManagerURL = 'Zotero Storage Server';
- Zotero.debug('Getting WebDAV password');
- var loginManager = Components.classes["@mozilla.org/login-manager;1"]
- .getService(Components.interfaces.nsILoginManager);
- var logins = loginManager.findLogins({}, this._loginManagerHost, this._loginManagerURL, null);
-
- // Find user from returned array of nsILoginInfo objects
- for (var i = 0; i < logins.length; i++) {
- if (logins[i].username == username) {
- return logins[i].password;
- }
- }
-
- return '';
-});
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.__defineSetter__('password', function (password) {
- var username = this.username;
- if (!username) {
- Zotero.debug('Username not set before setting Zotero.Sync.Server.Session.WebDAV.password');
- return;
- }
-
- this._cachedCredentials = false;
-
- var loginManager = Components.classes["@mozilla.org/login-manager;1"]
- .getService(Components.interfaces.nsILoginManager);
- var logins = loginManager.findLogins({}, this._loginManagerHost, this._loginManagerURL, null);
-
- for (var i = 0; i < logins.length; i++) {
- Zotero.debug('Clearing WebDAV passwords');
- loginManager.removeLogin(logins[i]);
- break;
- }
-
- if (password) {
- Zotero.debug(this._loginManagerURL);
- var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
- Components.interfaces.nsILoginInfo, "init");
- var loginInfo = new nsLoginInfo(this._loginManagerHost, this._loginManagerURL,
- null, username, password, "", "");
- loginManager.addLogin(loginInfo);
- }
-});
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('rootURI', function () {
- if (!this._rootURI) {
- throw ("Root URI not initialized in Zotero.Sync.Storage.Session.WebDAV.rootURI");
- }
- return this._rootURI.clone();
-});
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('parentURI', function () {
- if (!this._parentURI) {
- throw ("Parent URI not initialized in Zotero.Sync.Storage.Session.WebDAV.parentURI");
- }
- return this._parentURI.clone();
-});
-
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.init = function (url, dir, username, password) {
- if (!url) {
- var msg = "WebDAV URL not provided";
- Zotero.debug(msg);
- throw ({
- message: msg,
- name: "Z_ERROR_NO_URL",
- filename: "webdav.js",
- toString: function () { return this.message; }
- });
- }
-
- if (username && !password) {
- var msg = "WebDAV password not provided";
- Zotero.debug(msg);
- throw ({
- message: msg,
- name: "Z_ERROR_NO_PASSWORD",
- filename: "webdav.js",
- toString: function () { return this.message; }
- });
- }
-
- var ios = Components.classes["@mozilla.org/network/io-service;1"].
- getService(Components.interfaces.nsIIOService);
- try {
- var uri = ios.newURI(url, null, null);
- if (username) {
- uri.username = username;
- uri.password = password;
- }
- }
- catch (e) {
- Zotero.debug(e);
- Components.utils.reportError(e);
- return false;
- }
- if (!uri.spec.match(/\/$/)) {
- uri.spec += "/";
- }
- this._parentURI = uri;
-
- var uri = uri.clone();
- uri.spec += "zotero/";
- this._rootURI = uri;
- return true;
-}
-
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.initFromPrefs = function () {
- var scheme = Zotero.Prefs.get('sync.storage.scheme');
- switch (scheme) {
- case 'http':
- case 'https':
- break;
+ //
+ // Private methods
+ //
+ /**
+ * Get mod time of file on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} callback Callback f(item, mdate)
+ */
+ function getStorageModificationTime(item, callback) {
+ var uri = getItemPropertyURI(item);
- default:
- throw ("Invalid WebDAV scheme '" + scheme
- + "' in Zotero.Sync.Storage.Session.WebDAV.rootURI");
- }
-
- var url = Zotero.Prefs.get('sync.storage.url');
- if (!url) {
- return false;
- }
-
- url = scheme + '://' + url;
- var dir = "zotero";
- var username = this.username;
- var password = this.password;
-
- return this.init(url, dir, username, password);
-}
-
-
-/**
- * Get mod time of file on storage server
- *
- * @param {Zotero.Item} item
- * @param {Function} callback Callback f(item, mdate)
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype._getStorageModificationTime = function (item, callback) {
- var uri = this._getItemPropertyURI(item);
-
- var self = this;
-
- Zotero.HTTP.doGet(uri, function (req) {
- self._checkResponse(req, self);
-
- var funcName = "Zotero.Sync.Storage.WebDAV_getStorageModificationTime()";
-
- // mod_speling can return 300s for 404s with base name matches
- if (req.status == 404 || req.status == 300) {
- callback(item, false);
- return;
- }
- else if (req.status != 200) {
+ Zotero.HTTP.doGet(uri, function (req) {
+ checkResponse(req);
+
+ var funcName = "Zotero.Sync.Storage.WebDAV.getStorageModificationTime()";
+
+ // mod_speling can return 300s for 404s with base name matches
+ if (req.status == 404 || req.status == 300) {
+ callback(item, false);
+ return;
+ }
+ else if (req.status != 200) {
+ Zotero.debug(req.responseText);
+ Zotero.Sync.Storage.EventManager.error(
+ "Unexpected status code " + req.status + " in " + funcName
+ );
+ }
+
Zotero.debug(req.responseText);
- self.onError("Unexpected status code " + req.status + " in " + funcName);
- return;
- }
-
- Zotero.debug(req.responseText);
-
- // No modification time set
- if (!req.responseText) {
- callback(item, false);
- return;
- }
-
- try {
- var xml = new XML(req.responseText);
- }
- catch (e) {
- Zotero.debug(e);
- var xml = null;
- }
-
- if (xml) {
- Zotero.debug(xml.children().length());
- }
-
- if (xml && xml.children().length()) {
- // TODO: other stuff, but this makes us forward-compatible
- mtime = xml.mtime.toString();
- var seconds = false;
- }
- else {
- mtime = req.responseText;
- var seconds = true;
- }
-
- var invalid = false;
-
- // Unix timestamps need to be converted to ms-based timestamps
- if (seconds) {
- if (mtime.match(/^[0-9]{1,10}$/)) {
- Zotero.debug("Converting Unix timestamp '" + mtime + "' to milliseconds");
- mtime = mtime * 1000;
+
+ // No modification time set
+ if (!req.responseText) {
+ callback(item, false);
+ return;
+ }
+
+ try {
+ var xml = new XML(req.responseText);
+ }
+ catch (e) {
+ Zotero.debug(e);
+ var xml = null;
+ }
+
+ if (xml) {
+ Zotero.debug(xml.children().length());
+ }
+
+ if (xml && xml.children().length()) {
+ // TODO: other stuff, but this makes us forward-compatible
+ mtime = xml.mtime.toString();
+ var seconds = false;
}
else {
+ mtime = req.responseText;
+ var seconds = true;
+ }
+
+ var invalid = false;
+
+ // Unix timestamps need to be converted to ms-based timestamps
+ if (seconds) {
+ if (mtime.match(/^[0-9]{1,10}$/)) {
+ Zotero.debug("Converting Unix timestamp '" + mtime + "' to milliseconds");
+ mtime = mtime * 1000;
+ }
+ else {
+ invalid = true;
+ }
+ }
+ else if (!mtime.match(/^[0-9]{1,13}$/)) {
invalid = true;
}
- }
- else if (!mtime.match(/^[0-9]{1,13}$/)) {
- invalid = true;
- }
-
- // Delete invalid .prop files
- if (invalid) {
- var msg = "Invalid mod date '" + Zotero.Utilities.ellipsize(mtime, 20)
- + "' for item " + Zotero.Items.getLibraryKeyHash(item);
- Zotero.debug(msg, 1);
- Components.utils.reportError(msg);
- self._deleteStorageFiles([item.key + ".prop"], null, self);
- self.onError();
- return;
- }
-
- var mdate = new Date(parseInt(mtime));
- callback(item, mdate);
- });
-}
-
-
-/**
- * Set mod time of file on storage server
- *
- * @param {Zotero.Item} item
- * @param {Function} callback Callback f(item, props)
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype._setStorageModificationTime = function (item, callback) {
- var uri = this._getItemPropertyURI(item);
-
- var mtime = item.attachmentModificationTime;
- var hash = item.attachmentHash;
-
- var prop =
- {mtime}
- {hash}
- ;
-
- Zotero.HTTP.WebDAV.doPut(uri, prop.toXMLString(), function (req) {
- switch (req.status) {
- case 200:
- case 201:
- case 204:
- break;
- default:
- Zotero.debug(req.responseText);
- throw ("Unexpected status code " + req.status + " in "
- + "Zotero.Sync.Storage._setStorageModificationTime()");
- }
- callback(item, { mtime: mtime, hash: hash });
- });
-}
-
-
-
-/**
- * Begin download process for individual file
- *
- * @param {Zotero.Sync.Storage.Request} [request]
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype.downloadFile = function (request) {
- var funcName = "Zotero.Sync.Storage.Session.WebDAV.downloadFile()";
-
- var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
- if (!item) {
- throw ("Item '" + request.name + "' not found in " + funcName);
- }
-
- var self = this;
-
- // Retrieve modification time from server to store locally afterwards
- this._getStorageModificationTime(item, function (item, mdate) {
- if (!request.isRunning()) {
- Zotero.debug("Download request '" + request.name
- + "' is no longer running after getting mod time");
- return;
- }
-
- if (!mdate) {
- Zotero.debug("Remote file not found for item " + Zotero.Items.getLibraryKeyHash(item));
- request.finish();
- return;
- }
-
- try {
- var syncModTime = mdate.getTime();
-
- // Skip download if local file exists and matches mod time
- var file = item.getFile();
- if (file && file.exists() && syncModTime == file.lastModifiedTime) {
- Zotero.debug("File mod time matches remote file -- skipping download");
-
- Zotero.DB.beginTransaction();
- var syncState = Zotero.Sync.Storage.getSyncState(item.id);
- var updateItem = syncState != 1;
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.DB.commitTransaction();
- self.onChangesMade();
- request.finish();
- return;
- }
-
- var uri = self._getItemURI(item);
- var destFile = Zotero.getTempDirectory();
- destFile.append(item.key + '.zip.tmp');
- if (destFile.exists()) {
- destFile.remove(false);
- }
-
- var listener = new Zotero.Sync.Storage.StreamListener(
- {
- onStart: function (request, data) {
- if (data.request.isFinished()) {
- Zotero.debug("Download request " + data.request.name
- + " stopped before download started -- closing channel");
- request.cancel(0x804b0002); // NS_BINDING_ABORTED
- return;
- }
- },
- onProgress: function (a, b, c) {
- request.onProgress(a, b, c)
- },
- onStop: function (request, status, response, data) {
- if (status == 404) {
- var msg = "Remote ZIP file not found for item " + item.key;
- Zotero.debug(msg, 2);
- Components.utils.reportError(msg);
-
- // Delete the orphaned prop file
- self._deleteStorageFiles([item.key + ".prop"], null, self);
-
- data.request.finish();
- return;
- }
- else if (status != 200) {
- var msg = "Unexpected status code " + status
- + " for request " + data.request.name + " in Zotero.Sync.Storage.Session.WebDAV.downloadFile()";
- Zotero.debug(msg, 1);
- Components.utils.reportError(msg);
- self.onError();
- return;
- }
-
- // Don't try to process if the request has been cancelled
- if (data.request.isFinished()) {
- Zotero.debug("Download request " + data.request.name
- + " is no longer running after file download");
- return;
- }
-
- Zotero.debug("Finished download of " + destFile.path);
-
- try {
- Zotero.Sync.Storage.processDownload(data);
- data.request.finish();
- }
- catch (e) {
- self.onError(e);
- }
- },
- request: request,
- item: item,
- compressed: true,
- syncModTime: syncModTime
- }
- );
-
- // Don't display password in console
- var disp = uri.clone();
- if (disp.password) {
- disp.password = '********';
- }
- Zotero.debug('Saving ' + disp.spec + ' with saveURI()');
- const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
- var wbp = Components
- .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
- .createInstance(nsIWBP);
- wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
- wbp.progressListener = listener;
- wbp.saveURI(uri, null, null, null, null, destFile);
- }
- catch (e) {
- request.error(e);
- }
- });
-}
-
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.uploadFile = function (request) {
- var self = this;
- Zotero.Sync.Storage.createUploadFile(request, function (data) { self._processUploadFile(data); });
-}
-
-/**
- * Upload the generated ZIP file to the server
- *
- * @param {Object} Object with 'request' property
- * @return {void}
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype._processUploadFile = function (data) {
- /*
- _updateSizeMultiplier(
- (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100
- );
- */
- var request = data.request;
- var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
-
- var self = this;
-
- this._getStorageModificationTime(item, function (item, mdate) {
- try {
- if (!request.isRunning()) {
- Zotero.debug("Upload request '" + request.name
- + "' is no longer running after getting mod time");
- return;
- }
-
- // Check for conflict
- if (Zotero.Sync.Storage.getSyncState(item.id)
- != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) {
- if (mdate) {
- // Remote prop time
- var mtime = mdate.getTime();
-
- // Local file time
- var fmtime = item.attachmentModificationTime;
-
- var same = false;
- if (fmtime == mtime) {
- same = true;
- Zotero.debug("File mod time matches remote file -- skipping upload");
- }
- // Allow floored timestamps for filesystems that don't support
- // millisecond precision (e.g., HFS+)
- else if (Math.floor(mtime / 1000) * 1000 == fmtime || Math.floor(fmtime / 1000) * 1000 == mtime) {
- same = true;
- Zotero.debug("File mod times are within one-second precision (" + fmtime + " ≅ " + mtime + ") "
- + "-- skipping upload");
- }
- // Allow timestamp to be exactly one hour off to get around
- // time zone issues -- there may be a proper way to fix this
- else 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) {
- same = true;
- Zotero.debug("File mod time (" + fmtime + ") is exactly one hour off remote file (" + mtime + ") "
- + "-- assuming time zone issue and skipping upload");
- }
-
- if (same) {
- Zotero.DB.beginTransaction();
- var syncState = Zotero.Sync.Storage.getSyncState(item.id);
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime, true);
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.DB.commitTransaction();
- self.onChangesMade();
- request.finish();
- return;
- }
-
- var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id);
- if (smtime != mtime) {
- var localData = { modTime: fmtime };
- var remoteData = { modTime: mtime };
- Zotero.Sync.Storage.QueueManager.addConflict(
- request.name, localData, remoteData
- );
- Zotero.debug("Conflict -- last synced file mod time "
- + "does not match time on storage server"
- + " (" + smtime + " != " + mtime + ")");
- request.finish();
- return;
- }
- }
- else {
- Zotero.debug("Remote file not found for item " + item.id);
- }
- }
-
- var file = Zotero.getTempDirectory();
- file.append(item.key + '.zip');
-
- var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
- .createInstance(Components.interfaces.nsIFileInputStream);
- fis.init(file, 0x01, 0, 0);
-
- var bis = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
- .createInstance(Components.interfaces.nsIBufferedInputStream)
- bis.init(fis, 64 * 1024);
-
- var uri = self._getItemURI(item);
-
- var ios = Components.classes["@mozilla.org/network/io-service;1"].
- getService(Components.interfaces.nsIIOService);
- var channel = ios.newChannelFromURI(uri);
- channel.QueryInterface(Components.interfaces.nsIUploadChannel);
- channel.setUploadStream(bis, 'application/octet-stream', -1);
- channel.QueryInterface(Components.interfaces.nsIHttpChannel);
- channel.requestMethod = 'PUT';
- channel.allowPipelining = false;
-
- channel.setRequestHeader('Keep-Alive', '', false);
- channel.setRequestHeader('Connection', '', false);
-
- var listener = new Zotero.Sync.Storage.StreamListener(
- {
- onProgress: function (a, b, c) {
- request.onProgress(a, b, c);
- },
- onStop: function (httpRequest, status, response, data) { self._onUploadComplete(httpRequest, status, response,data); },
- onCancel: function (httpRequest, status, data) { self._onUploadCancel(httpRequest, status, data); },
- request: request,
- item: item,
- streams: [fis, bis]
- }
- );
- channel.notificationCallbacks = listener;
-
- var dispURI = uri.clone();
- if (dispURI.password) {
- dispURI.password = '********';
- }
- Zotero.debug("HTTP PUT of " + file.leafName + " to " + dispURI.spec);
-
- channel.asyncOpen(listener, null);
- }
- catch (e) {
- self.onError(e);
- }
- });
-}
-
-
-Zotero.Sync.Storage.Session.WebDAV.prototype._onUploadComplete = function (httpRequest, status, response, data) {
- var request = data.request;
- var item = data.item;
- var url = httpRequest.name;
-
- Zotero.debug("Upload of attachment " + item.key
- + " finished with status code " + status);
-
- switch (status) {
- case 200:
- case 201:
- case 204:
- break;
-
- case 403:
- case 500:
- this.onError(Zotero.localeJoin([
- Zotero.getString('sync.storage.error.fileUploadFailed'),
- Zotero.getString('sync.storage.error.checkFileSyncSettings')
- ]));
- return;
-
- case 507:
- this.onError(Zotero.getString('sync.storage.error.webdav.insufficientSpace'));
- return;
-
- default:
- this.onError("Unexpected file upload status " + status
- + " in Zotero.Sync.Storage.WebDAV._onUploadComplete()");
- return;
- }
-
- var self = this;
-
- this._setStorageModificationTime(item, function (item, props) {
- if (!request.isRunning()) {
- Zotero.debug("Upload request '" + request.name
- + "' is no longer running after getting mod time");
- return;
- }
-
- Zotero.DB.beginTransaction();
-
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, props.mtime, true);
- Zotero.Sync.Storage.setSyncedHash(item.id, props.hash);
-
- Zotero.DB.commitTransaction();
-
- try {
- var file = Zotero.getTempDirectory();
- file.append(item.key + '.zip');
- file.remove(false);
- }
- catch (e) {
- Components.utils.reportError(e);
- }
-
- self.onChangesMade();
- request.finish();
- });
-}
-
-
-Zotero.Sync.Storage.Session.WebDAV.prototype._onUploadCancel = function (httpRequest, status, data) {
- var request = data.request;
- var item = data.item;
-
- Zotero.debug("Upload of attachment " + item.key + " cancelled with status code " + status);
-
- try {
- var file = Zotero.getTempDirectory();
- file.append(item.key + '.zip');
- file.remove(false);
- }
- catch (e) {
- Components.utils.reportError(e);
- }
-
- request.finish();
-}
-
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.getLastSyncTime = function (callback) {
- // Cache the credentials at the root URI
- if (!this._cachedCredentials) {
- var self = this;
-
- Zotero.HTTP.doOptions(this.rootURI, function (req) {
- self._checkResponse(req, self);
-
- if (req.status != 200) {
- var msg = "Unexpected status code " + req.status + " for OPTIONS request "
- + "in Zotero.Sync.Storage.Session.WebDAV.getLastSyncTime()";
+ // Delete invalid .prop files
+ if (invalid) {
+ var msg = "Invalid mod date '" + Zotero.Utilities.ellipsize(mtime, 20)
+ + "' for item " + Zotero.Items.getLibraryKeyHash(item);
Zotero.debug(msg, 1);
Components.utils.reportError(msg);
- self.onError(Zotero.Sync.Storage.Session.WebDAV.prototype.defaultErrorRestart);
- return;
+ deleteStorageFiles([item.key + ".prop"]);
+ Zotero.Sync.Storage.EventManager.error(_defaultError);
}
- self._cachedCredentials = true;
- self.getLastSyncTime(callback);
+
+ var mdate = new Date(parseInt(mtime));
+ callback(item, mdate);
});
- return;
}
- try {
- var uri = this.rootURI;
- var successFileURI = uri.clone();
- successFileURI.spec += "lastsync";
- Zotero.HTTP.doGet(successFileURI, function (req) {
- var ts = undefined;
- try {
- if (req.responseText) {
- Zotero.debug(req.responseText);
- }
- Zotero.debug(req.status);
-
- if (req.status == 403) {
- Zotero.debug("Clearing WebDAV authentication credentials", 2);
- self._cachedCredentials = false;
- }
-
- if (req.status != 200 && req.status != 404) {
- var msg = "Unexpected status code " + req.status + " for HEAD request "
- + "in Zotero.Sync.Storage.Session.WebDAV.getLastSyncTime()";
- Zotero.debug(msg, 1);
- Components.utils.reportError(msg);
- self.onError();
- return;
- }
-
- if (req.status == 200) {
- var lastModified = req.getResponseHeader("Last-Modified");
- var date = new Date(lastModified);
- Zotero.debug("Last successful storage sync was " + date);
- ts = Zotero.Date.toUnixTimestamp(date);
- }
- else {
- ts = null;
- }
- }
- finally {
- callback(ts);
- }
- });
- return;
- }
- catch (e) {
- Zotero.debug(e);
- Components.utils.reportError(e);
- callback();
- return;
- }
-}
-
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.setLastSyncTime = function (callback) {
- try {
- var uri = this.rootURI;
- var successFileURI = uri.clone();
- successFileURI.spec += "lastsync";
+
+ /**
+ * Set mod time of file on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} callback Callback f(item, props)
+ */
+ function setStorageModificationTime(item, callback) {
+ var uri = getItemPropertyURI(item);
- var self = this;
+ var mtime = item.attachmentModificationTime;
+ var hash = item.attachmentHash;
- Zotero.HTTP.WebDAV.doPut(successFileURI, "1", function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
+ var prop =
+ {mtime}
+ {hash}
+ ;
+
+ Zotero.HTTP.WebDAV.doPut(uri, prop.toXMLString(), function (req) {
switch (req.status) {
case 200:
case 201:
case 204:
- self.getLastSyncTime(function (ts) {
- if (ts) {
- var sql = "REPLACE INTO version VALUES ('storage_webdav', ?)";
- Zotero.DB.query(sql, { int: ts });
- }
- if (callback) {
- callback();
- }
- });
- return;
- }
-
- var msg = "Unexpected error code " + req.status + " uploading storage success file";
- Zotero.debug(msg, 2);
- Components.utils.reportError(msg);
- if (callback) {
- callback();
- }
- });
- }
- catch (e) {
- Zotero.debug(e);
- Components.utils.reportError(e);
- if (callback) {
- callback();
- }
- return;
- }
-}
-
-
-/**
- * @param {Function} callback Function to pass URI and result value to
- * @param {Object} errorCallbacks
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype.checkServer = function (callback) {
- try {
- var parentURI = this.parentURI;
- var uri = this.rootURI;
- }
- catch (e) {
- switch (e.name) {
- case 'Z_ERROR_NO_URL':
- callback(null, Zotero.Sync.Storage.ERROR_NO_URL);
- return;
-
- case 'Z_ERROR_NO_PASSWORD':
- callback(null, Zotero.Sync.Storage.ERROR_NO_PASSWORD);
- return;
+ break;
- default:
- Zotero.debug(e);
+ default:
+ Zotero.debug(req.responseText);
+ throw new Error("Unexpected status code " + req.status);
+ }
+ callback(item, { mtime: mtime, hash: hash });
+ });
+
+
+ /**
+ * Upload the generated ZIP file to the server
+ *
+ * @param {Object} Object with 'request' property
+ * @return {void}
+ */
+ function processUploadFile(data) {
+ /*
+ updateSizeMultiplier(
+ (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100
+ );
+ */
+ var request = data.request;
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+
+ getStorageModificationTime(item, function (item, mdate) {
+ try {
+ if (!request.isRunning()) {
+ Zotero.debug("Upload request '" + request.name
+ + "' is no longer running after getting mod time");
+ return;
+ }
+
+ // Check for conflict
+ if (Zotero.Sync.Storage.getSyncState(item.id)
+ != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) {
+ if (mdate) {
+ // Remote prop time
+ var mtime = mdate.getTime();
+
+ // Local file time
+ var fmtime = item.attachmentModificationTime;
+
+ var same = false;
+ if (fmtime == mtime) {
+ same = true;
+ Zotero.debug("File mod time matches remote file -- skipping upload");
+ }
+ // Allow floored timestamps for filesystems that don't support
+ // millisecond precision (e.g., HFS+)
+ else if (Math.floor(mtime / 1000) * 1000 == fmtime || Math.floor(fmtime / 1000) * 1000 == mtime) {
+ same = true;
+ Zotero.debug("File mod times are within one-second precision (" + fmtime + " ≅ " + mtime + ") "
+ + "-- skipping upload");
+ }
+ // Allow timestamp to be exactly one hour off to get around
+ // time zone issues -- there may be a proper way to fix this
+ else 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) {
+ same = true;
+ Zotero.debug("File mod time (" + fmtime + ") is exactly one hour off remote file (" + mtime + ") "
+ + "-- assuming time zone issue and skipping upload");
+ }
+
+ if (same) {
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime, true);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ onChangesMade();
+ request.finish();
+ return;
+ }
+
+ var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id);
+ if (smtime != mtime) {
+ var localData = { modTime: fmtime };
+ var remoteData = { modTime: mtime };
+ Zotero.Sync.Storage.QueueManager.addConflict(
+ request.name, localData, remoteData
+ );
+ Zotero.debug("Conflict -- last synced file mod time "
+ + "does not match time on storage server"
+ + " (" + smtime + " != " + mtime + ")");
+ request.finish();
+ return;
+ }
+ }
+ else {
+ Zotero.debug("Remote file not found for item " + item.id);
+ }
+ }
+
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+
+ var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ fis.init(file, 0x01, 0, 0);
+
+ var bis = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
+ .createInstance(Components.interfaces.nsIBufferedInputStream)
+ bis.init(fis, 64 * 1024);
+
+ var uri = getItemURI(item);
+
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ var channel = ios.newChannelFromURI(uri);
+ channel.QueryInterface(Components.interfaces.nsIUploadChannel);
+ channel.setUploadStream(bis, 'application/octet-stream', -1);
+ channel.QueryInterface(Components.interfaces.nsIHttpChannel);
+ channel.requestMethod = 'PUT';
+ channel.allowPipelining = false;
+
+ channel.setRequestHeader('Keep-Alive', '', false);
+ channel.setRequestHeader('Connection', '', false);
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onProgress: function (a, b, c) {
+ request.onProgress(a, b, c);
+ },
+ onStop: function (httpRequest, status, response, data) { onUploadComplete(httpRequest, status, response,data); },
+ onCancel: function (httpRequest, status, data) { onUploadCancel(httpRequest, status, data); },
+ request: request,
+ item: item,
+ streams: [fis, bis]
+ }
+ );
+ channel.notificationCallbacks = listener;
+
+ var dispURI = uri.clone();
+ if (dispURI.password) {
+ dispURI.password = '********';
+ }
+ Zotero.debug("HTTP PUT of " + file.leafName + " to " + dispURI.spec);
+
+ channel.asyncOpen(listener, null);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+ });
+ }
+
+
+ function onUploadComplete(httpRequest, status, response, data) {
+ var request = data.request;
+ var item = data.item;
+ var url = httpRequest.name;
+
+ Zotero.debug("Upload of attachment " + item.key
+ + " finished with status code " + status);
+
+ switch (status) {
+ case 200:
+ case 201:
+ case 204:
+ break;
+
+ case 403:
+ case 500:
+ Zotero.Sync.Storage.EventManager.error(
+ Zotero.getString('sync.storage.error.fileUploadFailed')
+ + " " + Zotero.getString('sync.storage.error.checkFileSyncSettings')
+ );
+
+ case 507:
+ Zotero.Sync.Storage.EventManager.error(
+ Zotero.getString('sync.storage.error.webdav.insufficientSpace')
+ );
+
+ default:
+ Zotero.Sync.Storage.EventManager.error(
+ "Unexpected file upload status " + status
+ + " in Zotero.Sync.Storage.WebDAV.onUploadComplete()"
+ );
+ }
+
+ setStorageModificationTime(item, function (item, props) {
+ if (!request.isRunning()) {
+ Zotero.debug("Upload request '" + request.name
+ + "' is no longer running after getting mod time");
+ return;
+ }
+
+ Zotero.DB.beginTransaction();
+
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, props.mtime, true);
+ Zotero.Sync.Storage.setSyncedHash(item.id, props.hash);
+
+ Zotero.DB.commitTransaction();
+
+ try {
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+ file.remove(false);
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ onChangesMade();
+ request.finish();
+ });
+ }
+
+
+ function onUploadCancel(httpRequest, status, data) {
+ var request = data.request;
+ var item = data.item;
+
+ Zotero.debug("Upload of attachment " + item.key + " cancelled with status code " + status);
+
+ try {
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+ file.remove(false);
+ }
+ catch (e) {
Components.utils.reportError(e);
- callback(null, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
+ }
+
+ request.finish();
}
}
- var requestHolder = { request: null };
- var prolog = '\n';
- var D = new Namespace("D", "DAV:");
- var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '"';
-
- var requestXML = new XML('');
- requestXML.D::prop = '';
- // IIS 5.1 requires at least one property in PROPFIND
- requestXML.D::prop.D::getcontentlength = '';
-
- var xmlstr = prolog + requestXML.toXMLString();
-
- var self = this;
-
- // Test whether URL is WebDAV-enabled
- var request = Zotero.HTTP.doOptions(uri, function (req) {
- // Timeout
- if (req.status == 0) {
- self._checkResponse(req, self);
-
- callback(uri, Zotero.Sync.Storage.ERROR_UNREACHABLE);
- return;
- }
-
- Zotero.debug(req.getAllResponseHeaders());
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 400:
- callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- case 500:
- callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
- return;
- }
-
- var dav = req.getResponseHeader("DAV");
- if (dav == null) {
- callback(uri, Zotero.Sync.Storage.ERROR_NOT_DAV);
- return;
- }
-
- // Get the Authorization header used in case we need to do a request
- // on the parent below
- var channelAuthorization = Zotero.HTTP.getChannelAuthorization(req.channel);
-
- var headers = { Depth: 0 };
-
- // Test whether Zotero directory exists
- Zotero.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
+ /**
+ * Create a Zotero directory on the storage server
+ */
+ function createServerDirectory(callback) {
+ var uri = Zotero.Sync.Storage.Module.WebDAV.rootURI;
+ Zotero.HTTP.WebDAV.doMkCol(uri, function (req) {
Zotero.debug(req.responseText);
Zotero.debug(req.status);
switch (req.status) {
- case 207:
- // Test if Zotero directory is writable
- var testFileURI = uri.clone();
- testFileURI.spec += "zotero-test-file";
- Zotero.HTTP.WebDAV.doPut(testFileURI, "1", function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 200:
- case 201:
- case 204:
- Zotero.HTTP.doGet(
- testFileURI,
- function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 200:
- // Delete test file
- Zotero.HTTP.WebDAV.doDelete(
- testFileURI,
- function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 200: // IIS 5.1 and Sakai return 200
- case 204:
- callback(
- uri,
- Zotero.Sync.Storage.SUCCESS
- );
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- }
- );
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- // IIS 6+ configured not to serve extensionless files or .prop files
- // http://support.microsoft.com/kb/326965
- case 404:
- callback(uri, Zotero.Sync.Storage.ERROR_FILE_MISSING_AFTER_UPLOAD);
- return;
-
- case 500:
- callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- }
- );
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- case 500:
- callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- });
- return;
-
- case 400:
- callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
- return;
+ case 201:
+ callback(uri, Zotero.Sync.Storage.SUCCESS);
+ break;
case 401:
callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
@@ -1023,655 +405,1246 @@ Zotero.Sync.Storage.Session.WebDAV.prototype.checkServer = function (callback) {
callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
return;
- case 404:
- // Include Authorization header from /zotero request,
- // since Firefox probably won't apply it to the parent request
- var newHeaders = {};
- for (var header in headers) {
- newHeaders[header] = headers[header];
- }
- newHeaders["Authorization"] = channelAuthorization;
-
- // Zotero directory wasn't found, so see if at least
- // the parent directory exists
- Zotero.HTTP.WebDAV.doProp("PROPFIND", parentURI, xmlstr,
- function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- // Parent directory existed
- case 207:
- callback(uri, Zotero.Sync.Storage.ERROR_ZOTERO_DIR_NOT_FOUND);
- return;
-
- case 400:
- callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- // Parent directory wasn't found either
- case 404:
- callback(uri, Zotero.Sync.Storage.ERROR_PARENT_DIR_NOT_FOUND);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- }, newHeaders);
+ case 405:
+ callback(uri, Zotero.Sync.Storage.ERROR_NOT_ALLOWED);
return;
case 500:
callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
return;
-
+
default:
callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
return;
}
- }, headers);
- });
-
- if (!request) {
- callback(uri, Zotero.Sync.Storage.ERROR_OFFLINE);
+ });
}
- requestHolder.request = request;
- return requestHolder;
-}
-
-
-Zotero.Sync.Storage.Session.WebDAV.prototype.checkServerCallback = function (uri, status, window, skipSuccessMessage, e) {
- var promptService =
- Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
- createInstance(Components.interfaces.nsIPromptService);
- if (uri) {
- var spec = uri.scheme + '://' + uri.hostPort + uri.path;
+
+ /**
+ * Get the storage URI for an item
+ *
+ * @inner
+ * @param {Zotero.Item}
+ * @return {nsIURI} URI of file on storage server
+ */
+ function getItemURI(item) {
+ var uri = Zotero.Sync.Storage.Module.WebDAV.rootURI;
+ uri.spec = uri.spec + item.key + '.zip';
+ return uri;
}
- // If there's an error, just display that
- if (e) {
- promptService.alert(
- window,
- Zotero.getString('general.error'),
- e.toString()
- );
- return false;
+
+ /**
+ * Get the storage property file URI for an item
+ *
+ * @inner
+ * @param {Zotero.Item}
+ * @return {nsIURI} URI of property file on storage server
+ */
+ function getItemPropertyURI(item) {
+ var uri = Zotero.Sync.Storage.Module.WebDAV.rootURI;
+ uri.spec = uri.spec + item.key + '.prop';
+ return uri;
+ }
+
+
+ /**
+ * Get the storage property file URI corresponding to a given item storage URI
+ *
+ * @param {nsIURI} Item storage URI
+ * @return {nsIURI|FALSE} Property file URI, or FALSE if not an item storage URI
+ */
+ function getPropertyURIFromItemURI(uri) {
+ if (!uri.spec.match(/\.zip$/)) {
+ return false;
+ }
+ var propURI = uri.clone();
+ propURI.QueryInterface(Components.interfaces.nsIURL);
+ propURI.fileName = uri.fileName.replace(/\.zip$/, '.prop');
+ propURI.QueryInterface(Components.interfaces.nsIURI);
+ return propURI;
}
- switch (status) {
- case Zotero.Sync.Storage.SUCCESS:
- if (!skipSuccessMessage) {
- promptService.alert(
- window,
- Zotero.getString('sync.storage.serverConfigurationVerified'),
- Zotero.getString('sync.storage.fileSyncSetUp')
+
+ /**
+ * @inner
+ * @param {String[]} files Remote filenames to delete (e.g., ZIPs)
+ * @param {Function} callback Passed object containing three arrays:
+ * 'deleted', 'missing', and 'error',
+ * each containing filenames
+ */
+ function deleteStorageFiles(files, callback) {
+ var results = {
+ deleted: [],
+ missing: [],
+ error: []
+ };
+
+ if (files.length == 0) {
+ if (callback) {
+ callback(results);
+ }
+ return;
+ }
+
+ for (var i=0; i i) {
+ delete files[propIndex];
+ i--;
+ last = (i == files.length - 1);
+ }
+
+ // Delete property file
+ Zotero.HTTP.WebDAV.doDelete(deletePropURI, function (req) {
+ switch (req.status) {
+ case 204:
+ // IIS 5.1 and Sakai return 200
+ case 200:
+ results.deleted.push(fileName);
+ break;
+
+ case 404:
+ if (fileDeleted) {
+ results.deleted.push(fileName);
+ }
+ else {
+ results.missing.push(fileName);
+ }
+ break;
+
+ default:
+ var error = true;
+ }
+
+ if (last && callback) {
+ callback(results);
+ }
+
+ if (error) {
+ results.error.push(fileName);
+ var msg = "An error occurred attempting to delete "
+ + "'" + fileName
+ + "' (" + req.status + " " + req.statusText + ").";
+ Zotero.Sync.Storage.EventManager.error(msg);
+ }
+ });
+ });
+ }
+ }
+
+
+ /**
+ * Checks for an invalid SSL certificate and displays a nice error
+ */
+ function checkResponse(req) {
+ var channel = req.channel;
+ if (!channel instanceof Ci.nsIChannel) {
+ Zotero.Sync.Storage.EventManager.error('No HTTPS channel available');
+ }
+ var secInfo = channel.securityInfo;
+ if (secInfo instanceof Ci.nsITransportSecurityInfo) {
+ secInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE) == Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
+ var host = 'host';
+ try {
+ host = channel.URI.host;
+ }
+ catch (e) {
+ Zotero.debug(e);
+ }
+
+ var msg = Zotero.getString('sync.storage.error.webdav.sslCertificateError', host)
+ + " " + Zotero.getString('sync.storage.error.webdav.loadURLForMoreInfo');
+
+ Zotero.Sync.Storage.EventManager.error(msg);
+ }
+ else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) == Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
+ var msg = Zotero.localeJoin([
+ Zotero.getString('sync.storage.error.webdav.sslConnectionError', host),
+ Zotero.getString('sync.storage.error.webdav.loadURLForMoreInfo')
+ ]);
+ Zotero.Sync.Storage.EventManager.error(msg);
+ }
+ }
+ }
+
+
+ return {
+ name: "WebDAV",
- case Zotero.Sync.Storage.ERROR_NO_URL:
- var errorMessage = Zotero.getString('sync.storage.error.webdav.enterURL');
- break;
+ get includeUserFiles() {
+ return Zotero.Prefs.get("sync.storage.enabled") && Zotero.Prefs.get("sync.storage.protocol") == 'webdav';
+ },
+ includeGroupItems: false,
- case Zotero.Sync.Storage.ERROR_NO_PASSWORD:
- var errorMessage = Zotero.getString('sync.error.enterPassword');
- break;
+ get enabled() {
+ return this.includeUserFiles;
+ },
- case Zotero.Sync.Storage.ERROR_UNREACHABLE:
- var errorMessage = Zotero.getString('sync.storage.error.serverCouldNotBeReached', uri.host);
- break;
+ get verified() {
+ return Zotero.Prefs.get("sync.storage.verified");
+ },
- case Zotero.Sync.Storage.ERROR_NOT_DAV:
- var errorMessage = Zotero.getString('sync.storage.error.webdav.invalidURL', spec);
- break;
+ get username() {
+ return Zotero.Prefs.get('sync.storage.username');
+ },
- case Zotero.Sync.Storage.ERROR_AUTH_FAILED:
- var errorTitle = Zotero.getString('general.permissionDenied');
- var errorMessage = Zotero.localeJoin([
- Zotero.getString('sync.storage.error.webdav.invalidLogin'),
- Zotero.getString('sync.storage.error.checkFileSyncSettings')
- ]);
- break;
-
- case Zotero.Sync.Storage.ERROR_FORBIDDEN:
- var errorTitle = Zotero.getString('general.permissionDenied');
- var errorMessage = Zotero.localeJoin([
- Zotero.getString('sync.storage.error.webdav.permissionDenied', uri.path),
- Zotero.getString('sync.storage.error.checkFileSyncSettings')
- ]);
- break;
-
- case Zotero.Sync.Storage.ERROR_PARENT_DIR_NOT_FOUND:
- var errorTitle = Zotero.getString('sync.storage.error.directoryNotFound');
- var parentSpec = spec.replace(/\/zotero\/$/, "");
- var errorMessage = Zotero.getString('sync.storage.error.doesNotExist', parentSpec);
- break;
-
- case Zotero.Sync.Storage.ERROR_ZOTERO_DIR_NOT_FOUND:
- var create = promptService.confirmEx(
- window,
- Zotero.getString('sync.storage.error.directoryNotFound'),
- Zotero.getString('sync.storage.error.doesNotExist', spec) + "\n\n"
- + Zotero.getString('sync.storage.error.createNow'),
- promptService.BUTTON_POS_0
- * promptService.BUTTON_TITLE_IS_STRING
- + promptService.BUTTON_POS_1
- * promptService.BUTTON_TITLE_CANCEL,
- Zotero.getString('general.create'),
- null, null, null, {}
- );
+ get password() {
+ var username = this.username;
- if (create != 0) {
+ if (!username) {
+ Zotero.debug('Username not set before getting Zotero.Sync.Storage.Module.WebDAV.password');
+ return '';
+ }
+
+ Zotero.debug('Getting WebDAV password');
+ var loginManager = Components.classes["@mozilla.org/login-manager;1"]
+ .getService(Components.interfaces.nsILoginManager);
+ var logins = loginManager.findLogins({}, _loginManagerHost, _loginManagerURL, null);
+
+ // Find user from returned array of nsILoginInfo objects
+ for (var i = 0; i < logins.length; i++) {
+ if (logins[i].username == username) {
+ return logins[i].password;
+ }
+ }
+
+ return '';
+ },
+
+ set password(password) {
+ var username = this.username;
+ if (!username) {
+ Zotero.debug('Username not set before setting Zotero.Sync.Server.Module.WebDAV.password');
return;
}
- this._createServerDirectory(function (uri, status) {
- switch (status) {
- case Zotero.Sync.Storage.SUCCESS:
- if (!skipSuccessMessage) {
- promptService.alert(
- window,
- Zotero.getString('sync.storage.serverConfigurationVerified'),
- Zotero.getString('sync.storage.fileSyncSetUp')
- );
- }
- Zotero.Prefs.set("sync.storage.verified", true);
- return true;
-
- case Zotero.Sync.Storage.ERROR_FORBIDDEN:
- var errorTitle = Zotero.getString('general.permissionDenied');
- var errorMessage = Zotero.getString('sync.storage.error.permissionDeniedAtAddress') + "\n\n"
- + spec + "\n\n"
- + Zotero.getString('sync.storage.error.checkFileSyncSettings');
- break;
+ _cachedCredentials = false;
+
+ var loginManager = Components.classes["@mozilla.org/login-manager;1"]
+ .getService(Components.interfaces.nsILoginManager);
+ var logins = loginManager.findLogins({}, _loginManagerHost, _loginManagerURL, null);
+
+ for (var i = 0; i < logins.length; i++) {
+ Zotero.debug('Clearing WebDAV passwords');
+ loginManager.removeLogin(logins[i]);
+ break;
+ }
+
+ if (password) {
+ Zotero.debug(_loginManagerURL);
+ var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Components.interfaces.nsILoginInfo, "init");
+ var loginInfo = new nsLoginInfo(_loginManagerHost, _loginManagerURL,
+ null, username, password, "", "");
+ loginManager.addLogin(loginInfo);
+ }
+ },
+
+ get rootURI() {
+ if (!_rootURI) {
+ throw new Error("Root URI not initialized");
+ }
+ return _rootURI.clone();
+ },
+
+ get parentURI() {
+ if (!_parentURI) {
+ throw new Error("Parent URI not initialized");
+ }
+ return _parentURI.clone();
+ },
+
+
+ init: function (url, dir, username, password) {
+ if (!url) {
+ var msg = "WebDAV URL not provided";
+ Zotero.debug(msg);
+ throw ({
+ message: msg,
+ name: "Z_ERROR_NO_URL",
+ filename: "webdav.js",
+ toString: function () { return this.message; }
+ });
+ }
+
+ if (username && !password) {
+ var msg = "WebDAV password not provided";
+ Zotero.debug(msg);
+ throw ({
+ message: msg,
+ name: "Z_ERROR_NO_PASSWORD",
+ filename: "webdav.js",
+ toString: function () { return this.message; }
+ });
+ }
+
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ try {
+ var uri = ios.newURI(url, null, null);
+ if (username) {
+ uri.username = username;
+ uri.password = password;
+ }
+ }
+ catch (e) {
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ return false;
+ }
+ if (!uri.spec.match(/\/$/)) {
+ uri.spec += "/";
+ }
+ _parentURI = uri;
+
+ var uri = uri.clone();
+ uri.spec += "zotero/";
+ _rootURI = uri;
+ return true;
+ },
+
+
+ initFromPrefs: function () {
+ var scheme = Zotero.Prefs.get('sync.storage.scheme');
+ switch (scheme) {
+ case 'http':
+ case 'https':
+ break;
+
+ default:
+ throw new Error("Invalid WebDAV scheme '" + scheme + "'");
+ }
+
+ var url = Zotero.Prefs.get('sync.storage.url');
+ if (!url) {
+ return false;
+ }
+
+ url = scheme + '://' + url;
+ var dir = "zotero";
+ var username = this.username;
+ var password = this.password;
+
+ return this.init(url, dir, username, password);
+ },
+
+
+ /**
+ * Begin download process for individual file
+ *
+ * @param {Zotero.Sync.Storage.Request} [request]
+ */
+ downloadFile: function (request) {
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+ if (!item) {
+ throw new Error("Item '" + request.name + "' not found");
+ }
+
+ // Retrieve modification time from server to store locally afterwards
+ getStorageModificationTime(item, function (item, mdate) {
+ if (!request.isRunning()) {
+ Zotero.debug("Download request '" + request.name
+ + "' is no longer running after getting mod time");
+ return;
}
+ if (!mdate) {
+ Zotero.debug("Remote file not found for item " + Zotero.Items.getLibraryKeyHash(item));
+ request.finish();
+ return;
+ }
+
+ try {
+ var syncModTime = mdate.getTime();
+
+ // Skip download if local file exists and matches mod time
+ var file = item.getFile();
+ if (file && file.exists() && syncModTime == file.lastModifiedTime) {
+ Zotero.debug("File mod time matches remote file -- skipping download");
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ var updateItem = syncState != 1;
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ onChangesMade();
+ request.finish();
+ return;
+ }
+
+ var uri = getItemURI(item);
+ var destFile = Zotero.getTempDirectory();
+ destFile.append(item.key + '.zip.tmp');
+ if (destFile.exists()) {
+ destFile.remove(false);
+ }
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onStart: function (request, data) {
+ if (data.request.isFinished()) {
+ Zotero.debug("Download request " + data.request.name
+ + " stopped before download started -- closing channel");
+ request.cancel(0x804b0002); // NS_BINDING_ABORTED
+ return;
+ }
+ },
+ onProgress: function (a, b, c) {
+ request.onProgress(a, b, c)
+ },
+ onStop: function (request, status, response, data) {
+ if (status == 404) {
+ var msg = "Remote ZIP file not found for item " + item.key;
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg);
+
+ // Delete the orphaned prop file
+ deleteStorageFiles([item.key + ".prop"]);
+
+ data.request.finish();
+ return;
+ }
+ else if (status != 200) {
+ var msg = "Unexpected status code " + status
+ + " for request " + data.request.name
+ + " in Zotero.Sync.Storage.Module.WebDAV.downloadFile()";
+ Zotero.debug(msg, 1);
+ Components.utils.reportError(msg);
+ Zotero.Sync.Storage.EventManager.error(_defaultError);
+ }
+
+ // Don't try to process if the request has been cancelled
+ if (data.request.isFinished()) {
+ Zotero.debug("Download request " + data.request.name
+ + " is no longer running after file download");
+ return;
+ }
+
+ Zotero.debug("Finished download of " + destFile.path);
+
+ try {
+ Zotero.Sync.Storage.processDownload(data);
+ data.request.finish();
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+ },
+ request: request,
+ item: item,
+ compressed: true,
+ syncModTime: syncModTime
+ }
+ );
+
+ // Don't display password in console
+ var disp = uri.clone();
+ if (disp.password) {
+ disp.password = '********';
+ }
+ Zotero.debug('Saving ' + disp.spec + ' with saveURI()');
+ const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
+ var wbp = Components
+ .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(nsIWBP);
+ wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
+ wbp.progressListener = listener;
+ wbp.saveURI(uri, null, null, null, null, destFile);
+ }
+ catch (e) {
+ request.error(e);
+ }
+ });
+ },
+
+
+ uploadFile: function (request) {
+ Zotero.Sync.Storage.createUploadFile(request, function (data) { processUploadFile(data); });
+ },
+
+
+ getLastSyncTime: function (callback) {
+ // Cache the credentials at the root URI
+ var self = this;
+ this.cacheCredentials(function () {
+ try {
+ var uri = this.rootURI;
+ var successFileURI = uri.clone();
+ successFileURI.spec += "lastsync";
+ Zotero.HTTP.doGet(successFileURI, function (req) {
+ var ts = undefined;
+ try {
+ if (req.responseText) {
+ Zotero.debug(req.responseText);
+ }
+ Zotero.debug(req.status);
+
+ if (req.status == 403) {
+ Zotero.debug("Clearing WebDAV authentication credentials", 2);
+ _cachedCredentials = false;
+ }
+
+ if (req.status != 200 && req.status != 404) {
+ var msg = "Unexpected status code " + req.status + " for HEAD request "
+ + "in Zotero.Sync.Storage.Module.WebDAV.getLastSyncTime()";
+ Zotero.debug(msg, 1);
+ Components.utils.reportError(msg);
+ Zotero.Sync.Storage.EventManager.error(_defaultError);
+ }
+
+ if (req.status == 200) {
+ var lastModified = req.getResponseHeader("Last-Modified");
+ var date = new Date(lastModified);
+ Zotero.debug("Last successful storage sync was " + date);
+ ts = Zotero.Date.toUnixTimestamp(date);
+ }
+ else {
+ ts = null;
+ }
+ }
+ finally {
+ callback(ts);
+ }
+ });
+ return;
+ }
+ catch (e) {
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ callback();
+ return;
+ }
+ });
+ },
+
+
+ setLastSyncTime: function (callback) {
+ try {
+ var uri = this.rootURI;
+ var successFileURI = uri.clone();
+ successFileURI.spec += "lastsync";
+
+ Zotero.HTTP.WebDAV.doPut(successFileURI, " ", function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 200:
+ case 201:
+ case 204:
+ getLastSyncTime(function (ts) {
+ if (ts) {
+ var sql = "REPLACE INTO version VALUES ('storage_webdav', ?)";
+ Zotero.DB.query(sql, { int: ts });
+ }
+ if (callback) {
+ callback();
+ }
+ });
+ return;
+ }
+
+ var msg = "Unexpected error code " + req.status + " uploading storage success file";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg);
+ if (callback) {
+ callback();
+ }
+ });
+ }
+ catch (e) {
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ if (callback) {
+ callback();
+ }
+ return;
+ }
+ },
+
+
+ cacheCredentials: function (callback) {
+ if (_cachedCredentials) {
+ Zotero.debug("Credentials are already cached");
+ setTimeout(function () {
+ callback();
+ }, 0);
+ return false;
+ }
+
+ Zotero.HTTP.doOptions(this.rootURI, function (req) {
+ checkResponse(req);
+
+ if (req.status != 200) {
+ var msg = "Unexpected status code " + req.status + " for OPTIONS request "
+ + "in Zotero.Sync.Storage.Module.WebDAV.getLastSyncTime()";
+ Zotero.debug(msg, 1);
+ Components.utils.reportError(msg);
+ Zotero.Sync.Storage.EventManager.error(_defaultErrorRestart);
+ }
+ Zotero.debug("Credentials are cached");
+ _cachedCredentials = true;
+ callback();
+ });
+ return true;
+ },
+
+
+ /**
+ * @param {Function} callback Function to pass URI and result value to
+ * @param {Object} errorCallbacks
+ */
+ checkServer: function (callback) {
+ this.initFromPrefs();
+
+ try {
+ var parentURI = this.parentURI;
+ var uri = this.rootURI;
+ }
+ catch (e) {
+ switch (e.name) {
+ case 'Z_ERROR_NO_URL':
+ callback(null, Zotero.Sync.Storage.ERROR_NO_URL);
+ return;
+
+ case 'Z_ERROR_NO_PASSWORD':
+ callback(null, Zotero.Sync.Storage.ERROR_NO_PASSWORD);
+ return;
+
+ default:
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ callback(null, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ }
+
+ var requestHolder = { request: null };
+
+ var prolog = '\n';
+ var D = new Namespace("D", "DAV:");
+ var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '"';
+
+ var requestXML = new XML('');
+ requestXML.D::prop = '';
+ // IIS 5.1 requires at least one property in PROPFIND
+ requestXML.D::prop.D::getcontentlength = '';
+
+ var xmlstr = prolog + requestXML.toXMLString();
+
+ // Test whether URL is WebDAV-enabled
+ var request = Zotero.HTTP.doOptions(uri, function (req) {
+ // Timeout
+ if (req.status == 0) {
+ checkResponse(req);
+
+ callback(uri, Zotero.Sync.Storage.ERROR_UNREACHABLE);
+ return;
+ }
+
+ Zotero.debug(req.getAllResponseHeaders());
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 400:
+ callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ case 500:
+ callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
+ return;
+ }
+
+ var dav = req.getResponseHeader("DAV");
+ if (dav == null) {
+ callback(uri, Zotero.Sync.Storage.ERROR_NOT_DAV);
+ return;
+ }
+
+ // Get the Authorization header used in case we need to do a request
+ // on the parent below
+ var channelAuthorization = Zotero.HTTP.getChannelAuthorization(req.channel);
+
+ var headers = { Depth: 0 };
+
+ // Test whether Zotero directory exists
+ Zotero.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 207:
+ // Test if Zotero directory is writable
+ var testFileURI = uri.clone();
+ testFileURI.spec += "zotero-test-file";
+ Zotero.HTTP.WebDAV.doPut(testFileURI, " ", function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 200:
+ case 201:
+ case 204:
+ Zotero.HTTP.doGet(
+ testFileURI,
+ function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 200:
+ // Delete test file
+ Zotero.HTTP.WebDAV.doDelete(
+ testFileURI,
+ function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 200: // IIS 5.1 and Sakai return 200
+ case 204:
+ callback(
+ uri,
+ Zotero.Sync.Storage.SUCCESS
+ );
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ }
+ );
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ // IIS 6+ configured not to serve extensionless files or .prop files
+ // http://support.microsoft.com/kb/326965
+ case 404:
+ callback(uri, Zotero.Sync.Storage.ERROR_FILE_MISSING_AFTER_UPLOAD);
+ return;
+
+ case 500:
+ callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ }
+ );
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ case 500:
+ callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ });
+ return;
+
+ case 400:
+ callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ case 404:
+ // Include Authorization header from /zotero request,
+ // since Firefox probably won't apply it to the parent request
+ var newHeaders = {};
+ for (var header in headers) {
+ newHeaders[header] = headers[header];
+ }
+ newHeaders["Authorization"] = channelAuthorization;
+
+ // Zotero directory wasn't found, so see if at least
+ // the parent directory exists
+ Zotero.HTTP.WebDAV.doProp("PROPFIND", this.parentURI, xmlstr,
+ function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ // Parent directory existed
+ case 207:
+ callback(uri, Zotero.Sync.Storage.ERROR_ZOTERO_DIR_NOT_FOUND);
+ return;
+
+ case 400:
+ callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ // Parent directory wasn't found either
+ case 404:
+ callback(uri, Zotero.Sync.Storage.ERROR_PARENT_DIR_NOT_FOUND);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ }, newHeaders);
+ return;
+
+ case 500:
+ callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ }, headers);
+ });
+
+ if (!request) {
+ callback(uri, Zotero.Sync.Storage.ERROR_OFFLINE);
+ }
+
+ requestHolder.request = request;
+ return requestHolder;
+ },
+
+
+ checkServerCallback: function (uri, status, window, skipSuccessMessage) {
+ var promptService =
+ Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
+ createInstance(Components.interfaces.nsIPromptService);
+ if (uri) {
+ var spec = uri.scheme + '://' + uri.hostPort + uri.path;
+ }
+
+ switch (status) {
+ case Zotero.Sync.Storage.SUCCESS:
+ if (!skipSuccessMessage) {
+ promptService.alert(
+ window,
+ Zotero.getString('sync.storage.serverConfigurationVerified'),
+ Zotero.getString('sync.storage.fileSyncSetUp')
+ );
+ }
+ Zotero.Prefs.set("sync.storage.verified", true);
+ return true;
+
+ case Zotero.Sync.Storage.ERROR_NO_URL:
+ var errorMessage = Zotero.getString('sync.storage.error.webdav.enterURL');
+ break;
+
+ case Zotero.Sync.Storage.ERROR_NO_PASSWORD:
+ var errorMessage = Zotero.getString('sync.error.enterPassword');
+ break;
+
+ case Zotero.Sync.Storage.ERROR_UNREACHABLE:
+ var errorMessage = Zotero.getString('sync.storage.error.serverCouldNotBeReached', uri.host);
+ break;
+
+ case Zotero.Sync.Storage.ERROR_NOT_DAV:
+ var errorMessage = Zotero.getString('sync.storage.error.webdav.invalidURL', spec);
+ break;
+
+ case Zotero.Sync.Storage.ERROR_AUTH_FAILED:
+ var errorTitle = Zotero.getString('general.permissionDenied');
+ var errorMessage = Zotero.localeJoin([
+ Zotero.getString('sync.storage.error.webdav.invalidLogin'),
+ Zotero.getString('sync.storage.error.checkFileSyncSettings')
+ ]);
+ break;
+
+ case Zotero.Sync.Storage.ERROR_FORBIDDEN:
+ var errorTitle = Zotero.getString('general.permissionDenied');
+ var errorMessage = Zotero.localeJoin([
+ Zotero.getString('sync.storage.error.webdav.permissionDenied', uri.path),
+ Zotero.getString('sync.storage.error.checkFileSyncSettings')
+ ]);
+ break;
+
+ case Zotero.Sync.Storage.ERROR_PARENT_DIR_NOT_FOUND:
+ var errorTitle = Zotero.getString('sync.storage.error.directoryNotFound');
+ var parentSpec = spec.replace(/\/zotero\/$/, "");
+ var errorMessage = Zotero.getString('sync.storage.error.doesNotExist', parentSpec);
+ break;
+
+ case Zotero.Sync.Storage.ERROR_ZOTERO_DIR_NOT_FOUND:
+ var create = promptService.confirmEx(
+ window,
+ Zotero.getString('sync.storage.error.directoryNotFound'),
+ Zotero.getString('sync.storage.error.doesNotExist', spec) + "\n\n"
+ + Zotero.getString('sync.storage.error.createNow'),
+ promptService.BUTTON_POS_0
+ * promptService.BUTTON_TITLE_IS_STRING
+ + promptService.BUTTON_POS_1
+ * promptService.BUTTON_TITLE_CANCEL,
+ Zotero.getString('general.create'),
+ null, null, null, {}
+ );
+
+ if (create != 0) {
+ return;
+ }
+
+ createServerDirectory(function (uri, status) {
+ switch (status) {
+ case Zotero.Sync.Storage.SUCCESS:
+ if (!skipSuccessMessage) {
+ promptService.alert(
+ window,
+ Zotero.getString('sync.storage.serverConfigurationVerified'),
+ Zotero.getString('sync.storage.fileSyncSetUp')
+ );
+ }
+ Zotero.Prefs.set("sync.storage.verified", true);
+ return true;
+
+ case Zotero.Sync.Storage.ERROR_FORBIDDEN:
+ var errorTitle = Zotero.getString('general.permissionDenied');
+ var errorMessage = Zotero.getString('sync.storage.error.permissionDeniedAtAddress') + "\n\n"
+ + spec + "\n\n"
+ + Zotero.getString('sync.storage.error.checkFileSyncSettings');
+ break;
+ }
+
+ // TEMP
+ if (!errorMessage) {
+ var errorMessage = status;
+ }
+ promptService.alert(window, errorTitle, errorMessage);
+ });
+
+ return false;
+
+ case Zotero.Sync.Storage.ERROR_FILE_MISSING_AFTER_UPLOAD:
+ // TODO: localize
+ var errorTitle = "WebDAV Server Configuration Error";
+ var errorMessage = "Your WebDAV server must be configured to serve files without extensions "
+ + "and files with .prop extensions in order to work with Zotero.";
+ break;
+
+ case Zotero.Sync.Storage.ERROR_SERVER_ERROR:
+ // TODO: localize
+ var errorTitle = "WebDAV Server Configuration Error";
+ var errorMessage = "Your WebDAV server returned an internal error."
+ + "\n\n" + Zotero.getString('sync.storage.error.checkFileSyncSettings');
+ break;
+
+ case Zotero.Sync.Storage.ERROR_UNKNOWN:
+ var errorMessage = Zotero.localeJoin([
+ Zotero.getString('general.unknownErrorOccurred'),
+ Zotero.getString('sync.storage.error.checkFileSyncSettings')
+ ]);
+ break;
+ }
+
+ if (!skipSuccessMessage) {
+ if (!errorTitle) {
+ var errorTitle = Zotero.getString("general.error");
+ }
// TEMP
if (!errorMessage) {
var errorMessage = status;
}
promptService.alert(window, errorTitle, errorMessage);
- });
-
+ }
return false;
+ },
- case Zotero.Sync.Storage.ERROR_FILE_MISSING_AFTER_UPLOAD:
- // TODO: localize
- var errorTitle = "WebDAV Server Configuration Error";
- var errorMessage = "Your WebDAV server must be configured to serve files without extensions "
- + "and files with .prop extensions in order to work with Zotero.";
- break;
- case Zotero.Sync.Storage.ERROR_SERVER_ERROR:
- // TODO: localize
- var errorTitle = "WebDAV Server Configuration Error";
- var errorMessage = "Your WebDAV server returned an internal error."
- + "\n\n" + Zotero.getString('sync.storage.error.checkFileSyncSettings');
- break;
-
- case Zotero.Sync.Storage.ERROR_UNKNOWN:
- var errorMessage = Zotero.localeJoin([
- Zotero.getString('general.unknownErrorOccurred'),
- Zotero.getString('sync.storage.error.checkFileSyncSettings')
- ]);
- break;
- }
-
- if (!skipSuccessMessage) {
- if (!errorTitle) {
- var errorTitle = Zotero.getString("general.error");
- }
- // TEMP
- if (!errorMessage) {
- var errorMessage = status;
- }
- promptService.alert(window, errorTitle, errorMessage);
- }
- return false;
-}
-
-
-/**
- * Remove files on storage server that were deleted locally more than
- * sync.storage.deleteDelayDays days ago
- *
- * @param {Function} callback Passed number of files deleted
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype.purgeDeletedStorageFiles = function (callback) {
- if (!this.active) {
- return;
- }
-
- Zotero.debug("Purging deleted storage files");
- var files = Zotero.Sync.Storage.getDeletedFiles();
- if (!files) {
- Zotero.debug("No files to delete remotely");
- if (callback) {
- callback();
- }
- return;
- }
-
- // Add .zip extension
- var files = files.map(function (file) file + ".zip");
-
- this._deleteStorageFiles(files, function (results) {
- // Remove deleted and nonexistent files from storage delete log
- var toPurge = results.deleted.concat(results.missing);
- if (toPurge.length > 0) {
- var done = 0;
- var maxFiles = 999;
- var numFiles = toPurge.length;
-
- Zotero.DB.beginTransaction();
-
- do {
- var chunk = toPurge.splice(0, maxFiles);
- var sql = "DELETE FROM storageDeleteLog WHERE key IN ("
- + chunk.map(function () '?').join() + ")";
- Zotero.DB.query(sql, chunk);
- done += chunk.length;
- }
- while (done < numFiles);
-
- Zotero.DB.commitTransaction();
- }
-
- if (callback) {
- callback(results.deleted.length);
- }
- });
-}
-
-
-/**
- * Delete orphaned storage files older than a day before last sync time
- *
- * @param {Function} callback
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype.purgeOrphanedStorageFiles = function (callback) {
- const daysBeforeSyncTime = 1;
-
- if (!this.active) {
- return;
- }
-
- // If recently purged, skip
- var lastpurge = Zotero.Prefs.get('lastWebDAVOrphanPurge');
- var days = 10;
- if (lastpurge && new Date(lastpurge * 1000) > (new Date() - (1000 * 60 * 60 * 24 * days))) {
- return;
- }
-
- Zotero.debug("Purging orphaned storage files");
-
- var uri = this.rootURI;
- var path = uri.path;
-
- var prolog = '\n';
- var D = new Namespace("D", "DAV:");
- var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '"';
-
- var requestXML = new XML('');
- requestXML.D::prop = '';
- requestXML.D::prop.D::getlastmodified = '';
-
- var xmlstr = prolog + requestXML.toXMLString();
-
- var lastSyncDate = new Date(Zotero.Sync.Server.lastLocalSyncTime * 1000);
-
- var self = this;
-
- Zotero.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
- Zotero.debug(req.responseText);
-
- var funcName = "Zotero.Sync.Storage.purgeOrphanedStorageFiles()";
-
- // Strip XML declaration and convert to E4X
- var xml = new XML(req.responseText.replace(/<\?xml.*\?>/, ''));
-
- var deleteFiles = [];
- var trailingSlash = !!path.match(/\/$/);
- for each(var response in xml.D::response) {
- var href = response.D::href.toString();
-
- // Strip trailing slash if there isn't one on the root path
- if (!trailingSlash) {
- href = href.replace(/\/$/, "")
- }
-
- // Absolute
- if (href.match(/^https?:\/\//)) {
- var ios = Components.classes["@mozilla.org/network/io-service;1"].
- getService(Components.interfaces.nsIIOService);
- var href = ios.newURI(href, null, null);
- href = href.path;
- }
-
- // Skip root URI
- if (href == path
- // Some Apache servers respond with a "/zotero" href
- // even for a "/zotero/" request
- || (trailingSlash && href + '/' == path)
- // Try URL-encoded as well, as above
- || decodeURIComponent(href) == path) {
- continue;
- }
-
- if (href.indexOf(path) == -1
- // Try URL-encoded as well, in case there's a '~' or similar
- // character in the URL and the server (e.g., Sakai) is
- // encoding the value
- && decodeURIComponent(href).indexOf(path) == -1) {
- self.onError("DAV:href '" + href
- + "' does not begin with path '" + path + "' in " + funcName);
- }
-
- var matches = href.match(/[^\/]+$/);
- if (!matches) {
- self.onError("Unexpected href '" + href + "' in " + funcName)
- }
- var file = matches[0];
-
- if (file.indexOf('.') == 0) {
- Zotero.debug("Skipping hidden file " + file);
- continue;
- }
- if (!file.match(/\.zip$/) && !file.match(/\.prop$/)) {
- Zotero.debug("Skipping file " + file);
- continue;
- }
-
- var key = file.replace(/\.(zip|prop)$/, '');
- var item = Zotero.Items.getByLibraryAndKey(null, key);
- if (item) {
- Zotero.debug("Skipping existing file " + file);
- continue;
- }
-
- Zotero.debug("Checking orphaned file " + file);
-
- // TODO: Parse HTTP date properly
- var lastModified = response..*::getlastmodified.toString();
- lastModified = Zotero.Date.strToISO(lastModified);
- lastModified = Zotero.Date.sqlToDate(lastModified);
-
- // Delete files older than a day before last sync time
- var days = (lastSyncDate - lastModified) / 1000 / 60 / 60 / 24;
-
- if (days > daysBeforeSyncTime) {
- deleteFiles.push(file);
- }
- }
-
- self._deleteStorageFiles(deleteFiles, function (results) {
- Zotero.Prefs.set("lastWebDAVOrphanPurge", Math.round(new Date().getTime() / 1000))
- if (callback) {
- callback(results);
- }
- });
- },
- { Depth: 1 });
-}
-
-
-/**
- * Create a Zotero directory on the storage server
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype._createServerDirectory = function (callback) {
- var uri = this.rootURI;
- Zotero.HTTP.WebDAV.doMkCol(uri, function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 201:
- callback(uri, Zotero.Sync.Storage.SUCCESS);
- break;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ /**
+ * Remove files on storage server that were deleted locally more than
+ * sync.storage.deleteDelayDays days ago
+ *
+ * @param {Function} callback Passed number of files deleted
+ */
+ purgeDeletedStorageFiles: function (callback) {
+ if (!this.active) {
return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- case 405:
- callback(uri, Zotero.Sync.Storage.ERROR_NOT_ALLOWED);
- return;
-
- case 500:
- callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- });
-}
-
-
-
-//
-// Private methods
-//
-
-/**
- * Get the storage URI for an item
- *
- * @inner
- * @param {Zotero.Item}
- * @return {nsIURI} URI of file on storage server
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype._getItemURI = function (item) {
- var uri = this.rootURI;
- uri.spec = uri.spec + item.key + '.zip';
- return uri;
-}
-
-
-/**
- * Get the storage property file URI for an item
- *
- * @inner
- * @param {Zotero.Item}
- * @return {nsIURI} URI of property file on storage server
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype._getItemPropertyURI = function (item) {
- var uri = this.rootURI;
- uri.spec = uri.spec + item.key + '.prop';
- return uri;
-}
-
-
-/**
- * Get the storage property file URI corresponding to a given item storage URI
- *
- * @param {nsIURI} Item storage URI
- * @return {nsIURI|FALSE} Property file URI, or FALSE if not an item storage URI
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype._getPropertyURIFromItemURI = function (uri) {
- if (!uri.spec.match(/\.zip$/)) {
- return false;
- }
- var propURI = uri.clone();
- propURI.QueryInterface(Components.interfaces.nsIURL);
- propURI.fileName = uri.fileName.replace(/\.zip$/, '.prop');
- propURI.QueryInterface(Components.interfaces.nsIURI);
- return propURI;
-}
-
-
-/**
- * @inner
- * @param {String[]} files Remote filenames to delete (e.g., ZIPs)
- * @param {Function} callback Passed object containing three arrays:
- * 'deleted', 'missing', and 'error',
- * each containing filenames
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype._deleteStorageFiles = function (files, callback, session) {
- var results = {
- deleted: [],
- missing: [],
- error: []
- };
-
- if (files.length == 0) {
- if (callback) {
- callback(results);
- }
- return;
- }
-
- var self = session ? session : this;
-
- for (var i=0; i 0) {
+ var done = 0;
+ var maxFiles = 999;
+ var numFiles = toPurge.length;
+
+ Zotero.DB.beginTransaction();
+
+ do {
+ var chunk = toPurge.splice(0, maxFiles);
+ var sql = "DELETE FROM storageDeleteLog WHERE key IN ("
+ + chunk.map(function () '?').join() + ")";
+ Zotero.DB.query(sql, chunk);
+ done += chunk.length;
+ }
+ while (done < numFiles);
+
+ Zotero.DB.commitTransaction();
+ }
- case 404:
- var fileDeleted = false;
- break;
+ if (callback) {
+ callback(results.deleted.length);
+ }
- default:
- if (last && callback) {
- callback(results);
+ Zotero.Sync.Storage.EventManager.success();
+ });
+ },
+
+
+ /**
+ * Delete orphaned storage files older than a day before last sync time
+ *
+ * @param {Function} callback
+ */
+ purgeOrphanedStorageFiles: function (callback) {
+ const daysBeforeSyncTime = 1;
+
+ if (!this.active) {
+ Zotero.Sync.Storage.EventManager.skip();
+ return;
+ }
+
+ // If recently purged, skip
+ var lastpurge = Zotero.Prefs.get('lastWebDAVOrphanPurge');
+ var days = 10;
+ if (lastpurge && new Date(lastpurge * 1000) > (new Date() - (1000 * 60 * 60 * 24 * days))) {
+ Zotero.Sync.Storage.EventManager.skip();
+ return;
+ }
+
+ Zotero.debug("Purging orphaned storage files");
+
+ var uri = this.rootURI;
+ var path = uri.path;
+
+ var prolog = '\n';
+ var D = new Namespace("D", "DAV:");
+ var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '"';
+
+ var requestXML = new XML('');
+ requestXML.D::prop = '';
+ requestXML.D::prop.D::getlastmodified = '';
+
+ var xmlstr = prolog + requestXML.toXMLString();
+
+ var lastSyncDate = new Date(Zotero.Sync.Server.lastLocalSyncTime * 1000);
+
+ Zotero.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
+ Zotero.debug(req.responseText);
+
+ var funcName = "Zotero.Sync.Storage.purgeOrphanedStorageFiles()";
+
+ // Strip XML declaration and convert to E4X
+ var xml = new XML(req.responseText.replace(/<\?xml.*\?>/, ''));
+
+ var deleteFiles = [];
+ var trailingSlash = !!path.match(/\/$/);
+ for each(var response in xml.D::response) {
+ var href = response.D::href.toString();
+
+ // Strip trailing slash if there isn't one on the root path
+ if (!trailingSlash) {
+ href = href.replace(/\/$/, "")
}
- results.error.push(fileName);
- var msg = "An error occurred attempting to delete "
- + "'" + fileName
- + "' (" + req.status + " " + req.statusText + ").";
- self.onError(msg);
- return;
- }
-
- // If an item file URI, get the property URI
- var deletePropURI = self._getPropertyURIFromItemURI(deleteURI);
- if (!deletePropURI) {
- if (fileDeleted) {
- results.deleted.push(fileName);
- }
- else {
- results.missing.push(fileName);
- }
- if (last && callback) {
- callback(results);
- }
- return;
- }
-
- // If property file appears separately in delete queue,
- // remove it, since we're taking care of it here
- var propIndex = files.indexOf(deletePropURI.fileName);
- if (propIndex > i) {
- delete files[propIndex];
- i--;
- last = (i == files.length - 1);
- }
-
- // Delete property file
- Zotero.HTTP.WebDAV.doDelete(deletePropURI, function (req) {
- switch (req.status) {
- case 204:
- // IIS 5.1 and Sakai return 200
- case 200:
- results.deleted.push(fileName);
- break;
+ // Absolute
+ if (href.match(/^https?:\/\//)) {
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ var href = ios.newURI(href, null, null);
+ href = href.path;
+ }
- case 404:
- if (fileDeleted) {
- results.deleted.push(fileName);
- }
- else {
- results.missing.push(fileName);
- }
- break;
+ // Skip root URI
+ if (href == path
+ // Some Apache servers respond with a "/zotero" href
+ // even for a "/zotero/" request
+ || (trailingSlash && href + '/' == path)
+ // Try URL-encoded as well, as above
+ || decodeURIComponent(href) == path) {
+ continue;
+ }
- default:
- var error = true;
+ if (href.indexOf(path) == -1
+ // Try URL-encoded as well, in case there's a '~' or similar
+ // character in the URL and the server (e.g., Sakai) is
+ // encoding the value
+ && decodeURIComponent(href).indexOf(path) == -1) {
+ Zotero.Sync.Storage.EventManager.error(
+ "DAV:href '" + href + "' does not begin with path '"
+ + path + "' in " + funcName
+ );
+ }
+
+ var matches = href.match(/[^\/]+$/);
+ if (!matches) {
+ Zotero.Sync.Storage.EventManager.error(
+ "Unexpected href '" + href + "' in " + funcName
+ )
+ }
+ var file = matches[0];
+
+ if (file.indexOf('.') == 0) {
+ Zotero.debug("Skipping hidden file " + file);
+ continue;
+ }
+ if (!file.match(/\.zip$/) && !file.match(/\.prop$/)) {
+ Zotero.debug("Skipping file " + file);
+ continue;
+ }
+
+ var key = file.replace(/\.(zip|prop)$/, '');
+ var item = Zotero.Items.getByLibraryAndKey(null, key);
+ if (item) {
+ Zotero.debug("Skipping existing file " + file);
+ continue;
+ }
+
+ Zotero.debug("Checking orphaned file " + file);
+
+ // TODO: Parse HTTP date properly
+ var lastModified = response..*::getlastmodified.toString();
+ lastModified = Zotero.Date.strToISO(lastModified);
+ lastModified = Zotero.Date.sqlToDate(lastModified);
+
+ // Delete files older than a day before last sync time
+ var days = (lastSyncDate - lastModified) / 1000 / 60 / 60 / 24;
+
+ if (days > daysBeforeSyncTime) {
+ deleteFiles.push(file);
+ }
}
- if (last && callback) {
- callback(results);
- }
-
- if (error) {
- results.error.push(fileName);
- var msg = "An error occurred attempting to delete "
- + "'" + fileName
- + "' (" + req.status + " " + req.statusText + ").";
- self.onError(msg);
- }
- });
- });
- }
-}
-
-
-/**
- * Checks for an invalid SSL certificate and displays a nice error
- */
-Zotero.Sync.Storage.Session.WebDAV.prototype._checkResponse = function (req, obj) {
- var channel = req.channel;
- if (!channel instanceof Ci.nsIChannel) {
- obj.onError('No HTTPS channel available');
- }
- var secInfo = channel.securityInfo;
- if (secInfo instanceof Ci.nsITransportSecurityInfo) {
- secInfo.QueryInterface(Ci.nsITransportSecurityInfo);
- if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE) == Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
- var host = 'host';
- try {
- host = channel.URI.host;
- }
- catch (e) {
- Zotero.debug(e);
- }
-
- var msg = Zotero.localeJoin([
- Zotero.getString('sync.storage.error.webdav.sslCertificateError', host),
- Zotero.getString('sync.storage.error.webdav.loadURLForMoreInfo')
- ]);
-
- obj.onError(msg);
- return;
+ deleteStorageFiles(deleteFiles, function (results) {
+ Zotero.Prefs.set("lastWebDAVOrphanPurge", Math.round(new Date().getTime() / 1000))
+ if (callback) {
+ callback(results);
+ }
+ Zotero.Sync.Storage.EventManager.success();
+ });
+ }, { Depth: 1 });
}
- else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) == Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
- var msg = Zotero.localeJoin([
- Zotero.getString('sync.storage.error.webdav.sslConnectionError', host),
- Zotero.getString('sync.storage.error.webdav.loadURLForMoreInfo')
- ]);
- obj.onError(msg);
- return;
- }
- }
-}
+ };
+}());
diff --git a/chrome/content/zotero/xpcom/storage/zfs.js b/chrome/content/zotero/xpcom/storage/zfs.js
index d91a85b214..5bf2e60bc9 100644
--- a/chrome/content/zotero/xpcom/storage/zfs.js
+++ b/chrome/content/zotero/xpcom/storage/zfs.js
@@ -24,486 +24,233 @@
*/
-Zotero.Sync.Storage.Session.ZFS = function (callbacks) {
- this.onChangesMade = callbacks.onChangesMade ? callbacks.onChangesMade : function () {};
- this.onError = callbacks.onError ? callbacks.onError : function () {};
+Zotero.Sync.Storage.Module.ZFS = (function () {
+ var _rootURI;
+ var _userURI;
+ var _cachedCredentials = false;
+ var _lastSyncTime = null;
- this._rootURI;
- this._userURI;
- this._cachedCredentials = false;
- this._lastSyncTime = null;
-}
-
-Zotero.Sync.Storage.Session.ZFS.prototype.name = "ZFS";
-
-Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('includeUserFiles', function () {
- return Zotero.Prefs.get("sync.storage.enabled") && Zotero.Prefs.get("sync.storage.protocol") == 'zotero';
-});
-
-Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('includeGroupFiles', function () {
- return Zotero.Prefs.get("sync.storage.groups.enabled");
-});
-
-Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('enabled', function () {
- return this.includeUserFiles || this.includeGroupFiles;
-});
-
-Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('active', function () {
- return this.enabled;
-});
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('rootURI', function () {
- if (!this._rootURI) {
- throw ("Root URI not initialized in Zotero.Sync.Storage.Session.ZFS.rootURI");
- }
- return this._rootURI.clone();
-});
-
-Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('userURI', function () {
- if (!this._userURI) {
- throw ("User URI not initialized in Zotero.Sync.Storage.Session.ZFS.userURI");
- }
- return this._userURI.clone();
-});
-
-Zotero.Sync.Storage.Session.ZFS.prototype.init = function (url, username, password) {
- var ios = Components.classes["@mozilla.org/network/io-service;1"].
- getService(Components.interfaces.nsIIOService);
- try {
- var uri = ios.newURI(url, null, null);
- if (username) {
- uri.username = username;
- uri.password = password;
- }
- }
- catch (e) {
- Zotero.debug(e);
- Components.utils.reportError(e);
- return false;
- }
- this._rootURI = uri;
-
- uri = uri.clone();
- uri.spec += 'users/' + Zotero.userID + '/';
- this._userURI = uri;
-
- return true;
-}
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype.initFromPrefs = function () {
- var url = ZOTERO_CONFIG.API_URL;
- var username = Zotero.Sync.Server.username;
- var password = Zotero.Sync.Server.password;
- return this.init(url, username, password);
-}
-
-
-/**
- * Get file metadata on storage server
- *
- * @param {Zotero.Item} item
- * @param {Function} callback Callback f(item, etag)
- */
-Zotero.Sync.Storage.Session.ZFS.prototype._getStorageFileInfo = function (item, callback) {
- var uri = this._getItemInfoURI(item);
-
- var self = this;
-
- Zotero.HTTP.doGet(uri, function (req) {
- var funcName = "Zotero.Sync.Storage.Session.ZFS._getStorageFileInfo()";
+ /**
+ * Get file metadata on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} callback Callback f(item, etag)
+ */
+ function getStorageFileInfo(item, callback) {
+ var uri = getItemInfoURI(item);
- if (req.status == 404) {
- callback(item, false);
- return;
- }
- else if (req.status != 200) {
- var msg = "Unexpected status code " + req.status + " in " + funcName
- + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
- Zotero.debug(msg, 1);
- Zotero.debug(req.responseText);
- Components.utils.reportError(msg);
- self.onError();
- return;
- }
-
- var info = {};
- info.hash = req.getResponseHeader('ETag');
- if (!info.hash) {
- var msg = "Hash not found in info response in " + funcName
- + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
- Zotero.debug(msg, 1);
- Zotero.debug(req.responseText);
- Components.utils.reportError(msg);
- try {
- Zotero.debug(req.getAllResponseHeaders());
- }
- catch (e) {
- Zotero.debug("Response headers unavailable");
- }
- // TODO: localize?
- var msg = "A file sync error occurred. Please restart Firefox and/or your computer and try syncing again.\n\n"
- + "If the error persists, there may be a problem with either your computer or your network: security software, proxy server, VPN, etc. "
- + "Try disabling any security/firewall software you're using or, if this is a laptop, try from a different network.";
- self.onError(msg);
- return;
- }
- info.filename = req.getResponseHeader('X-Zotero-Filename');
- var mtime = req.getResponseHeader('X-Zotero-Modification-Time');
- info.mtime = parseInt(mtime);
- info.compressed = req.getResponseHeader('X-Zotero-Compressed') == 'Yes';
- Zotero.debug(info);
-
- callback(item, info);
- });
-}
-
-
-/**
- * Begin download process for individual file
- *
- * @param {Zotero.Sync.Storage.Request} [request]
- */
-Zotero.Sync.Storage.Session.ZFS.prototype.downloadFile = function (request) {
- var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
- if (!item) {
- throw ("Item '" + request.name + "' not found in Zotero.Sync.Storage.Session.ZFS.downloadFile()");
- }
-
- var self = this;
-
- // Retrieve file info from server to store locally afterwards
- this._getStorageFileInfo(item, function (item, info) {
- if (!request.isRunning()) {
- Zotero.debug("Download request '" + request.name
- + "' is no longer running after getting remote file info");
- return;
- }
-
- if (!info) {
- Zotero.debug("Remote file not found for item " + item.libraryID + "/" + item.key);
- request.finish();
- return;
- }
-
- try {
- var syncModTime = info.mtime;
- var syncHash = info.hash;
+ Zotero.HTTP.doGet(uri, function (req) {
+ var funcName = "Zotero.Sync.Storage.Module.ZFS.getStorageFileInfo()";
- var file = item.getFile();
- // Skip download if local file exists and matches mod time
- if (file && file.exists()) {
- if (syncModTime == file.lastModifiedTime) {
- Zotero.debug("File mod time matches remote file -- skipping download");
-
- Zotero.DB.beginTransaction();
- var syncState = Zotero.Sync.Storage.getSyncState(item.id);
- //var updateItem = syncState != 1;
- var updateItem = false;
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.DB.commitTransaction();
- self.onChangesMade();
- request.finish();
- return;
- }
- // If not compressed, check hash, in case only timestamp changed
- else if (!info.compressed && item.attachmentHash == syncHash) {
- Zotero.debug("File hash matches remote file -- skipping download");
-
- Zotero.DB.beginTransaction();
- var syncState = Zotero.Sync.Storage.getSyncState(item.id);
- //var updateItem = syncState != 1;
- var updateItem = false;
- if (!info.compressed) {
- Zotero.Sync.Storage.setSyncedHash(item.id, syncHash, false);
- }
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.DB.commitTransaction();
- self.onChangesMade();
- request.finish();
- return;
- }
+ if (req.status == 404) {
+ callback(item, false);
+ return;
+ }
+ else if (req.status != 200) {
+ var msg = "Unexpected status code " + req.status + " in " + funcName
+ + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
+ Zotero.debug(msg, 1);
+ Zotero.debug(req.responseText);
+ Components.utils.reportError(msg);
+ Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError);
}
- var destFile = Zotero.getTempDirectory();
- if (info.compressed) {
- destFile.append(item.key + '.zip.tmp');
- }
- else {
- destFile.append(item.key + '.tmp');
- }
-
- if (destFile.exists()) {
+ var info = {};
+ info.hash = req.getResponseHeader('ETag');
+ if (!info.hash) {
+ var msg = "Hash not found in info response in " + funcName
+ + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
+ Zotero.debug(msg, 1);
+ Zotero.debug(req.responseText);
+ Components.utils.reportError(msg);
try {
- destFile.remove(false);
+ Zotero.debug(req.getAllResponseHeaders());
}
catch (e) {
- Zotero.File.checkFileAccessError(e, destFile, 'delete');
+ Zotero.debug("Response headers unavailable");
}
+ // TODO: localize?
+ var msg = "A file sync error occurred. Please restart Firefox and/or your computer and try syncing again.\n\n"
+ + "If the error persists, there may be a problem with either your computer or your network: security software, proxy server, VPN, etc. "
+ + "Try disabling any security/firewall software you're using or, if this is a laptop, try from a different network.";
+ Zotero.Sync.Storage.EventManager.error(msg);
+ }
+ info.filename = req.getResponseHeader('X-Zotero-Filename');
+ var mtime = req.getResponseHeader('X-Zotero-Modification-Time');
+ info.mtime = parseInt(mtime);
+ info.compressed = req.getResponseHeader('X-Zotero-Compressed') == 'Yes';
+ Zotero.debug(info);
+
+ callback(item, info);
+ });
+ }
+
+
+ /**
+ * Upload the file to the server
+ *
+ * @param {Object} Object with 'request' property
+ * @return {void}
+ */
+ function processUploadFile(data) {
+ /*
+ updateSizeMultiplier(
+ (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100
+ );
+ */
+
+ var request = data.request;
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+ getStorageFileInfo(item, function (item, info) {
+ if (request.isFinished()) {
+ Zotero.debug("Upload request '" + request.name
+ + "' is no longer running after getting file info");
+ return;
}
- // saveURI() below appears not to create empty files for Content-Length: 0,
- // so we create one here just in case
try {
- destFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
- }
- catch (e) {
- Zotero.File.checkFileAccessError(e, destFile, 'create');
- }
-
- var listener = new Zotero.Sync.Storage.StreamListener(
- {
- onStart: function (request, data) {
- if (data.request.isFinished()) {
- Zotero.debug("Download request " + data.request.name
- + " stopped before download started -- closing channel");
- request.cancel(0x804b0002); // NS_BINDING_ABORTED
- return;
+ // Check for conflict
+ if (Zotero.Sync.Storage.getSyncState(item.id)
+ != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) {
+ if (info) {
+ // Remote mod time
+ var mtime = info.mtime;
+ // Local file time
+ var fmtime = item.attachmentModificationTime;
+
+ var same = false;
+ var useLocal = false;
+ if (fmtime == mtime) {
+ same = true;
+ Zotero.debug("File mod time matches remote file -- skipping upload");
}
- },
- onProgress: function (a, b, c) {
- request.onProgress(a, b, c)
- },
- onStop: function (request, status, response, data) {
- if (status != 200) {
- var msg = "Unexpected status code " + status
- + " for request " + data.request.name + " in Zotero.Sync.Storage.Session.ZFS.downloadFile()";
- Zotero.debug(msg, 1);
- Components.utils.reportError(msg);
- self.onError();
+ // Allow floored timestamps for filesystems that don't support
+ // millisecond precision (e.g., HFS+)
+ else if (Math.floor(mtime / 1000) * 1000 == fmtime || Math.floor(fmtime / 1000) * 1000 == mtime) {
+ same = true;
+ Zotero.debug("File mod times are within one-second precision (" + fmtime + " ≅ " + mtime + ") "
+ + "-- skipping upload");
+ }
+ // Allow timestamp to be exactly one hour off to get around
+ // time zone issues -- there may be a proper way to fix this
+ else 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) {
+ same = true;
+ Zotero.debug("File mod time (" + fmtime + ") is exactly one hour off remote file (" + mtime + ") "
+ + "-- assuming time zone issue and skipping upload");
+ }
+ // Ignore maxed-out 32-bit ints, from brief problem after switch to 32-bit servers
+ else if (mtime == 2147483647) {
+ Zotero.debug("Remote mod time is invalid -- uploading local file version");
+ useLocal = true;
+ }
+
+ if (same) {
+ Zotero.debug(Zotero.Sync.Storage.getSyncedModificationTime(item.id));
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ //Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime, true);
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ Zotero.Sync.Storage.EventManager.changesMade();
+ request.finish();
return;
}
- // Don't try to process if the request has been cancelled
- if (data.request.isFinished()) {
- Zotero.debug("Download request " + data.request.name
- + " is no longer running after file download", 2);
+ var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id);
+ if (!useLocal && smtime != mtime) {
+ var localData = { modTime: fmtime };
+ var remoteData = { modTime: mtime };
+ Zotero.Sync.Storage.QueueManager.addConflict(
+ request.name, localData, remoteData
+ );
+ Zotero.debug("Conflict -- last synced file mod time "
+ + "does not match time on storage server"
+ + " (" + smtime + " != " + mtime + ")");
+ request.finish();
return;
}
-
- Zotero.debug("Finished download of " + destFile.path);
-
+ }
+ else {
+ Zotero.debug("Remote file not found for item " + item.libraryID + "/" + item.key);
+ }
+ }
+
+ getFileUploadParameters(
+ item,
+ function (item, target, uploadKey, params) {
try {
- Zotero.Sync.Storage.processDownload(data);
- data.request.finish();
+ postFile(request, item, target, uploadKey, params);
}
catch (e) {
- self.onError(e);
+ Zotero.Sync.Storage.EventManager.error(e);
}
},
- request: request,
- item: item,
- compressed: info.compressed,
- syncModTime: syncModTime,
- syncHash: syncHash
- }
- );
-
- var uri = self._getItemURI(item);
-
- // Don't display password in console
- var disp = uri.clone();
- if (disp.password) {
- disp.password = "********";
+ function () {
+ updateItemFileInfo(item);
+ request.finish();
+ }
+ );
}
- Zotero.debug('Saving ' + disp.spec + ' with saveURI()');
- const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
- var wbp = Components
- .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
- .createInstance(nsIWBP);
- wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
- wbp.progressListener = listener;
- wbp.saveURI(uri, null, null, null, null, destFile);
- }
- catch (e) {
- self.onError(e);
- }
- });
-}
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype.uploadFile = function (request) {
- var self = this;
- var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
- if (Zotero.Attachments.getNumFiles(item) > 1) {
- Zotero.Sync.Storage.createUploadFile(request, function (data) { self._processUploadFile(data); });
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+ });
}
- else {
- this._processUploadFile({ request: request });
- }
-}
-
-
-/**
- * Upload the file to the server
- *
- * @param {Object} Object with 'request' property
- * @return {void}
- */
-Zotero.Sync.Storage.Session.ZFS.prototype._processUploadFile = function (data) {
- /*
- _updateSizeMultiplier(
- (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100
- );
- */
- var request = data.request;
- var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
- var self = this;
-
- this._getStorageFileInfo(item, function (item, info) {
- if (request.isFinished()) {
- Zotero.debug("Upload request '" + request.name
- + "' is no longer running after getting file info");
- return;
+ /**
+ * Get mod time of file on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} uploadCallback Callback f(request, item, target, params)
+ * @param {Function} existsCallback Callback f() to call when file already exists
+ * on server and uploading isn't necessary
+ */
+ function getFileUploadParameters(item, uploadCallback, existsCallback) {
+ var uri = getItemURI(item);
+
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ var file = Zotero.getTempDirectory();
+ var filename = item.key + '.zip';
+ file.append(filename);
+ uri.spec = uri.spec;
+ var zip = true;
+ }
+ else {
+ var file = item.getFile();
+ var filename = file.leafName;
+ var zip = false;
}
- try {
- // Check for conflict
- if (Zotero.Sync.Storage.getSyncState(item.id)
- != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) {
- if (info) {
- // Remote mod time
- var mtime = info.mtime;
- // Local file time
- var fmtime = item.attachmentModificationTime;
-
- var same = false;
- var useLocal = false;
- if (fmtime == mtime) {
- same = true;
- Zotero.debug("File mod time matches remote file -- skipping upload");
- }
- // Allow floored timestamps for filesystems that don't support
- // millisecond precision (e.g., HFS+)
- else if (Math.floor(mtime / 1000) * 1000 == fmtime || Math.floor(fmtime / 1000) * 1000 == mtime) {
- same = true;
- Zotero.debug("File mod times are within one-second precision (" + fmtime + " ≅ " + mtime + ") "
- + "-- skipping upload");
- }
- // Allow timestamp to be exactly one hour off to get around
- // time zone issues -- there may be a proper way to fix this
- else 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) {
- same = true;
- Zotero.debug("File mod time (" + fmtime + ") is exactly one hour off remote file (" + mtime + ") "
- + "-- assuming time zone issue and skipping upload");
- }
- // Ignore maxed-out 32-bit ints, from brief problem after switch to 32-bit servers
- else if (mtime == 2147483647) {
- Zotero.debug("Remote mod time is invalid -- uploading local file version");
- useLocal = true;
- }
-
- if (same) {
- Zotero.debug(Zotero.Sync.Storage.getSyncedModificationTime(item.id));
-
- Zotero.DB.beginTransaction();
- var syncState = Zotero.Sync.Storage.getSyncState(item.id);
- //Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime, true);
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime);
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.DB.commitTransaction();
- self.onChangesMade();
- request.finish();
- return;
- }
-
- var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id);
- if (!useLocal && smtime != mtime) {
- var localData = { modTime: fmtime };
- var remoteData = { modTime: mtime };
- Zotero.Sync.Storage.QueueManager.addConflict(
- request.name, localData, remoteData
- );
- Zotero.debug("Conflict -- last synced file mod time "
- + "does not match time on storage server"
- + " (" + smtime + " != " + mtime + ")");
- request.finish();
- return;
- }
- }
- else {
- Zotero.debug("Remote file not found for item " + item.libraryID + "/" + item.key);
- }
- }
-
- self._getFileUploadParameters(
- item,
- function (item, target, uploadKey, params) {
- try {
- self._postFile(request, item, target, uploadKey, params);
- }
- catch (e) {
- self.onError(e);
- }
- },
- function () {
- self._updateItemFileInfo(item);
- request.finish();
- }
- );
- }
- catch (e) {
- self.onError(e);
- }
- });
-}
-
-
-/**
- * Get mod time of file on storage server
- *
- * @param {Zotero.Item} item
- * @param {Function} uploadCallback Callback f(request, item, target, params)
- * @param {Function} existsCallback Callback f() to call when file already exists
- * on server and uploading isn't necessary
- */
-Zotero.Sync.Storage.Session.ZFS.prototype._getFileUploadParameters = function (item, uploadCallback, existsCallback) {
- var uri = this._getItemURI(item);
-
- if (Zotero.Attachments.getNumFiles(item) > 1) {
- var file = Zotero.getTempDirectory();
- var filename = item.key + '.zip';
- file.append(filename);
- uri.spec = uri.spec;
- var zip = true;
- }
- else {
- var file = item.getFile();
- var filename = file.leafName;
- var zip = false;
- }
-
- var mtime = item.attachmentModificationTime;
- var hash = Zotero.Utilities.Internal.md5(file);
-
- var body = "md5=" + hash + "&filename=" + encodeURIComponent(filename)
- + "&filesize=" + file.fileSize + "&mtime=" + mtime;
- if (zip) {
- body += "&zip=1";
- }
-
- var self = this;
-
- Zotero.HTTP.doPost(uri, body, function (req) {
- var funcName = "Zotero.Sync.Storage.Session.ZFS._getFileUploadParameters()";
+ var mtime = item.attachmentModificationTime;
+ var hash = Zotero.Utilities.Internal.md5(file);
- if (req.status == 413) {
- var retry = req.getResponseHeader('Retry-After');
- if (retry) {
- var minutes = Math.round(retry / 60);
- var e = new Zotero.Error("You have too many queued uploads. Please try again in " + minutes + " minutes.", "ZFS_UPLOAD_QUEUE_LIMIT");
- self.onError(e);
- }
- else {
- // TODO: localize
+ var body = "md5=" + hash + "&filename=" + encodeURIComponent(filename)
+ + "&filesize=" + file.fileSize + "&mtime=" + mtime;
+ if (zip) {
+ body += "&zip=1";
+ }
+
+ Zotero.HTTP.doPost(uri, body, function (req) {
+ var funcName = "Zotero.Sync.Storage.Module.ZFS.getFileUploadParameters()";
+
+ if (req.status == 413) {
+ var retry = req.getResponseHeader('Retry-After');
+ if (retry) {
+ var minutes = Math.round(retry / 60);
+ // TODO: localize
+ var e = new Zotero.Error(
+ "You have too many queued uploads. "
+ + "Please try again in " + minutes + " minutes.",
+ "ZFS_UPLOAD_QUEUE_LIMIT"
+ );
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
var text, buttonText = null, buttonCallback;
@@ -543,518 +290,773 @@ Zotero.Sync.Storage.Session.ZFS.prototype._getFileUploadParameters = function (i
dialogButtonCallback: buttonCallback
}
);
- self.onError(e);
- }
- return;
- }
- else if (req.status == 403) {
- Zotero.debug(req.responseText);
-
- var groupID = Zotero.Groups.getGroupIDFromLibraryID(item.libraryID);
- var e = new Zotero.Error("File editing denied for group", "ZFS_FILE_EDITING_DENIED", { groupID: groupID });
- self.onError(e);
- return;
- }
- else if (req.status == 404) {
- Components.utils.reportError("Unexpected status code 404 in " + funcName
- + " (" + Zotero.Items.getLibraryKeyHash(item) + ")");
- if (Zotero.Prefs.get('sync.debugNoAutoResetClient')) {
- Components.utils.reportError("Skipping automatic client reset due to debug pref");
+ Zotero.debug(e, 2);
+ Components.utils.reportError(e);
+ // Stop uploads, log warning, and continue
+ Zotero.Sync.Storage.QueueManager.get('upload').stop();
+ Zotero.Sync.Storage.EventManager.warning(e);
+ Zotero.Sync.Storage.EventManager.success();
return;
}
- if (!Zotero.Sync.Server.canAutoResetClient) {
- Components.utils.reportError("Client has already been auto-reset -- manual sync required");
- return;
+ else if (req.status == 403) {
+ Zotero.debug(req.responseText);
+
+ var groupID = Zotero.Groups.getGroupIDFromLibraryID(item.libraryID);
+ var e = new Zotero.Error(
+ "File editing denied for group",
+ "ZFS_FILE_EDITING_DENIED",
+ {
+ groupID: groupID
+ }
+ );
+ Zotero.Sync.Storage.EventManager.error(e);
}
- Zotero.Sync.Server.resetClient();
- Zotero.Sync.Server.canAutoResetClient = false;
- self.onError();
- return;
- }
- else if (req.status != 200) {
- var msg = "Unexpected status code " + req.status + " in " + funcName
- + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
- Zotero.debug(msg, 1);
- Zotero.debug(req.responseText);
- Zotero.debug(req.getAllResponseHeaders());
- Components.utils.reportError(msg);
- self.onError();
- return;
- }
-
- Zotero.debug(req.responseText);
-
- try {
- // Strip XML declaration and convert to E4X
- var xml = new XML(Zotero.Utilities.trim(req.responseText.replace(/<\?xml.*\?>/, '')));
- }
- catch (e) {
- self.onError("Invalid response retrieving file upload parameters");
- return;
- }
-
- if (xml.name() != 'upload' && xml.name() != 'exists') {
- self.onError("Invalid response retrieving file upload parameters");
- return;
- }
- // File was already available, so uploading isn't required
- if (xml.name() == 'exists') {
- existsCallback();
- return;
- }
-
- var url = xml.url.toString();
- var uploadKey = xml.key.toString();
- var params = {}, p = '';
- for each(var param in xml.params.children()) {
- params[param.name()] = param.toString();
- }
- Zotero.debug(params);
- uploadCallback(item, url, uploadKey, params);
- });
-}
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype._postFile = function (request, item, url, uploadKey, params) {
- if (request.isFinished()) {
- Zotero.debug("Upload request " + request.name + " is no longer running after getting upload parameters");
- return;
- }
-
- var file = this._getUploadFile(item);
-
- // TODO: make sure this doesn't appear in file
- var boundary = "---------------------------" + Math.random().toString().substr(2);
-
- var mis = Components.classes["@mozilla.org/io/multiplex-input-stream;1"]
- .createInstance(Components.interfaces.nsIMultiplexInputStream);
-
- // Add parameters
- for (var key in params) {
- var storage = Components.classes["@mozilla.org/storagestream;1"]
- .createInstance(Components.interfaces.nsIStorageStream);
- storage.init(4096, 4294967295, null); // PR_UINT32_MAX
- var out = storage.getOutputStream(0);
-
- var conv = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
- .createInstance(Components.interfaces.nsIConverterOutputStream);
- conv.init(out, null, 4096, "?");
-
- var str = "--" + boundary + '\r\nContent-Disposition: form-data; name="' + key + '"'
- + '\r\n\r\n' + params[key] + '\r\n';
- conv.writeString(str);
- conv.close();
-
- var instr = storage.newInputStream(0);
- mis.appendStream(instr);
- }
-
- // Add file
- var sis = Components.classes["@mozilla.org/io/string-input-stream;1"]
- .createInstance(Components.interfaces.nsIStringInputStream);
- var str = "--" + boundary + '\r\nContent-Disposition: form-data; name="file"\r\n\r\n';
- sis.setData(str, -1);
- mis.appendStream(sis);
-
- var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
- .createInstance(Components.interfaces.nsIFileInputStream);
- fis.init(file, 0x01, 0, Components.interfaces.nsIFileInputStream.CLOSE_ON_EOF);
-
- var bis = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
- .createInstance(Components.interfaces.nsIBufferedInputStream)
- bis.init(fis, 64 * 1024);
- mis.appendStream(bis);
-
- // End request
- var sis = Components.classes["@mozilla.org/io/string-input-stream;1"]
- .createInstance(Components.interfaces.nsIStringInputStream);
- var str = "\r\n--" + boundary + "--";
- sis.setData(str, -1);
- mis.appendStream(sis);
-
-
-/* var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
- createInstance(Components.interfaces.nsIConverterInputStream);
- cstream.init(mis, "UTF-8", 0, 0); // you can use another encoding here if you wish
-
- let (str = {}) {
- cstream.readString(-1, str); // read the whole file and put it in str.value
- data = str.value;
- }
- cstream.close(); // this closes fstream
- alert(data);
-*/
-
- var ios = Components.classes["@mozilla.org/network/io-service;1"].
- getService(Components.interfaces.nsIIOService);
- var uri = ios.newURI(url, null, null);
- var channel = ios.newChannelFromURI(uri);
-
- channel.QueryInterface(Components.interfaces.nsIUploadChannel);
- channel.setUploadStream(mis, "multipart/form-data", -1);
- channel.QueryInterface(Components.interfaces.nsIHttpChannel);
- channel.requestMethod = 'POST';
- channel.allowPipelining = false;
- channel.setRequestHeader('Keep-Alive', '', false);
- channel.setRequestHeader('Connection', '', false);
- channel.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary, false);
- //channel.setRequestHeader('Date', date, false);
-
- var self = this;
-
- request.setChannel(channel);
-
- var listener = new Zotero.Sync.Storage.StreamListener(
- {
- onProgress: function (a, b, c) {
- request.onProgress(a, b, c);
- },
- onStop: function (httpRequest, status, response, data) { self._onUploadComplete(httpRequest, status, response, data); },
- onCancel: function (httpRequest, status, data) { self._onUploadCancel(httpRequest, status, data); },
- request: request,
- item: item,
- uploadKey: uploadKey,
- streams: [mis]
- }
- );
- channel.notificationCallbacks = listener;
-
- var dispURI = uri.clone();
- if (dispURI.password) {
- dispURI.password = '********';
- }
- Zotero.debug("HTTP POST of " + file.leafName + " to " + dispURI.spec);
-
- channel.asyncOpen(listener, null);
-}
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype._onUploadComplete = function (httpRequest, status, response, data) {
- var request = data.request;
- var item = data.item;
- var uploadKey = data.uploadKey;
-
- Zotero.debug("Upload of attachment " + item.key
- + " finished with status code " + status);
-
- Zotero.debug(response);
-
- switch (status) {
- case 201:
- break;
-
- case 500:
- this.onError("File upload failed. Please try again.");
- return;
-
- default:
- var msg = "Unexpected file upload status " + status
- + " in Zotero.Sync.Storage._onUploadComplete()"
- + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
- Zotero.debug(msg, 1);
- Components.utils.reportError(msg);
- this.onError();
- return;
- }
-
- var uri = this._getItemURI(item);
- var body = "update=" + uploadKey + "&mtime=" + item.attachmentModificationTime;
-
- var self = this;
-
- // Register upload on server
- Zotero.HTTP.doPost(uri, body, function (req) {
- if (req.status != 204) {
- var msg = "Unexpected file registration status " + req.status
- + " in Zotero.Sync.Storage._onUploadComplete()"
- + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
- Zotero.debug(msg, 1);
- Zotero.debug(req.responseText);
- Zotero.debug(req.getAllResponseHeaders());
- Components.utils.reportError(msg);
- self.onError();
- return;
- }
-
- self._updateItemFileInfo(item);
- request.finish();
- });
-}
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype._updateItemFileInfo = function (item) {
- // Mark as changed locally
- Zotero.DB.beginTransaction();
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
-
- // Store file mod time
- var mtime = item.attachmentModificationTime;
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, mtime, true);
-
- // Store file hash of individual files
- if (Zotero.Attachments.getNumFiles(item) == 1) {
- var hash = item.attachmentHash;
- Zotero.Sync.Storage.setSyncedHash(item.id, hash);
- }
-
- Zotero.DB.commitTransaction();
-
- try {
- if (Zotero.Attachments.getNumFiles(item) > 1) {
- var file = Zotero.getTempDirectory();
- file.append(item.key + '.zip');
- file.remove(false);
- }
- }
- catch (e) {
- Components.utils.reportError(e);
- }
-
- this.onChangesMade();
-}
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype._onUploadCancel = function (httpRequest, status, data) {
- var request = data.request;
- var item = data.item;
-
- Zotero.debug("Upload of attachment " + item.key + " cancelled with status code " + status);
-
- try {
- if (Zotero.Attachments.getNumFiles(item) > 1) {
- var file = Zotero.getTempDirectory();
- file.append(item.key + '.zip');
- file.remove(false);
- }
- }
- catch (e) {
- Components.utils.reportError(e);
- }
-
- request.finish();
-}
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype.getLastSyncTime = function (callback) {
- var uri = this.userURI;
- var successFileURI = uri.clone();
- successFileURI.spec += "laststoragesync?auth=1";
-
- var self = this;
-
- // Cache the credentials
- if (!this._cachedCredentials) {
- var uri = this.rootURI;
- // TODO: move to root uri
- uri.spec += "?auth=1";
- Zotero.HTTP.doGet(uri, function (req) {
- if (req.status == 401) {
- // TODO: localize
- var msg = "File sync login failed\n\nCheck your username and password in the Sync pane of the Zotero preferences.";
- self.onError(msg);
- return;
+ else if (req.status == 404) {
+ Components.utils.reportError("Unexpected status code 404 in " + funcName
+ + " (" + Zotero.Items.getLibraryKeyHash(item) + ")");
+ if (Zotero.Prefs.get('sync.debugNoAutoResetClient')) {
+ Components.utils.reportError("Skipping automatic client reset due to debug pref");
+ return;
+ }
+ if (!Zotero.Sync.Server.canAutoResetClient) {
+ Components.utils.reportError("Client has already been auto-reset -- manual sync required");
+ return;
+ }
+ Zotero.Sync.Server.resetClient();
+ Zotero.Sync.Server.canAutoResetClient = false;
+ Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError);
}
else if (req.status != 200) {
- var msg = "Unexpected status code " + req.status + " caching "
- + "authentication credentials in Zotero.Sync.Storage.Session.ZFS.getLastSyncTime()";
+ var msg = "Unexpected status code " + req.status + " in " + funcName
+ + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
Zotero.debug(msg, 1);
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.getAllResponseHeaders());
Components.utils.reportError(msg);
- self.onError(Zotero.Sync.Storage.defaultErrorRestart);
+ Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError);
+ }
+
+ Zotero.debug(req.responseText);
+
+ try {
+ // Strip XML declaration and convert to E4X
+ var xml = new XML(Zotero.Utilities.trim(req.responseText.replace(/<\?xml.*\?>/, '')));
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(
+ "Invalid response retrieving file upload parameters"
+ );
+ }
+
+ if (xml.name() != 'upload' && xml.name() != 'exists') {
+ Zotero.Sync.Storage.EventManager.error(
+ "Invalid response retrieving file upload parameters"
+ );
+ }
+ // File was already available, so uploading isn't required
+ if (xml.name() == 'exists') {
+ existsCallback();
return;
}
- self._cachedCredentials = true;
- self.getLastSyncTime(callback);
- });
- return;
- }
-
- Zotero.HTTP.doGet(successFileURI, function (req) {
- if (req.responseText) {
- Zotero.debug(req.responseText);
- }
- Zotero.debug(req.status);
-
- if (req.status == 401 || req.status == 403) {
- Zotero.debug("Clearing ZFS authentication credentials", 2);
- self._cachedCredentials = false;
- }
-
- if (req.status != 200 && req.status != 404) {
- var msg = "Unexpected status code " + req.status + " getting "
- + "last file sync time";
- Zotero.debug(msg, 1);
- Components.utils.reportError(msg);
- self.onError();
- return;
- }
-
- if (req.status == 200) {
- var ts = req.responseText;
- var date = new Date(ts * 1000);
- Zotero.debug("Last successful storage sync was " + date);
- self._lastSyncTime = ts;
- }
- else {
- var ts = null;
- self._lastSyncTime = null;
- }
- callback(ts);
- });
-}
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype.setLastSyncTime = function (callback, useLastSyncTime) {
- if (useLastSyncTime) {
- if (!this._lastSyncTime) {
- if (callback) {
- callback();
+
+ var url = xml.url.toString();
+ var uploadKey = xml.key.toString();
+ var params = {}, p = '';
+ for each(var param in xml.params.children()) {
+ params[param.name()] = param.toString();
}
+ Zotero.debug(params);
+ uploadCallback(item, url, uploadKey, params);
+ });
+ }
+
+
+ function postFile(request, item, url, uploadKey, params) {
+ if (request.isFinished()) {
+ Zotero.debug("Upload request " + request.name + " is no longer running after getting upload parameters");
return;
}
- var sql = "REPLACE INTO version VALUES ('storage_zfs', ?)";
- Zotero.DB.query(sql, { int: this._lastSyncTime });
+ var file = getUploadFile(item);
- this._lastSyncTime = null;
- this._cachedCredentials = false;
+ // TODO: make sure this doesn't appear in file
+ var boundary = "---------------------------" + Math.random().toString().substr(2);
- if (callback) {
- callback();
- }
- return;
- }
- this._lastSyncTime = null;
-
- var uri = this.userURI;
- var successFileURI = uri.clone();
- successFileURI.spec += "laststoragesync?auth=1";
-
- var self = this;
-
- Zotero.HTTP.doPost(successFileURI, "", function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
+ var mis = Components.classes["@mozilla.org/io/multiplex-input-stream;1"]
+ .createInstance(Components.interfaces.nsIMultiplexInputStream);
- if (req.status != 200) {
- var msg = "Unexpected status code " + req.status + " setting "
- + "last file sync time";
- Zotero.debug(msg, 1);
- Components.utils.reportError(msg);
- self.onError();
- return;
+ // Add parameters
+ for (var key in params) {
+ var storage = Components.classes["@mozilla.org/storagestream;1"]
+ .createInstance(Components.interfaces.nsIStorageStream);
+ storage.init(4096, 4294967295, null); // PR_UINT32_MAX
+ var out = storage.getOutputStream(0);
+
+ var conv = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterOutputStream);
+ conv.init(out, null, 4096, "?");
+
+ var str = "--" + boundary + '\r\nContent-Disposition: form-data; name="' + key + '"'
+ + '\r\n\r\n' + params[key] + '\r\n';
+ conv.writeString(str);
+ conv.close();
+
+ var instr = storage.newInputStream(0);
+ mis.appendStream(instr);
}
- var ts = req.responseText;
+ // Add file
+ var sis = Components.classes["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Components.interfaces.nsIStringInputStream);
+ var str = "--" + boundary + '\r\nContent-Disposition: form-data; name="file"\r\n\r\n';
+ sis.setData(str, -1);
+ mis.appendStream(sis);
- var sql = "REPLACE INTO version VALUES ('storage_zfs', ?)";
- Zotero.DB.query(sql, { int: ts });
+ var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ fis.init(file, 0x01, 0, Components.interfaces.nsIFileInputStream.CLOSE_ON_EOF);
- self._cachedCredentials = false;
+ var bis = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
+ .createInstance(Components.interfaces.nsIBufferedInputStream)
+ bis.init(fis, 64 * 1024);
+ mis.appendStream(bis);
- if (callback) {
- callback();
+ // End request
+ var sis = Components.classes["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Components.interfaces.nsIStringInputStream);
+ var str = "\r\n--" + boundary + "--";
+ sis.setData(str, -1);
+ mis.appendStream(sis);
+
+
+ /* var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Components.interfaces.nsIConverterInputStream);
+ cstream.init(mis, "UTF-8", 0, 0); // you can use another encoding here if you wish
+
+ let (str = {}) {
+ cstream.readString(-1, str); // read the whole file and put it in str.value
+ data = str.value;
}
- });
-}
-
-
-/**
- * Remove all synced files from the server
- */
-Zotero.Sync.Storage.Session.ZFS.prototype.purgeDeletedStorageFiles = function (callback) {
- // If we don't have a user id we've never synced and don't need to bother
- if (!Zotero.userID) {
- return;
+ cstream.close(); // this closes fstream
+ alert(data);
+ */
+
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ var uri = ios.newURI(url, null, null);
+ var channel = ios.newChannelFromURI(uri);
+
+ channel.QueryInterface(Components.interfaces.nsIUploadChannel);
+ channel.setUploadStream(mis, "multipart/form-data", -1);
+ channel.QueryInterface(Components.interfaces.nsIHttpChannel);
+ channel.requestMethod = 'POST';
+ channel.allowPipelining = false;
+ channel.setRequestHeader('Keep-Alive', '', false);
+ channel.setRequestHeader('Connection', '', false);
+ channel.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary, false);
+ //channel.setRequestHeader('Date', date, false);
+
+ request.setChannel(channel);
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onProgress: function (a, b, c) {
+ request.onProgress(a, b, c);
+ },
+ onStop: function (httpRequest, status, response, data) { onUploadComplete(httpRequest, status, response, data); },
+ onCancel: function (httpRequest, status, data) { onUploadCancel(httpRequest, status, data); },
+ request: request,
+ item: item,
+ uploadKey: uploadKey,
+ streams: [mis]
+ }
+ );
+ channel.notificationCallbacks = listener;
+
+ var dispURI = uri.clone();
+ if (dispURI.password) {
+ dispURI.password = '********';
+ }
+ Zotero.debug("HTTP POST of " + file.leafName + " to " + dispURI.spec);
+
+ channel.asyncOpen(listener, null);
}
- var sql = "SELECT value FROM settings WHERE setting=? AND key=?";
- var values = Zotero.DB.columnQuery(sql, ['storage', 'zfsPurge']);
- if (!values) {
- return;
- }
- Zotero.debug("Unlinking synced files on ZFS");
-
- var uri = this.userURI;
- uri.spec += "removestoragefiles?";
- // Unused
- for each(var value in values) {
- switch (value) {
- case 'user':
- uri.spec += "user=1&";
+ function onUploadComplete(httpRequest, status, response, data) {
+ var request = data.request;
+ var item = data.item;
+ var uploadKey = data.uploadKey;
+
+ Zotero.debug("Upload of attachment " + item.key
+ + " finished with status code " + status);
+
+ Zotero.debug(response);
+
+ switch (status) {
+ case 201:
break;
- case 'group':
- uri.spec += "group=1&";
- break;
+ case 500:
+ Zotero.Sync.Storage.EventManager.error(
+ "File upload failed. Please try again."
+ );
default:
- throw ("Invalid zfsPurge value '" + value + "' in ZFS purgeDeletedStorageFiles()");
+ var msg = "Unexpected file upload status " + status
+ + " in Zotero.Sync.Storage.ZFS.onUploadComplete()"
+ + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
+ Zotero.debug(msg, 1);
+ Components.utils.reportError(msg);
+ Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError);
}
- }
- uri.spec = uri.spec.substr(0, uri.spec.length - 1);
-
- var self = this;
-
- Zotero.HTTP.doPost(uri, "", function (xmlhttp) {
- if (xmlhttp.status != 204) {
- if (callback) {
- callback(false);
+
+ var uri = getItemURI(item);
+ var body = "update=" + uploadKey + "&mtime=" + item.attachmentModificationTime;
+
+ // Register upload on server
+ Zotero.HTTP.doPost(uri, body, function (req) {
+ if (req.status != 204) {
+ var msg = "Unexpected file registration status " + req.status
+ + " in Zotero.Sync.Storage.ZFS.onUploadComplete()"
+ + " (" + Zotero.Items.getLibraryKeyHash(item) + ")";
+ Zotero.debug(msg, 1);
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.getAllResponseHeaders());
+ Components.utils.reportError(msg);
+ Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError);
}
- self.onError("Unexpected status code " + xmlhttp.status + " purging ZFS files");
+
+ updateItemFileInfo(item);
+ request.finish();
+ });
+ }
+
+
+ function updateItemFileInfo(item) {
+ // Mark as changed locally
+ Zotero.DB.beginTransaction();
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+
+ // Store file mod time
+ var mtime = item.attachmentModificationTime;
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, mtime, true);
+
+ // Store file hash of individual files
+ if (Zotero.Attachments.getNumFiles(item) == 1) {
+ var hash = item.attachmentHash;
+ Zotero.Sync.Storage.setSyncedHash(item.id, hash);
}
- var sql = "DELETE FROM settings WHERE setting=? AND key=?";
- Zotero.DB.query(sql, ['storage', 'zfsPurge']);
+ Zotero.DB.commitTransaction();
- if (callback) {
- callback(true);
+ try {
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+ file.remove(false);
+ }
}
- });
-}
-
-
-//
-// Private methods
-//
-
-/**
- * Get the storage URI for an item
- *
- * @inner
- * @param {Zotero.Item}
- * @return {nsIURI} URI of file on storage server
- */
-Zotero.Sync.Storage.Session.ZFS.prototype._getItemURI = function (item) {
- var uri = this.rootURI;
- // Be sure to mirror parameter changes to _getItemInfoURI below
- uri.spec += Zotero.URI.getItemPath(item) + '/file?auth=1&iskey=1&version=1';
- return uri;
-}
-
-
-/**
- * Get the storage info URI for an item
- *
- * @inner
- * @param {Zotero.Item}
- * @return {nsIURI} URI of file on storage server with info flag
- */
-Zotero.Sync.Storage.Session.ZFS.prototype._getItemInfoURI = function (item) {
- var uri = this.rootURI;
- uri.spec += Zotero.URI.getItemPath(item) + '/file?auth=1&iskey=1&version=1&info=1';
- return uri;
-}
-
-
-Zotero.Sync.Storage.Session.ZFS.prototype._getUploadFile = function (item) {
- if (Zotero.Attachments.getNumFiles(item) > 1) {
- var file = Zotero.getTempDirectory();
- var filename = item.key + '.zip';
- file.append(filename);
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ Zotero.Sync.Storage.EventManager.changesMade();
}
- else {
- var file = item.getFile();
+
+
+ function onUploadCancel(httpRequest, status, data) {
+ var request = data.request;
+ var item = data.item;
+
+ Zotero.debug("Upload of attachment " + item.key + " cancelled with status code " + status);
+
+ try {
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+ file.remove(false);
+ }
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ request.finish();
}
- return file;
-}
+
+
+ /**
+ * Get the storage URI for an item
+ *
+ * @inner
+ * @param {Zotero.Item}
+ * @return {nsIURI} URI of file on storage server
+ */
+ function getItemURI(item) {
+ var uri = Zotero.Sync.Storage.Module.ZFS.rootURI;
+ // Be sure to mirror parameter changes to getItemInfoURI() below
+ uri.spec += Zotero.URI.getItemPath(item) + '/file?auth=1&iskey=1&version=1';
+ return uri;
+ }
+
+
+ /**
+ * Get the storage info URI for an item
+ *
+ * @inner
+ * @param {Zotero.Item}
+ * @return {nsIURI} URI of file on storage server with info flag
+ */
+ function getItemInfoURI(item) {
+ var uri = Zotero.Sync.Storage.Module.ZFS.rootURI;
+ uri.spec += Zotero.URI.getItemPath(item) + '/file?auth=1&iskey=1&version=1&info=1';
+ return uri;
+ }
+
+
+ function getUploadFile(item) {
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ var file = Zotero.getTempDirectory();
+ var filename = item.key + '.zip';
+ file.append(filename);
+ }
+ else {
+ var file = item.getFile();
+ }
+ return file;
+ }
+
+
+ return {
+ name: "ZFS",
+
+ get includeUserFiles() {
+ return Zotero.Prefs.get("sync.storage.enabled") && Zotero.Prefs.get("sync.storage.protocol") == 'zotero';
+ },
+
+ get includeGroupFiles() {
+ return Zotero.Prefs.get("sync.storage.groups.enabled");
+ },
+
+ get enabled() {
+ return this.includeUserFiles || this.includeGroupFiles;
+ },
+
+ get verified() {
+ return true;
+ },
+
+ get rootURI() {
+ if (!_rootURI) {
+ throw ("Root URI not initialized in Zotero.Sync.Storage.ZFS.rootURI");
+ }
+ return _rootURI.clone();
+ },
+
+ get userURI() {
+ if (!_userURI) {
+ throw ("User URI not initialized in Zotero.Sync.Storage.ZFS.userURI");
+ }
+ return _userURI.clone();
+ },
+
+
+ init: function (url, username, password) {
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ try {
+ var uri = ios.newURI(url, null, null);
+ if (username) {
+ uri.username = username;
+ uri.password = password;
+ }
+ }
+ catch (e) {
+ Zotero.debug(e, 1);
+ Components.utils.reportError(e);
+ return false;
+ }
+ _rootURI = uri;
+
+ uri = uri.clone();
+ uri.spec += 'users/' + Zotero.userID + '/';
+ _userURI = uri;
+
+ return true;
+ },
+
+
+ initFromPrefs: function () {
+ var url = ZOTERO_CONFIG.API_URL;
+ var username = Zotero.Sync.Server.username;
+ var password = Zotero.Sync.Server.password;
+ return this.init(url, username, password);
+ },
+
+
+ /**
+ * Begin download process for individual file
+ *
+ * @param {Zotero.Sync.Storage.Request} [request]
+ */
+ downloadFile: function (request) {
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+ if (!item) {
+ throw new Error("Item '" + request.name + "' not found");
+ }
+
+ // Retrieve file info from server to store locally afterwards
+ getStorageFileInfo(item, function (item, info) {
+ if (!request.isRunning()) {
+ Zotero.debug("Download request '" + request.name
+ + "' is no longer running after getting remote file info");
+ return;
+ }
+
+ if (!info) {
+ Zotero.debug("Remote file not found for item " + item.libraryID + "/" + item.key);
+ request.finish();
+ return;
+ }
+
+ try {
+ var syncModTime = info.mtime;
+ var syncHash = info.hash;
+
+ var file = item.getFile();
+ // Skip download if local file exists and matches mod time
+ if (file && file.exists()) {
+ if (syncModTime == file.lastModifiedTime) {
+ Zotero.debug("File mod time matches remote file -- skipping download");
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ //var updateItem = syncState != 1;
+ var updateItem = false;
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ Zotero.Sync.Storage.EventManager.changesMade();
+ request.finish();
+ return;
+ }
+ // If not compressed, check hash, in case only timestamp changed
+ else if (!info.compressed && item.attachmentHash == syncHash) {
+ Zotero.debug("File hash matches remote file -- skipping download");
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ //var updateItem = syncState != 1;
+ var updateItem = false;
+ if (!info.compressed) {
+ Zotero.Sync.Storage.setSyncedHash(item.id, syncHash, false);
+ }
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ Zotero.Sync.Storage.EventManager.changesMade();
+ request.finish();
+ return;
+ }
+ }
+
+ var destFile = Zotero.getTempDirectory();
+ if (info.compressed) {
+ destFile.append(item.key + '.zip.tmp');
+ }
+ else {
+ destFile.append(item.key + '.tmp');
+ }
+
+ if (destFile.exists()) {
+ try {
+ destFile.remove(false);
+ }
+ catch (e) {
+ Zotero.File.checkFileAccessError(e, destFile, 'delete');
+ }
+ }
+
+ // saveURI() below appears not to create empty files for Content-Length: 0,
+ // so we create one here just in case
+ try {
+ destFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
+ }
+ catch (e) {
+ Zotero.File.checkFileAccessError(e, destFile, 'create');
+ }
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onStart: function (request, data) {
+ if (data.request.isFinished()) {
+ Zotero.debug("Download request " + data.request.name
+ + " stopped before download started -- closing channel");
+ request.cancel(0x804b0002); // NS_BINDING_ABORTED
+ return;
+ }
+ },
+ onProgress: function (a, b, c) {
+ request.onProgress(a, b, c)
+ },
+ onStop: function (request, status, response, data) {
+ if (status != 200) {
+ var msg = "Unexpected status code " + status
+ + " for request " + data.request.name
+ + " in Zotero.Sync.Storage.Module.ZFS.downloadFile()";
+ Zotero.debug(msg, 1);
+ Components.utils.reportError(msg);
+ Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError);
+ }
+
+ // Don't try to process if the request has been cancelled
+ if (data.request.isFinished()) {
+ Zotero.debug("Download request " + data.request.name
+ + " is no longer running after file download", 2);
+ return;
+ }
+
+ Zotero.debug("Finished download of " + destFile.path);
+
+ try {
+ Zotero.Sync.Storage.processDownload(data);
+ data.request.finish();
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+ },
+ request: request,
+ item: item,
+ compressed: info.compressed,
+ syncModTime: syncModTime,
+ syncHash: syncHash
+ }
+ );
+
+ var uri = getItemURI(item);
+
+ // Don't display password in console
+ var disp = uri.clone();
+ if (disp.password) {
+ disp.password = "********";
+ }
+ Zotero.debug('Saving ' + disp.spec + ' with saveURI()');
+ const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
+ var wbp = Components
+ .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(nsIWBP);
+ wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
+ wbp.progressListener = listener;
+ wbp.saveURI(uri, null, null, null, null, destFile);
+ }
+ catch (e) {
+ Zotero.Sync.Storage.EventManager.error(e);
+ }
+ });
+ },
+
+
+ uploadFile: function (request) {
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ Zotero.Sync.Storage.createUploadFile(request, function (data) { processUploadFile(data); });
+ }
+ else {
+ processUploadFile({ request: request });
+ }
+ },
+
+
+ getLastSyncTime: function (callback) {
+ var uri = this.userURI;
+ var successFileURI = uri.clone();
+ successFileURI.spec += "laststoragesync?auth=1";
+
+ // Cache the credentials at the root
+ var self = this;
+ this.cacheCredentials(function () {
+ Zotero.HTTP.doGet(successFileURI, function (req) {
+ if (req.responseText) {
+ Zotero.debug(req.responseText);
+ }
+ Zotero.debug(req.status);
+
+ if (req.status == 401 || req.status == 403) {
+ Zotero.debug("Clearing ZFS authentication credentials", 2);
+ _cachedCredentials = false;
+ }
+
+ if (req.status != 200 && req.status != 404) {
+ Zotero.Sync.Storage.EventManager.error(
+ "Unexpected status code " + req.status + " getting "
+ + "last file sync time"
+ );
+ }
+
+ if (req.status == 200) {
+ var ts = req.responseText;
+ var date = new Date(ts * 1000);
+ Zotero.debug("Last successful storage sync was " + date);
+ _lastSyncTime = ts;
+ }
+ else {
+ var ts = null;
+ _lastSyncTime = null;
+ }
+ callback(ts);
+ });
+ });
+ },
+
+
+ setLastSyncTime: function (callback, useLastSyncTime) {
+ if (useLastSyncTime) {
+ if (!_lastSyncTime) {
+ if (callback) {
+ callback();
+ }
+ return;
+ }
+
+ var sql = "REPLACE INTO version VALUES ('storage_zfs', ?)";
+ Zotero.DB.query(sql, { int: _lastSyncTime });
+
+ Zotero.debug("Clearing ZFS authentication credentials", 2);
+ _lastSyncTime = null;
+ _cachedCredentials = false;
+
+ if (callback) {
+ callback();
+ }
+
+ return;
+ }
+ _lastSyncTime = null;
+
+ var uri = this.userURI;
+ var successFileURI = uri.clone();
+ successFileURI.spec += "laststoragesync?auth=1";
+
+ Zotero.HTTP.doPost(successFileURI, "", function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ if (req.status != 200) {
+ var msg = "Unexpected status code " + req.status + " setting last file sync time";
+ Zotero.debug(msg, 1);
+ Components.utils.reportError(msg);
+ Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultError);
+ }
+
+ var ts = req.responseText;
+
+ var sql = "REPLACE INTO version VALUES ('storage_zfs', ?)";
+ Zotero.DB.query(sql, { int: ts });
+
+ Zotero.debug("Clearing ZFS authentication credentials", 2);
+ _cachedCredentials = false;
+
+ if (callback) {
+ callback();
+ }
+ });
+ },
+
+
+ cacheCredentials: function (callback) {
+ if (_cachedCredentials) {
+ Zotero.debug("Credentials are already cached");
+ setTimeout(function () {
+ callback();
+ }, 0);
+ return false;
+ }
+
+ var uri = this.rootURI;
+ // TODO: move to root uri
+ uri.spec += "?auth=1";
+ Zotero.HTTP.doGet(uri, function (req) {
+ if (req.status == 401) {
+ // TODO: localize
+ var msg = "File sync login failed\n\nCheck your username and password in the Sync pane of the Zotero preferences.";
+ Zotero.Sync.Storage.EventManager.error(msg);
+ }
+ else if (req.status != 200) {
+ var msg = "Unexpected status code " + req.status + " caching "
+ + "authentication credentials in Zotero.Sync.Storage.Module.ZFS.cacheCredentials()";
+ Zotero.debug(msg, 1);
+ Components.utils.reportError(msg);
+ Zotero.Sync.Storage.EventManager.error(Zotero.Sync.Storage.defaultErrorRestart);
+ }
+ Zotero.debug("Credentials are cached");
+ _cachedCredentials = true;
+ callback();
+ });
+ return true;
+ },
+
+
+ /**
+ * Remove all synced files from the server
+ */
+ purgeDeletedStorageFiles: function (callback) {
+ // If we don't have a user id we've never synced and don't need to bother
+ if (!Zotero.userID) {
+ Zotero.Sync.Storage.EventManager.skip();
+ return;
+ }
+
+ var sql = "SELECT value FROM settings WHERE setting=? AND key=?";
+ var values = Zotero.DB.columnQuery(sql, ['storage', 'zfsPurge']);
+ if (!values) {
+ Zotero.Sync.Storage.EventManager.skip();
+ return;
+ }
+
+ Zotero.debug("Unlinking synced files on ZFS");
+
+ var uri = this.userURI;
+ uri.spec += "removestoragefiles?";
+ // Unused
+ for each(var value in values) {
+ switch (value) {
+ case 'user':
+ uri.spec += "user=1&";
+ break;
+
+ case 'group':
+ uri.spec += "group=1&";
+ break;
+
+ default:
+ Zotero.Sync.Storage.EventManager.error(
+ "Invalid zfsPurge value '" + value + "' in ZFS purgeDeletedStorageFiles()"
+ );
+ }
+ }
+ uri.spec = uri.spec.substr(0, uri.spec.length - 1);
+
+ Zotero.HTTP.doPost(uri, "", function (xmlhttp) {
+ if (xmlhttp.status != 204) {
+ if (callback) {
+ callback(false);
+ }
+ Zotero.Sync.Storage.EventManager.error(
+ "Unexpected status code " + xmlhttp.status + " purging ZFS files"
+ );
+ }
+
+ var sql = "DELETE FROM settings WHERE setting=? AND key=?";
+ Zotero.DB.query(sql, ['storage', 'zfsPurge']);
+
+ if (callback) {
+ callback(true);
+ }
+
+ Zotero.Sync.Storage.EventManager.success();
+ });
+ }
+ }
+}());
diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js
index 0956f4a997..0bdfb0e129 100644
--- a/chrome/content/zotero/xpcom/sync.js
+++ b/chrome/content/zotero/xpcom/sync.js
@@ -425,7 +425,7 @@ Zotero.Sync.EventListener = new function () {
var sql = "REPLACE INTO syncDeleteLog VALUES (?, ?, ?, ?)";
var syncStatement = Zotero.DB.getStatement(sql);
- if (isItem && Zotero.Sync.Storage.isActive('webdav')) {
+ if (isItem && Zotero.Sync.Storage.isActive('WebDAV')) {
var storageEnabled = true;
var sql = "INSERT INTO storageDeleteLog VALUES (?, ?, ?)";
var storageStatement = Zotero.DB.getStatement(sql);
@@ -562,74 +562,59 @@ Zotero.Sync.Runner = new function () {
Zotero.Sync.Runner.setSyncStatus(Zotero.getString('sync.status.syncingFiles'));
- Zotero.Sync.Storage.sync(
- 'webdav',
-
- {
- // WebDAV success
+ var zfsSync = function (skipSyncNeeded) {
+ Zotero.Sync.Storage.sync('ZFS', {
+ // ZFS success
onSuccess: function () {
- syncNeeded = true;
-
- Zotero.Sync.Storage.sync(
- 'zfs',
-
- {
- // ZFS success
- onSuccess: function () {
- Zotero.Sync.Server.sync(finalCallbacks);
- },
-
- // ZFS skip
- onSkip: function () {
- if (syncNeeded) {
- Zotero.Sync.Server.sync(finalCallbacks);
- }
- },
-
- // ZFS cancel
- onStop: Zotero.Sync.Runner.stop,
-
- // ZFS failure
- onError: Zotero.Sync.Runner.error,
-
- onWarning: Zotero.Sync.Runner.warning
- }
- )
+ setTimeout(function () {
+ Zotero.Sync.Server.sync(finalCallbacks);
+ }, 0);
},
- // WebDAV skip
+ // ZFS skip
onSkip: function () {
- Zotero.Sync.Storage.sync(
- 'zfs',
-
- {
- // ZFS success
- onSuccess: function () {
- Zotero.Sync.Server.sync(finalCallbacks);
- },
-
- // ZFS skip
- onSkip: Zotero.Sync.Runner.stop,
-
- // ZFS cancel
- onStop: Zotero.Sync.Runner.stop,
-
- // ZFS failure
- onError: Zotero.Sync.Runner.error,
-
- onWarning: Zotero.Sync.Runner.warning
+ setTimeout(function () {
+ if (skipSyncNeeded) {
+ Zotero.Sync.Server.sync(finalCallbacks);
}
- )
+ else {
+ Zotero.Sync.Runner.stop();
+ }
+ }, 0);
},
- // WebDAV cancel
- onStop: Zotero.Sync.Runner.stop,
+ // ZFS cancel
+ onStop: function () {
+ setTimeout(function () {
+ Zotero.Sync.Runner.stop();
+ }, 0);
+ },
- // WebDAV failure
- onError: Zotero.Sync.Runner.error
- }
- )
- }
+ // ZFS failure
+ onError: Zotero.Sync.Runner.error,
+
+ onWarning: Zotero.Sync.Runner.warning
+ })
+ };
+
+ Zotero.Sync.Storage.sync('WebDAV', {
+ // WebDAV success
+ onSuccess: function () {
+ zfsSync(true);
+ },
+
+ // WebDAV skip
+ onSkip: function () {
+ zfsSync();
+ },
+
+ // WebDAV cancel
+ onStop: Zotero.Sync.Runner.stop,
+
+ // WebDAV failure
+ onError: Zotero.Sync.Runner.error
+ });
+ };
Zotero.Sync.Server.sync({
// Sync 1 success
diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js
index 3cf9afb247..989b5316f7 100644
--- a/chrome/content/zotero/xpcom/zotero.js
+++ b/chrome/content/zotero/xpcom/zotero.js
@@ -1859,12 +1859,12 @@ const ZOTERO_CONFIG = {
Zotero.Relations.purge();
if (!skipStoragePurge && Math.random() < 1/10) {
- Zotero.Sync.Storage.purgeDeletedStorageFiles('zfs');
- Zotero.Sync.Storage.purgeDeletedStorageFiles('webdav');
+ Zotero.Sync.Storage.purgeDeletedStorageFiles('ZFS');
+ Zotero.Sync.Storage.purgeDeletedStorageFiles('WebDAV');
}
if (!skipStoragePurge) {
- Zotero.Sync.Storage.purgeOrphanedStorageFiles('webdav');
+ Zotero.Sync.Storage.purgeOrphanedStorageFiles('WebDAV');
}
}
@@ -1911,6 +1911,32 @@ Zotero.Prefs = new function(){
// Register observer to handle pref changes
this.register();
+
+ // Process pref version updates
+ var fromVersion = this.get('prefVersion');
+ if (!fromVersion) {
+ fromVersion = 0;
+ }
+ var toVersion = 1;
+ if (fromVersion < toVersion) {
+ for (var i = fromVersion + 1; i <= toVersion; i++) {
+ switch (i) {
+ case 1:
+ // If a sync username is entered and ZFS is enabled, turn
+ // on-demand downloading off to maintain current behavior
+ if (this.get('sync.server.username')) {
+ if (this.get('sync.storage.enabled')
+ && this.get('sync.storage.protocol') == 'zotero') {
+ this.set('sync.storage.downloadMode.personal', 'on-sync');
+ }
+ if (this.get('sync.storage.groups.enabled')) {
+ this.set('sync.storage.downloadMode.groups', 'on-sync');
+ }
+ }
+ }
+ }
+ this.set('prefVersion', toVersion);
+ }
}
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index 9a5bea8465..cb29234dc9 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -2590,14 +2590,8 @@ var ZoteroPane = new function()
createInstance(Components.interfaces.nsIURI);
var snapID = item.getBestAttachment();
if (snapID) {
- spec = Zotero.Items.get(snapID).getLocalFileURL();
- if (spec) {
- uri.spec = spec;
- if (uri.scheme && uri.scheme == 'file') {
- ZoteroPane_Local.viewAttachment(snapID, event);
- return;
- }
- }
+ ZoteroPane_Local.viewAttachment(snapID, event);
+ return;
}
var uri = item.getField('url');
@@ -3352,22 +3346,22 @@ var ZoteroPane = new function()
}
for each(var itemID in itemIDs) {
- var attachment = Zotero.Items.get(itemID);
- if (!attachment.isAttachment()) {
+ var item = Zotero.Items.get(itemID);
+ if (!item.isAttachment()) {
throw ("Item " + itemID + " is not an attachment in ZoteroPane_Local.viewAttachment()");
}
- if (attachment.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
- this.loadURI(attachment.getField('url'), event);
+ if (item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
+ this.loadURI(item.getField('url'), event);
continue;
}
- var file = attachment.getFile();
+ var file = item.getFile();
if (file) {
if(forceExternalViewer !== undefined) {
var externalViewer = forceExternalViewer;
} else {
- var mimeType = attachment.attachmentMIMEType;
+ var mimeType = item.attachmentMIMEType;
// If no MIME type specified, try to detect again (I guess in case
// we've gotten smarter since the file was imported?)
if (!mimeType) {
@@ -3393,12 +3387,43 @@ var ZoteroPane = new function()
}
catch (e) {
Zotero.debug("launch() not supported -- passing file to loadURI()");
- var fileURL = attachment.getLocalFileURL();
+ var fileURL = item.getLocalFileURL();
this.loadURI(fileURL);
}
}
}
else {
+ if (item.isImportedAttachment() && Zotero.Sync.Storage.downloadAsNeeded(item.libraryID)) {
+ let downloadedItem = item;
+ var started = Zotero.Sync.Storage.downloadFile(item, {
+ onStart: function (request) {
+ if (!(request instanceof Zotero.Sync.Storage.Request)) {
+ throw new Error("Invalid request object");
+ }
+ },
+
+ onProgress: function (progress, progressMax) {
+
+ },
+
+ onStop: function () {
+ if (!downloadedItem.getFile()) {
+ ZoteroPane_Local.showAttachmentNotFoundDialog(itemID, noLocateOnMissing);
+ return;
+ }
+
+ // check if unchanged?
+ // maybe not necessary, since we'll get an error if there's an error
+
+ ZoteroPane_Local.viewAttachment(downloadedItem.id, event, false, forceExternalViewer);
+ },
+ });
+
+ if (started) {
+ continue;
+ }
+ }
+
this.showAttachmentNotFoundDialog(itemID, noLocateOnMissing);
}
}
diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul
index 1ba2c61340..973e09a167 100644
--- a/chrome/content/zotero/zoteroPane.xul
+++ b/chrome/content/zotero/zoteroPane.xul
@@ -38,7 +38,7 @@
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
+
@@ -413,8 +413,17 @@
flex="1" zotero-persist="width ordinal hidden sortActive sortDirection"/>
+
+
diff --git a/chrome/locale/en-US/zotero/preferences.dtd b/chrome/locale/en-US/zotero/preferences.dtd
index b8c78505d8..21cc2ed634 100644
--- a/chrome/locale/en-US/zotero/preferences.dtd
+++ b/chrome/locale/en-US/zotero/preferences.dtd
@@ -3,6 +3,7 @@
+
@@ -58,7 +59,9 @@
-
+
+
+
@@ -69,6 +72,7 @@
+
diff --git a/chrome/skin/default/zotero/attach-small.png b/chrome/skin/default/zotero/attach-small.png
new file mode 100644
index 0000000000..8cfef5693d
Binary files /dev/null and b/chrome/skin/default/zotero/attach-small.png differ
diff --git a/chrome/skin/default/zotero/bullet_blue.png b/chrome/skin/default/zotero/bullet_blue.png
new file mode 100755
index 0000000000..a7651ec8a0
Binary files /dev/null and b/chrome/skin/default/zotero/bullet_blue.png differ
diff --git a/chrome/skin/default/zotero/bullet_blue_empty.png b/chrome/skin/default/zotero/bullet_blue_empty.png
new file mode 100644
index 0000000000..b3b2b40c6f
Binary files /dev/null and b/chrome/skin/default/zotero/bullet_blue_empty.png differ
diff --git a/chrome/skin/default/zotero/bullet_yellow.png b/chrome/skin/default/zotero/bullet_yellow.png
new file mode 100755
index 0000000000..6469cea7e9
Binary files /dev/null and b/chrome/skin/default/zotero/bullet_yellow.png differ
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
index b893677073..963cde0023 100644
--- a/chrome/skin/default/zotero/overlay.css
+++ b/chrome/skin/default/zotero/overlay.css
@@ -66,6 +66,11 @@
background-image: none;
}
+#zotero-items-column-hasAttachment, #zotero-items-column-hasNote
+{
+ min-width: 21px;
+}
+
#zotero-items-tree treechildren::-moz-tree-image
{
margin-right: 5px;
diff --git a/chrome/skin/default/zotero/preferences.css b/chrome/skin/default/zotero/preferences.css
index bc58f315f5..3e039bbb7b 100644
--- a/chrome/skin/default/zotero/preferences.css
+++ b/chrome/skin/default/zotero/preferences.css
@@ -22,7 +22,7 @@ radio[pane]
}
/* Links within messages */
-description label[class=text-link], label label[class=text-link]
+description label[class=zotero-text-link], label label[class=zotero-text-link]
{
margin: 0;
}
@@ -114,10 +114,9 @@ grid row hbox:first-child
margin-right: 10px;
}
-#storage-settings
+.storage-settings-download-options
{
- margin-left: 10px;
- margin-right: 5px;
+ margin-left: 40px;
}
#storage-verify, #storage-abort, #storage-clean
@@ -137,7 +136,7 @@ grid row hbox:first-child
margin-right: .25em;
}
-#storage-terms > label[class=text-link]
+#storage-terms > label[class=zotero-text-link]
{
margin-right: 0;
}
diff --git a/chrome/skin/default/zotero/treeitem-note-small.png b/chrome/skin/default/zotero/treeitem-note-small.png
new file mode 100644
index 0000000000..9595fe786f
Binary files /dev/null and b/chrome/skin/default/zotero/treeitem-note-small.png differ
diff --git a/components/zotero-service.js b/components/zotero-service.js
index 8f9a8d80d7..ca753e5789 100644
--- a/components/zotero-service.js
+++ b/components/zotero-service.js
@@ -96,7 +96,12 @@ const xpcomFilesLocal = [
'style',
'sync',
'storage',
- 'storage/session',
+ 'storage/streamListener',
+ 'storage/eventManager',
+ 'storage/queueManager',
+ 'storage/queue',
+ 'storage/request',
+ 'storage/module',
'storage/zfs',
'storage/webdav',
'timeline',
diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js
index 523c1ae587..18e7094cce 100644
--- a/defaults/preferences/zotero.js
+++ b/defaults/preferences/zotero.js
@@ -140,6 +140,8 @@ pref("extensions.zotero.sync.storage.maxDownloads", 4);
pref("extensions.zotero.sync.storage.maxUploads", 4);
pref("extensions.zotero.sync.storage.deleteDelayDays", 30);
pref("extensions.zotero.sync.storage.groups.enabled", true);
+pref("extensions.zotero.sync.storage.downloadMode.personal", "on-demand");
+pref("extensions.zotero.sync.storage.downloadMode.groups", "on-demand");
// Proxy
pref("extensions.zotero.proxies.autoRecognize", true);