diff --git a/chrome/content/zotero/overlay.xul b/chrome/content/zotero/overlay.xul
index a9c3e95ab9..f4d487f129 100644
--- a/chrome/content/zotero/overlay.xul
+++ b/chrome/content/zotero/overlay.xul
@@ -133,6 +133,10 @@
+
+
+
+
@@ -305,16 +309,49 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ oncommand="Zotero.Sync.Runner.sync()">
+ noautohide="true">
+
diff --git a/chrome/content/zotero/preferences/preferences.js b/chrome/content/zotero/preferences/preferences.js
index a2003390b8..b68bd5efcb 100644
--- a/chrome/content/zotero/preferences/preferences.js
+++ b/chrome/content/zotero/preferences/preferences.js
@@ -137,6 +137,170 @@ function populateOpenURLResolvers() {
}
+//
+// Sync
+//
+function unverifyStorageServer() {
+ Zotero.debug("Clearing storage settings");
+ Zotero.Sync.Storage.clearSettingsCache();
+ Zotero.Prefs.set('sync.storage.verified', false);
+}
+
+function verifyStorageServer() {
+ Zotero.debug("Verifying storage");
+
+ var verifyButton = document.getElementById("storage-verify");
+ var abortButton = document.getElementById("storage-abort");
+ var progressMeter = document.getElementById("storage-progress");
+
+ var callback = function (uri, status, authRequired) {
+ verifyButton.hidden = false;
+ abortButton.hidden = true;
+ progressMeter.hidden = true;
+
+ var promptService =
+ Components.classes["@mozilla.org/network/default-prompt;1"].
+ createInstance(Components.interfaces.nsIPrompt);
+ if (uri) {
+ var spec = uri.scheme + '://' + uri.hostPort + uri.path;
+ }
+
+ switch (status) {
+ case Zotero.Sync.Storage.SUCCESS:
+ promptService.alert(
+ "Server configuration verified",
+ "File storage is successfully set up."
+ );
+ Zotero.Prefs.set("sync.storage.verified", true);
+ return true;
+
+ case Zotero.Sync.Storage.ERROR_NO_URL:
+ var errorMessage = "Please enter a URL.";
+ setTimeout(function () {
+ document.getElementById("storage-url").focus();
+ }, 1);
+ break;
+
+ case Zotero.Sync.Storage.ERROR_NO_USERNAME:
+ var errorMessage = "Please enter a username.";
+ setTimeout(function () {
+ document.getElementById("storage-username").focus();
+ }, 1);
+ break;
+
+ case Zotero.Sync.Storage.ERROR_NO_PASSWORD:
+ var errorMessage = "Please enter a password.";
+ setTimeout(function () {
+ document.getElementById("storage-password").focus();
+ }, 1);
+ break;
+
+ case Zotero.Sync.Storage.ERROR_UNREACHABLE:
+ var errorMessage = "The server " + uri.host + " could not be reached.";
+ break;
+
+ case Zotero.Sync.Storage.ERROR_NOT_DAV:
+ var errorMessage = spec + " is not a valid WebDAV URL.";
+ break;
+
+ case Zotero.Sync.Storage.ERROR_AUTH_FAILED:
+ var errorTitle = "Permission denied";
+ var errorMessage = "The server did not accept the username and "
+ + "password you entered." + " "
+ + "Please check your server settings "
+ + "or contact your server administrator.";
+ break;
+
+ case Zotero.Sync.Storage.ERROR_FORBIDDEN:
+ var errorTitle = "Permission denied";
+ var errorMessage = "You don't have permission to access "
+ + uri.path + " on this server." + " "
+ + "Please check your server settings "
+ + "or contact your server administrator.";
+ break;
+
+ case Zotero.Sync.Storage.ERROR_PARENT_DIR_NOT_FOUND:
+ var errorTitle = "Directory not found";
+ var parentSpec = spec.replace(/\/zotero\/$/, "");
+ var errorMessage = parentSpec + " does not exist.";
+ break;
+
+ case Zotero.Sync.Storage.ERROR_ZOTERO_DIR_NOT_FOUND:
+ var create = promptService.confirmEx(
+ // TODO: localize
+ "Directory not found",
+ spec + " does not exist.\n\nDo you want to create it now?",
+ promptService.BUTTON_POS_0
+ * promptService.BUTTON_TITLE_IS_STRING
+ + promptService.BUTTON_POS_1
+ * promptService.BUTTON_TITLE_CANCEL,
+ "Create",
+ null, null, null, {}
+ );
+
+ if (create != 0) {
+ return;
+ }
+
+ Zotero.Sync.Storage.createServerDirectory(function (uri, status) {
+ switch (status) {
+ case Zotero.Sync.Storage.SUCCESS:
+ promptService.alert(
+ "Server configuration verified",
+ "File storage is successfully set up."
+ );
+ Zotero.Prefs.set("sync.storage.verified", true);
+ return true;
+
+ case Zotero.Sync.Storage.ERROR_FORBIDDEN:
+ var errorTitle = "Permission denied";
+ var errorMessage = "You do not have "
+ + "permission to create a Zotero directory "
+ + "at the following address:" + "\n\n" + spec;
+ errorMessage += "\n\n"
+ + "Please check your server settings or "
+ + "contact your server administrator.";
+ break;
+ }
+
+ // TEMP
+ if (!errorMessage) {
+ var errorMessage = status;
+ }
+ promptService.alert(errorTitle, errorMessage);
+ });
+
+ return false;
+ }
+
+ if (!errorTitle) {
+ var errorTitle = Zotero.getString("general.error");
+ }
+ // TEMP
+ if (!errorMessage) {
+ var errorMessage = status;
+ }
+ promptService.alert(errorTitle, errorMessage);
+ return false;
+ }
+
+ verifyButton.hidden = true;
+ abortButton.hidden = false;
+ progressMeter.hidden = false;
+ var requestHolder = Zotero.Sync.Storage.checkServer(callback);
+ abortButton.onclick = function () {
+ if (requestHolder.request) {
+ requestHolder.request.onreadystatechange = undefined;
+ requestHolder.request.abort();
+ verifyButton.hidden = false;
+ abortButton.hidden = true;
+ progressMeter.hidden = true;
+ }
+ }
+}
+
+
+
/*
* Builds the main Quick Copy drop-down from the current global pref
*/
diff --git a/chrome/content/zotero/preferences/preferences.xul b/chrome/content/zotero/preferences/preferences.xul
index 7180b20ed8..3e8100e0c8 100644
--- a/chrome/content/zotero/preferences/preferences.xul
+++ b/chrome/content/zotero/preferences/preferences.xul
@@ -156,31 +156,116 @@ To add a new preference:
-
+
+
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -282,8 +367,12 @@ To add a new preference:
+
-
+
+
@@ -511,9 +600,8 @@ To add a new preference:
-
-
+
\ No newline at end of file
diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js
index 0f499703b7..89eef9aa3c 100644
--- a/chrome/content/zotero/xpcom/attachments.js
+++ b/chrome/content/zotero/xpcom/attachments.js
@@ -943,13 +943,62 @@ Zotero.Attachments = new function(){
function getPath(file, linkMode) {
if (linkMode == self.LINK_MODE_IMPORTED_URL ||
linkMode == self.LINK_MODE_IMPORTED_FILE) {
- return 'storage:' + file.leafName;
+ file.QueryInterface(Components.interfaces.nsILocalFile);
+ var fileName = file.getRelativeDescriptor(file.parent);
+ return 'storage:' + fileName;
}
return file.persistentDescriptor;
}
+ /**
+ * @param {Zotero.Item} item
+ * @param {Boolean} [skipHidden=FALSE] Don't count hidden files
+ * @return {Integer} Total file size in bytes
+ */
+ this.getTotalFileSize = function (item, skipHidden) {
+ var funcName = "Zotero.Attachments.getTotalFileSize()";
+
+ if (!item.isAttachment()) {
+ throw ("Item is not an attachment in " + funcName);
+ }
+
+ var linkMode = item.attachmentLinkMode;
+ switch (linkMode) {
+ case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
+ case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
+ case Zotero.Attachments.LINK_MODE_LINKED_FILE:
+ break;
+
+ default:
+ throw ("Invalid attachment link mode in " + funcName);
+ }
+
+ var file = item.getFile();
+ if (!file) {
+ throw ("File not found in " + funcName);
+ }
+
+ if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
+ return item.fileSize;
+ }
+
+ var parentDir = file.parent;
+ var files = parentDir.directoryEntries;
+ var size = 0;
+ while (files.hasMoreElements()) {
+ file = files.getNext();
+ file.QueryInterface(Components.interfaces.nsIFile);
+ if (skipHidden && file.leafName.indexOf('.') == 0) {
+ continue;
+ }
+ size += file.fileSize;
+ }
+ return size;
+ }
+
+
function _getFileNameFromURL(url, mimeType){
var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"]
.createInstance(Components.interfaces.nsIURL);
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
index 32ed36b48e..d61b0266e1 100644
--- a/chrome/content/zotero/xpcom/data/item.js
+++ b/chrome/content/zotero/xpcom/data/item.js
@@ -87,8 +87,9 @@ Zotero.Item.prototype._init = function () {
this._attachmentLinkMode = null;
this._attachmentMIMEType = null;
- this._attachmentCharset = null;
+ this._attachmentCharset;
this._attachmentPath = null;
+ this._attachmentSyncState;
this._relatedItems = false;
}
@@ -1254,22 +1255,13 @@ Zotero.Item.prototype.save = function() {
// Attachment
if (this.isAttachment()) {
var sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, "
- + "mimeType, charsetID, path) VALUES (?,?,?,?,?,?)";
+ + "mimeType, charsetID, path, syncState) VALUES (?,?,?,?,?,?,?)";
var parent = this.getSource();
var linkMode = this.attachmentLinkMode;
- switch (linkMode) {
- case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
- case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
- case Zotero.Attachments.LINK_MODE_LINKED_FILE:
- case Zotero.Attachments.LINK_MODE_LINKED_URL:
- break;
-
- default:
- throw ("Invalid attachment link mode " + linkMode + " in Zotero.Item.save()");
- }
var mimeType = this.attachmentMIMEType;
var charsetID = this.attachmentCharset;
var path = this.attachmentPath;
+ var syncState = this.attachmentSyncState;
var bindParams = [
itemID,
@@ -1277,7 +1269,8 @@ Zotero.Item.prototype.save = function() {
{ int: linkMode },
mimeType ? { string: mimeType } : null,
charsetID ? { int: charsetID } : null,
- path ? { string: path } : null
+ path ? { string: path } : null,
+ syncState ? { int: syncState } : 0
];
Zotero.DB.query(sql, bindParams);
}
@@ -1596,21 +1589,24 @@ Zotero.Item.prototype.save = function() {
// Attachment
if (this._changedAttachmentData) {
- var sql = "REPLACE INTO itemAttachments (itemID, sourceItemID, linkMode, "
- + "mimeType, charsetID, path) VALUES (?,?,?,?,?,?)";
+ var sql = "UPDATE itemAttachments SET sourceItemID=?, "
+ + "linkMode=?, mimeType=?, charsetID=?, path=?, syncState=? "
+ + "WHERE itemID=?";
var parent = this.getSource();
var linkMode = this.attachmentLinkMode;
var mimeType = this.attachmentMIMEType;
var charsetID = this.attachmentCharset;
var path = this.attachmentPath;
+ var syncState = this.attachmentSyncState;
var bindParams = [
- this.id,
parent ? parent : null,
{ int: linkMode },
mimeType ? { string: mimeType } : null,
charsetID ? { int: charsetID } : null,
- path ? { string: path } : null
+ path ? { string: path } : null,
+ syncState ? { int: syncState } : 0,
+ this.id
];
Zotero.DB.query(sql, bindParams);
}
@@ -2109,7 +2105,7 @@ Zotero.Item.prototype.numAttachments = function() {
* Get an nsILocalFile for the attachment, or false if the associated file
* doesn't exist
*
-* _row_ is optional itemAttachments row if available to skip query
+* _row_ is optional itemAttachments row if available to skip queries
*
* Note: Always returns false for items with LINK_MODE_LINKED_URL,
* since they have no files -- use getField('url') instead
@@ -2120,12 +2116,10 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
}
if (!row) {
- var sql = "SELECT linkMode, path FROM itemAttachments WHERE itemID=?"
- var row = Zotero.DB.rowQuery(sql, this.id);
- }
-
- if (!row) {
- throw ('Attachment data not found for item ' + this.id + ' in getFile()');
+ var row = {
+ linkMode: this.attachmentLinkMode,
+ path: this.attachmentPath
+ };
}
// No associated files for linked URLs
@@ -2144,7 +2138,7 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
var path = row.path.substr(8);
var file = Zotero.Attachments.getStorageDirectory(this.id);
file.QueryInterface(Components.interfaces.nsILocalFile);
- file.append(path);
+ file.setRelativeDescriptor(file, path);
if (!file.exists()) {
Zotero.debug("Attachment file '" + path + "' not found");
throw ('File not found');
@@ -2321,7 +2315,8 @@ Zotero.Item.prototype.__defineSetter__('attachmentLinkMode', function (val) {
break;
default:
- throw ("Invalid attachment link mode '" + val + "' in Zotero.Item.attachmentLinkMode setter");
+ throw ("Invalid attachment link mode '" + val
+ + "' in Zotero.Item.attachmentLinkMode setter");
}
if (val === this._attachmentLinkMode) {
@@ -2402,18 +2397,18 @@ Zotero.Item.prototype.__defineGetter__('attachmentCharset', function () {
return undefined;
}
- if (this._attachmentCharset !== null) {
+ if (this._attachmentCharset != undefined) {
return this._attachmentCharset;
}
if (!this.id) {
- return '';
+ return null;
}
var sql = "SELECT charsetID FROM itemAttachments WHERE itemID=?";
var charset = Zotero.DB.valueQuery(sql, this.id);
if (!charset) {
- charset = '';
+ charset = null;
}
this._attachmentCharset = charset;
return charset;
@@ -2425,8 +2420,10 @@ Zotero.Item.prototype.__defineSetter__('attachmentCharset', function (val) {
throw (".attachmentCharset can only be set for attachment items");
}
+ val = Zotero.CharacterSets.getID(val);
+
if (!val) {
- val = '';
+ val = null;
}
if (val == this._attachmentCharset) {
@@ -2489,6 +2486,90 @@ Zotero.Item.prototype.__defineSetter__('attachmentPath', function (val) {
});
+Zotero.Item.prototype.__defineGetter__('attachmentSyncState', function () {
+ if (!this.isAttachment()) {
+ return undefined;
+ }
+
+ if (this._attachmentSyncState != undefined) {
+ return this._attachmentSyncState;
+ }
+
+ if (!this.id) {
+ return undefined;
+ }
+
+ var sql = "SELECT syncState FROM itemAttachments WHERE itemID=?";
+ var syncState = Zotero.DB.valueQuery(sql, this.id);
+ this._attachmentSyncState = syncState;
+ return syncState;
+});
+
+
+Zotero.Item.prototype.__defineSetter__('attachmentSyncState', function (val) {
+ if (!this.isAttachment()) {
+ throw ("attachmentSyncState can only be set for attachment items");
+ }
+
+ switch (this.attachmentLinkMode) {
+ case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
+ case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
+ break;
+
+ default:
+ throw ("attachmentSyncState can only be set for snapshots and "
+ + "imported files");
+ }
+
+ switch (val) {
+ case Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD:
+ case Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD:
+ case Zotero.Sync.Storage.SYNC_STATE_IN_SYNC:
+ break;
+
+ default:
+ throw ("Invalid sync state '" + val
+ + "' in Zotero.Item.attachmentSyncState setter");
+ }
+
+ if (val == this._attachmentSyncState) {
+ return;
+ }
+
+ if (!this._changedAttachmentData) {
+ this._changedAttachmentData = {};
+ }
+ this._changedAttachmentData.syncState = true;
+ this._attachmentSyncState = val;
+});
+
+
+/**
+ * Modification time of an attachment file
+ *
+ * Note: This is the mod time of the file itself, not the last-known mod time
+ * of the file on the storage server as stored in the database
+ *
+ * @return {Number} File modification time as UNIX timestamp
+ */
+Zotero.Item.prototype.__defineGetter__('attachmentModificationTime', function () {
+ if (!this.isAttachment()) {
+ return undefined;
+ }
+
+ if (!this.id) {
+ return undefined;
+ }
+
+ var file = this.getFile();
+ if (!file) {
+ return undefined;
+ }
+
+ return file.lastModifiedTime / 1000;
+});
+
+
/**
* Returns an array of attachment itemIDs that have this item as a source,
* or FALSE if none
@@ -2579,16 +2660,26 @@ Zotero.Item.prototype.addTag = function(name, type) {
Zotero.DB.beginTransaction();
- var existingTypes = Zotero.Tags.getTypes(name);
- if (existingTypes) {
- // If existing automatic and adding identical user, remove automatic
- if (type == 0 && existingTypes.indexOf(1) != -1) {
- this.removeTag(Zotero.Tags.getID(name, 1));
- }
- else {
- Zotero.debug('Identical tag already exists -- not adding tag');
- Zotero.DB.commitTransaction();
- return false;
+ var matchingTags = Zotero.Tags.getIDs(name);
+ if (matchingTags) {
+ var itemTags = this.getTags();
+ for each(var id in matchingTags) {
+ if (itemTags.indexOf(id) != -1) {
+ var tag = Zotero.Tags.get(id);
+ // If existing automatic and adding identical user,
+ // remove automatic
+ if (type == 0 && tag.type == 1) {
+ this.removeTag(id);
+ break;
+ }
+ // If existing user and adding automatic, skip
+ else if (type == 1 && tag.type == 0) {
+ Zotero.debug("Identical user tag '" + name
+ + "' already exists -- skipping automatic tag");
+ Zotero.DB.commitTransaction();
+ return false;
+ }
+ }
}
}
@@ -2601,9 +2692,9 @@ Zotero.Item.prototype.addTag = function(name, type) {
}
try {
- this.addTagByID(tagID);
+ var added = this.addTagByID(tagID);
Zotero.DB.commitTransaction();
- return tagID;
+ return added ? tagID : false;
}
catch (e) {
Zotero.DB.rollbackTransaction();
@@ -2641,8 +2732,12 @@ Zotero.Item.prototype.addTagByID = function(tagID) {
throw ('Cannot add invalid tag ' + tagID + ' in Zotero.Item.addTagByID()');
}
- tag.addItem(this.id);
+ var added = tag.addItem(this.id);
+ if (!added) {
+ return false;
+ }
tag.save();
+ return true;
}
Zotero.Item.prototype.hasTag = function(tagID) {
diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js
index d4839da42e..08e7608519 100644
--- a/chrome/content/zotero/xpcom/data/items.js
+++ b/chrome/content/zotero/xpcom/data/items.js
@@ -401,6 +401,11 @@ Zotero.Items = new function() {
var sql = "DELETE FROM itemDataValues WHERE valueID NOT IN "
+ "(SELECT valueID FROM itemData)";
Zotero.DB.query(sql);
+
+ var ZU = new Zotero.Utilities;
+ if (Zotero.Sync.Storage.active && ZU.probability(10)) {
+ Zotero.Sync.Storage.purgeDeletedStorageFiles();
+ }
}
diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js
index e85e128a33..69efb7bacd 100644
--- a/chrome/content/zotero/xpcom/schema.js
+++ b/chrome/content/zotero/xpcom/schema.js
@@ -1671,6 +1671,14 @@ Zotero.Schema = new function(){
Zotero.DB.query("CREATE TABLE proxyHosts (\n hostID INTEGER PRIMARY KEY,\n proxyID INTEGER,\n hostname TEXT,\n FOREIGN KEY (proxyID) REFERENCES proxies(proxyID)\n)");
Zotero.DB.query("CREATE INDEX proxyHosts_proxyID ON proxyHosts(proxyID)");
}
+
+ if (i==40) {
+ Zotero.DB.query("ALTER TABLE itemAttachments ADD COLUMN syncState INT DEFAULT 0");
+ Zotero.DB.query("ALTER TABLE itemAttachments ADD COLUMN storageModTime INT");
+ Zotero.DB.query("CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState)");
+ Zotero.DB.query("CREATE TABLE storageDeleteLog (\n key TEXT PRIMARY KEY,\n timestamp INT NOT NULL\n)");
+ Zotero.DB.query("CREATE INDEX storageDeleteLog_timestamp ON storageDeleteLog(timestamp)");
+ }
}
_updateDBVersion('userdata', toVersion);
diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js
new file mode 100644
index 0000000000..86bb6f4277
--- /dev/null
+++ b/chrome/content/zotero/xpcom/storage.js
@@ -0,0 +1,2168 @@
+Zotero.Sync.Storage = new function () {
+ //
+ // Constants
+ //
+ this.SYNC_STATE_TO_UPLOAD = 0;
+ this.SYNC_STATE_TO_DOWNLOAD = 1;
+ this.SYNC_STATE_IN_SYNC = 2;
+
+ this.SUCCESS = 1;
+ this.ERROR_NO_URL = -1;
+ this.ERROR_NO_USERNAME = -2;
+ this.ERROR_NO_PASSWORD = -3;
+ this.ERROR_OFFLINE = -4;
+ this.ERROR_UNREACHABLE = -5;
+ this.ERROR_SERVER_ERROR = -6;
+ this.ERROR_NOT_DAV = -7;
+ this.ERROR_BAD_REQUEST = -8;
+ this.ERROR_AUTH_FAILED = -9;
+ this.ERROR_FORBIDDEN = -10;
+ this.ERROR_PARENT_DIR_NOT_FOUND = -11;
+ this.ERROR_ZOTERO_DIR_NOT_FOUND = -12;
+ this.ERROR_ZOTERO_DIR_NOT_WRITABLE = -13;
+ this.ERROR_NOT_ALLOWED = -14;
+ this.ERROR_UNKNOWN = -15;
+
+
+ //
+ // Public properties
+ //
+
+ /**
+ * URI of Zotero directory on storage server
+ *
+ * @return {nsIURI} nsIURI of data directory, with spec ending in '/'
+ */
+ this.__defineGetter__('rootURI', function () {
+ if (_rootURI) {
+ return _rootURI.clone()
+ }
+
+ var spec = Zotero.Prefs.get('sync.storage.url');
+ if (!spec) {
+ var msg = "Zotero storage URL not provided";
+ Zotero.debug(msg);
+ throw ({
+ message: msg,
+ name: "Z_ERROR_NO_URL",
+ filename: "storage.js",
+ toString: function () { return this.message; }
+ });
+ }
+ var username = Zotero.Sync.Storage.username;
+ if (!username) {
+ var msg = "Zotero storage username not provided";
+ Zotero.debug(msg);
+ throw ({
+ message: msg,
+ name: "Z_ERROR_NO_USERNAME",
+ filename: "storage.js",
+ toString: function () { return this.message; }
+ });
+ }
+ var password = Zotero.Sync.Storage.password;
+ if (!password) {
+ var msg = "Zotero storage password not provided";
+ Zotero.debug(msg);
+ throw ({
+ message: msg,
+ name: "Z_ERROR_NO_PASSWORD",
+ filename: "storage.js",
+ toString: function () { return this.message; }
+ });
+ }
+
+ spec = 'https://' + spec + '/zotero/';
+
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ try {
+ var uri = ios.newURI(spec, null, null);
+ uri.username = username;
+ uri.password = password;
+ }
+ catch (e) {
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ return false;
+ }
+ _rootURI = uri;
+ return _rootURI.clone();
+
+
+ return ;
+ });
+
+ this.__defineGetter__('username', function () {
+ return Zotero.Prefs.get('sync.storage.username');
+ });
+
+ this.__defineGetter__('password', function () {
+ var username = this.username;
+
+ if (!username) {
+ Zotero.debug('Username not set before setting Zotero.Sync.Storage.password');
+ return '';
+ }
+
+ if (_cachedCredentials.username && _cachedCredentials.username == username) {
+ return _cachedCredentials.password;
+ }
+
+ Zotero.debug('Getting Zotero storage 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) {
+ _cachedCredentials.username = username;
+ _cachedCredentials.password = logins[i].password;
+ return logins[i].password;
+ }
+ }
+
+ return '';
+ });
+
+ this.__defineSetter__('password', function (password) {
+ _rootURI = false;
+
+ var username = this.username;
+ if (!username) {
+ Zotero.debug('Username not set before setting Zotero.Sync.Server.password');
+ return;
+ }
+
+ _cachedCredentials = {};
+
+ 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 Zotero storage passwords');
+ loginManager.removeLogin(logins[i]);
+ break;
+ }
+
+ if (password) {
+ Zotero.debug('Setting Zotero storage password');
+ 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);
+ _cachedCredentials.username = username;
+ _cachedCredentials.password = password;
+ }
+ });
+
+ this.__defineGetter__('active', function () {
+ return Zotero.Prefs.get("sync.storage.enabled") &&
+ Zotero.Prefs.get("sync.storage.verified");
+ });
+
+ this.__defineGetter__("syncInProgress", function () _syncInProgress);
+
+ this.compressionTracker = {
+ compressed: 0,
+ uncompressed: 0,
+ get ratio() {
+ return Math.round(
+ (Zotero.Sync.Storage.compressionTracker.uncompressed -
+ Zotero.Sync.Storage.compressionTracker.compressed) /
+ Zotero.Sync.Storage.compressionTracker.uncompressed * 100);
+ }
+ }
+
+ //
+ // Private properties
+ //
+ var _loginManagerHost = 'chrome://zotero';
+ var _loginManagerURL = 'Zotero Storage Server';
+ var _cachedCredentials = { username: null, password: null, authHeader: null };
+ var _rootURI;
+ var _syncInProgress;
+ var _finishCallback;
+
+ // Queue
+ var _queues = {
+ download: { current: 0, queue: [] },
+ upload: { current: 0, queue: [] }
+ };
+ var _queueSimultaneous = {
+ download: null,
+ upload: null
+ };
+
+ // Progress
+ var _requests = {
+ download: {},
+ upload: {}
+ };
+ var _numRequests = {
+ download: { active: 0, queued: 0, done: 0 },
+ upload: { active: 0, queued: 0, done: 0 }
+ }
+ var _totalProgress = {
+ download: 0,
+ upload: 0
+ };
+ var _totalProgressMax = {
+ download: 0,
+ upload: 0
+ }
+ _requestSizeMultiplier = 1;
+
+
+ //
+ // Public methods
+ //
+ this.init = function () {
+ _queueSimultaneous.download = Zotero.Prefs.get('sync.storage.maxDownloads');
+ _queueSimultaneous.upload = Zotero.Prefs.get('sync.storage.maxUploads');
+ }
+
+
+ this.sync = function () {
+ if (!Zotero.Sync.Storage.active) {
+ Zotero.debug("Storage sync is not active");
+ Zotero.Sync.Runner.next();
+ return;
+ }
+
+ if (_syncInProgress) {
+ _error("Storage sync operation already in progress");
+ }
+
+ Zotero.debug("Beginning storage sync");
+ Zotero.Sync.Runner.setSyncIcon('animate');
+ _syncInProgress = true;
+
+ Zotero.Sync.Storage.checkForUpdatedFiles();
+
+ // If authorization header isn't cached, cache it before proceeding,
+ // since during testing Firefox 3.0.1 was being a bit amnesic with auth
+ // info for subsequent requests -- surely a better way to fix this
+ if (!_cachedCredentials.authHeader) {
+ Zotero.Utilities.HTTP.doOptions(Zotero.Sync.Storage.rootURI, function (req) {
+ var authHeader = Zotero.Utilities.HTTP.getChannelAuthorization(req.channel);
+ if (authHeader) {
+ _cachedCredentials.authHeader = authHeader;
+ }
+
+ var activeDown = Zotero.Sync.Storage.downloadFiles();
+ var activeUp = Zotero.Sync.Storage.uploadFiles();
+ if (!activeDown && !activeUp) {
+ _syncInProgress = false;
+ Zotero.Sync.Runner.next();
+ }
+ });
+ return;
+ }
+
+ var activeDown = Zotero.Sync.Storage.downloadFiles();
+ var activeUp = Zotero.Sync.Storage.uploadFiles();
+ if (!activeDown && !activeUp) {
+ _syncInProgress = false;
+ Zotero.Sync.Runner.next();
+ }
+ }
+
+
+ /**
+ * @param {Integer} itemID
+ */
+ this.getSyncState = function (itemID) {
+ var sql = "SELECT syncState FROM itemAttachments WHERE itemID=?";
+ return Zotero.DB.valueQuery(sql, itemID);
+ }
+
+
+ /**
+ * @param {Integer} itemID
+ * @param {Integer} syncState Constant from Zotero.Sync.Storage
+ */
+ this.setSyncState = function (itemID, syncState) {
+ switch (syncState) {
+ case this.SYNC_STATE_TO_UPLOAD:
+ case this.SYNC_STATE_TO_DOWNLOAD:
+ case this.SYNC_STATE_IN_SYNC:
+ break;
+
+ default:
+ _error("Invalid sync state '" + syncState
+ + "' in Zotero.Sync.Storage.setSyncState()");
+ }
+
+ var sql = "UPDATE itemAttachments SET syncState=? WHERE itemID=?";
+ return Zotero.DB.valueQuery(sql, [syncState, itemID]);
+ }
+
+
+ /**
+ * @param {Integer} itemID
+ * @return {Integer|NULL} Mod time as Unix timestamp,
+ * or NULL if never synced
+ */
+ this.getSyncedModificationTime = function (itemID) {
+ var sql = "SELECT storageModTime FROM itemAttachments WHERE itemID=?";
+ var mtime = Zotero.DB.valueQuery(sql, itemID);
+ if (mtime === false) {
+ _error("Item " + itemID
+ + " not found in Zotero.Sync.Storage.getSyncedModificationTime()");
+ }
+ return mtime;
+ }
+
+
+ /**
+ * @param {Integer} itemID
+ * @param {Integer} mtime File modification time as
+ * Unix timestamp
+ * @param {Boolean} [updateItem=FALSE] Update dateModified field of
+ * attachment item
+ */
+ this.setSyncedModificationTime = function (itemID, mtime, updateItem) {
+ Zotero.DB.beginTransaction();
+
+ var sql = "UPDATE itemAttachments SET storageModTime=? WHERE itemID=?";
+ Zotero.DB.valueQuery(sql, [mtime, itemID]);
+
+ if (updateItem) {
+ // Update item date modified so the new mod time will be synced
+ var item = Zotero.Items.get(itemID);
+ //var date = new Date(mtime * 1000);
+ //item.setField('dateModified', Zotero.Date.dateToSQL(date, true));
+ item.setField('dateModified', Zotero.DB.transactionDateTime);
+ item.save();
+ }
+
+ Zotero.DB.commitTransaction();
+ }
+
+
+ /**
+ * Get mod time of file on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} callback Callback f(item, mdate)
+ */
+ this.getStorageModificationTime = function (item, callback) {
+ var prolog = '\n';
+ var D = new Namespace("D", "DAV:");
+ var dcterms = new Namespace("dcterms", "http://purl.org/dc/terms/");
+
+ var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '" '
+ + 'xmlns:' + dcterms.prefix + '=' + '"' + dcterms.uri + '" ';
+
+ // Retrieve Dublin Core 'modified' property
+ var requestXML = new XML('');
+ requestXML.D::prop = '';
+ requestXML.D::prop.dcterms::modified = '';
+
+ var xmlstr = prolog + requestXML.toXMLString();
+
+ var uri = _getItemURI(item);
+ var headers = _cachedCredentials.authHeader ?
+ { Authorization: _cachedCredentials.authHeader } : null;
+
+ Zotero.Utilities.HTTP.WebDAV.doProp('PROPFIND', uri, xmlstr, function (req) {
+ var funcName = "Zotero.Sync.Storage.getStorageModificationTime()";
+
+ if (req.status == 404) {
+ callback(item, false);
+ return;
+ }
+ else if (req.status != 207) {
+ _error("Unexpected status code " + req.status + " in " + funcName);
+ }
+
+ _checkResponse(req);
+
+ Zotero.debug(req.responseText);
+
+ var D = "DAV:";
+ var dcterms = "http://purl.org/dc/terms/";
+
+ // Error checking
+ var multistatus = req.responseXML.firstChild;
+ var responses = multistatus.getElementsByTagNameNS(D, "response");
+ if (responses.length == 0) {
+ _error("No sections found in " + funcName);
+ }
+ else if (responses.length > 1) {
+ _error("Multiple sections in " + funcName);
+ }
+
+ var response = responses.item(0);
+ var href = response.getElementsByTagNameNS(D, "href").item(0);
+ if (!href) {
+ _error("DAV:href not found in " + funcName);
+ }
+
+ if (href.firstChild.nodeValue != uri.path) {
+ _error("DAV:href does not match path in " + funcName);
+ }
+
+ var modified = response.getElementsByTagNameNS(dcterms, "modified").item(0);
+ if (!modified) {
+ _error("dcterms:modified not found in " + funcName);
+ }
+
+ // No modification time set
+ if (modified.childNodes.length == 0) {
+ callback(item, false);
+ return;
+ }
+
+ var mdate = Zotero.Date.isoToDate(modified.firstChild.nodeValue);
+ callback(item, mdate);
+ }, headers);
+ }
+
+
+ /**
+ * Set mod time of file on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} callback Callback f(item, mtime)
+ */
+ this.setStorageModificationTime = function (item, callback) {
+ var prolog = '\n';
+ var D = new Namespace("D", "DAV:");
+ var dcterms = new Namespace("dcterms", "http://purl.org/dc/terms/");
+
+ var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '" '
+ + 'xmlns:' + dcterms.prefix + '=' + '"' + dcterms.uri + '" ';
+
+ // Set Dublin Core 'modified' property
+ var requestXML = new XML('');
+
+ var mdate = new Date(item.attachmentModificationTime * 1000);
+ var modified = Zotero.Date.dateToISO(mdate);
+ requestXML.D::set.D::prop.dcterms::modified = modified;
+
+ var xmlstr = prolog + requestXML.toXMLString();
+
+ var uri = _getItemURI(item);
+ var headers = _cachedCredentials.authHeader ?
+ { Authorization: _cachedCredentials.authHeader } : null;
+
+ Zotero.Utilities.HTTP.WebDAV.doProp('PROPPATCH', uri, xmlstr, function (req) {
+ _checkResponse(req);
+
+ callback(item, Zotero.Date.toUnixTimestamp(mdate));
+ }, headers);
+ }
+
+
+ /**
+ * Check if modification time of file on disk matches the mod time
+ * in the database
+ *
+ * @param {Integer} itemID
+ * @return {Boolean}
+ */
+ this.isFileModified = function (itemID) {
+ var item = Zotero.Items.get(itemID);
+ if (!item.getFile()) {
+ return false;
+ }
+
+ var fileModTime = item.attachmentModificationTime;
+ if (!fileModTime) {
+ return false;
+ }
+
+ var syncModTime = Zotero.Sync.Storage.getSyncedModificationTime(itemID);
+ if (fileModTime != syncModTime) {
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Scans local files and marks any that have changed as 0 for uploading
+ * and any that are missing as 1 for downloading
+ *
+ * Also marks missing files for downloading
+ *
+ * @param {Integer[]} itemIDs An optional set of item ids to check
+ * @param {Object} itemModTimes Item mod times indexed by item ids
+ * appearing in itemIDs; if set,
+ * items with stored mod times
+ * that differ from the provided
+ * time but file mod times
+ * matching the stored time will
+ * be marked for download
+ * @return {Boolean} TRUE if any items changed state,
+ * FALSE otherwise
+ */
+ this.checkForUpdatedFiles = function (itemIDs, itemModTimes) {
+ Zotero.debug("Checking for locally changed attachment files");
+ // check for current ops?
+
+ if (itemModTimes && !itemIDs) {
+ _error("itemModTimes can only be set if itemIDs is an array "
+ + "in Zotero.Sync.Storage.checkForUpdatedFiles()");
+ }
+
+ var changed = false;
+
+ if (!itemIDs) {
+ itemIDs = [];
+ }
+
+ // Can only handle 999 bound parameters at a time
+ var numIDs = itemIDs.length;
+ var maxIDs = 999;
+ var done = 0;
+ var rows = [];
+
+ Zotero.DB.beginTransaction();
+
+ do {
+ var chunk = itemIDs.splice(0, maxIDs);
+ var sql = "SELECT itemID, linkMode, path, storageModTime FROM itemAttachments "
+ + "WHERE linkMode IN (?,?) AND syncState IN (?,?)";
+ var params = [
+ Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
+ Zotero.Attachments.LINK_MODE_IMPORTED_URL,
+ Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD,
+ Zotero.Sync.Storage.SYNC_STATE_IN_SYNC
+ ];
+ if (chunk.length) {
+ sql += " AND itemID IN (" + chunk.map(function () '?').join() + ")";
+ params = params.concat(chunk);
+ }
+ var chunkRows = Zotero.DB.query(sql, params);
+ if (chunkRows) {
+ rows = rows.concat(chunkRows);
+ }
+ done += chunk.length;
+ }
+ while (done < numIDs);
+
+ if (!rows) {
+ Zotero.debug("No to-upload or in-sync files found");
+ Zotero.DB.commitTransaction();
+ return changed;
+ }
+
+ // Index mtimes by item id
+ var itemIDs = [];
+ var mtimes = {};
+ var attachmentData = {};
+ for each(var row in rows) {
+ var id = row.itemID;
+
+ // In download-marking mode, ignore attachments whose
+ // storage mod times haven't changed
+ if (itemModTimes &&
+ row.storageModTime == itemModTimes[id]) {
+ Zotero.debug("Storage mod time (" + row.storageModTime
+ + ") hasn't changed for attachment " + id);
+ continue;
+ }
+ itemIDs.push(id);
+ mtimes[id] = row.storageModTime;
+ attachmentData[id] = {
+ linkMode: row.linkMode,
+ path: row.path
+ };
+ }
+ if (itemIDs.length == 0) {
+ Zotero.DB.commitTransaction();
+ return changed;
+ }
+
+ rows = undefined;
+
+ var updatedStates = {};
+ var items = Zotero.Items.get(itemIDs);
+ for each(var item in items) {
+ var file = item.getFile(attachmentData[item.id]);
+ if (!file) {
+ Zotero.debug("Marking attachment " + item.id + " as missing");
+ updatedStates[item.id] =
+ Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
+ continue;
+ }
+
+ var fileModTime = Math.round(file.lastModifiedTime / 1000);
+
+ //Zotero.debug("Stored mtime is " + mtimes[item.id]);
+ //Zotero.debug("File mtime is " + fileModTime);
+
+ if (itemModTimes) {
+ Zotero.debug("Item mod time is " + itemModTimes[item.id]);
+ }
+
+ if (mtimes[item.id] != fileModTime) {
+ Zotero.debug("Marking attachment " + item.id + " as changed");
+ updatedStates[item.id] =
+ Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD;
+ }
+ else if (itemModTimes) {
+ Zotero.debug("Marking attachment " + item.id + " for download");
+ updatedStates[item.id] =
+ Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
+ }
+ }
+
+ for (var itemID in updatedStates) {
+ var sql = "UPDATE itemAttachments SET syncState=? WHERE itemID=?";
+ Zotero.DB.query(
+ sql,
+ [
+ updatedStates[itemID],
+ itemID
+ ]
+ );
+ changed = true;
+ }
+
+ if (!changed) {
+ Zotero.debug("No synced files have changed locally");
+ }
+
+ //throw ('foo');
+
+ Zotero.DB.commitTransaction();
+ return changed;
+ }
+
+
+ /**
+ * Start download of all attachments marked for download
+ *
+ * @return {Boolean}
+ */
+ this.downloadFiles = function () {
+ // Check for active operations?
+ _queueReset('download');
+
+ var downloadFileIDs = _getFilesToDownload();
+ if (!downloadFileIDs) {
+ Zotero.debug("No files to download");
+ return false;
+ }
+
+ for each(var itemID in downloadFileIDs) {
+ var item = Zotero.Items.get(itemID);
+ if (this.isFileModified(itemID)) {
+ Zotero.debug("File for attachment " + itemID + " has been modified");
+ this.setSyncState(itemID, this.SYNC_STATE_TO_UPLOAD);
+ continue;
+ }
+
+ _addRequest({
+ name: _getItemURI(item).spec,
+ requestMethod: "GET",
+ QueryInterface: function (iid) {
+ if (iid.equals(Components.interfaces.nsIHttpChannel) ||
+ iid.equals(Components.interfaces.nsISupports)) {
+ return this;
+ }
+ throw Components.results.NS_NOINTERFACE;
+ }
+ });
+ _queueAdd('download', itemID);
+ }
+
+ // Start downloads
+ _queueAdvance('download', Zotero.Sync.Storage.downloadFile);
+ return true;
+ }
+
+
+ /**
+ * Begin download process for individual file
+ *
+ * @param {Integer} itemID
+ */
+ this.downloadFile = function (itemID) {
+ var item = Zotero.Items.get(itemID);
+ if (!item) {
+ var msg = "Item " + itemID
+ + " not found in Zotero.Sync.Storage.downloadFile()";
+ Zotero.debug(msg);
+ Components.utils.reportError(msg);
+ _queueAdvance('download', Zotero.Sync.Storage.downloadFile, true);
+ return;
+ }
+
+ // Retrieve modification time from server to store locally afterwards
+ Zotero.Sync.Storage.getStorageModificationTime(item, function (item, mdate) {
+ if (!mdate) {
+ Zotero.debug("Remote file not found for item " + item.id);
+ _queueAdvance('download', Zotero.Sync.Storage.downloadFile, true);
+ return;
+ }
+
+ var syncModTime = Zotero.Date.toUnixTimestamp(mdate);
+ var uri = _getItemURI(item);
+ var destFile = Zotero.getTempDirectory();
+ destFile.append(item.key + '.zip.tmp');
+ if (destFile.exists()) {
+ destFile.remove(null);
+ }
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onProgress: _updateProgress,
+ onStop: _processDownload,
+ item: item,
+ syncModTime: syncModTime
+ }
+ );
+
+ Zotero.debug('Saving 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);
+
+
+ /*
+ // Start the download
+ var incrDown = Components.classes["@mozilla.org/network/incremental-download;1"]
+ .createInstance(Components.interfaces.nsIIncrementalDownload);
+ incrDown.init(uri, destFile, -1, 2);
+ incrDown.start(listener, null);
+ */
+ });
+ }
+
+
+ /**
+ * Start upload of all attachments marked for upload
+ *
+ * If mod time on server doesn't match file, display conflict window
+ *
+ * @return {Boolean}
+ */
+ this.uploadFiles = function () {
+ // Check for active operations?
+ _queueReset('upload');
+
+ var uploadFileIDs = _getFilesToUpload();
+ if (!uploadFileIDs) {
+ Zotero.debug("No files to upload");
+ return false;
+ }
+
+ Zotero.debug(uploadFileIDs.length + " file(s) to upload");
+
+ for each(var itemID in uploadFileIDs) {
+ var item = Zotero.Items.get(itemID);
+ var size = Zotero.Attachments.getTotalFileSize(item, true);
+ _addRequest({
+ name: _getItemURI(item).spec,
+ requestMethod: "PUT",
+ QueryInterface: function (iid) {
+ if (iid.equals(Components.interfaces.nsIHttpChannel) ||
+ iid.equals(Components.interfaces.nsISupports)) {
+ return this;
+ }
+ throw Components.results.NS_NOINTERFACE;
+ }
+ }, size);
+ _queueAdd('upload', itemID);
+ }
+
+ // Start uploads
+ _queueAdvance('upload', Zotero.Sync.Storage.uploadFile);
+ return true;
+ }
+
+
+ this.uploadFile = function (itemID) {
+ _createUploadFile(itemID);
+ }
+
+
+ /**
+ * Remove files on storage server that were deleted locally more than
+ * sync.storage.deleteDelayDays days ago
+ *
+ * @param {Function} callback Passed number of files deleted
+ */
+ this.purgeDeletedStorageFiles = function (callback) {
+ Zotero.debug("Purging deleted storage files");
+ var files = _getDeletedFiles();
+ if (!files) {
+ Zotero.debug("No files to delete remotely");
+ return;
+ }
+
+ // Add .zip extension
+ var files = files.map(function (file) file + ".zip");
+
+ _deleteStorageFiles(files, function (results) {
+ // Remove nonexistent files from storage delete log
+ if (results.missing.length > 0) {
+ var done = 0;
+ var maxFiles = 999;
+ var numFiles = results.missing.length;
+
+ Zotero.DB.beginTransaction();
+
+ do {
+ var chunk = files.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
+ */
+ this.purgeOrphanedStorageFiles = function (callback) {
+ const daysBeforeSyncTime = 1;
+
+ Zotero.debug("Purging orphaned storage files");
+ var uri = Zotero.Sync.Storage.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.Utilities.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
+ // Strip XML declaration and convert to E4X
+ var xml = new XML(req.responseText.replace(/<\?xml.*\?>/, ''));
+
+ var deleteFiles = [];
+ for each(var response in xml.D::response) {
+ var href = response.D::href.toString();
+ if (href == path) {
+ continue;
+ }
+ var file = href.match(/[^\/]+$/)[0];
+ if (file.indexOf('.') == 0) {
+ Zotero.debug("Skipping hidden file " + file);
+ continue;
+ }
+ if (!file.match(/\.zip/)) {
+ Zotero.debug("Skipping non-ZIP file " + file);
+ continue;
+ }
+
+ var key = file.replace(/\.zip/, '');
+ var item = Zotero.Items.getByKey(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);
+ }
+ }
+
+ _deleteStorageFiles(deleteFiles, callback);
+ },
+ { Depth: 1 });
+ }
+
+
+ /**
+ * Create a Zotero directory on the storage server
+ */
+ this.createServerDirectory = function (callback) {
+ var uri = this.rootURI;
+ Zotero.Utilities.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);
+ 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;
+ }
+ });
+ }
+
+
+ this.resetAllSyncStates = function () {
+ var sql = "UPDATE itemAttachments SET syncState=?";
+ Zotero.DB.query(sql, [this.SYNC_STATE_TO_UPLOAD]);
+ }
+
+
+ this.clearSettingsCache = function () {
+ _rootURI = undefined;
+ }
+
+
+ //
+ // Private methods
+ //
+
+
+ /**
+ * Extract a downloaded ZIP file and update the database metadata
+ *
+ * This is called from Zotero.Sync.Server.StreamListener.onStopRequest()
+ *
+ * @param {nsIRequest} request
+ * @param {Integer} status Status code from download listener
+ * @param {String} response
+ * @return {Object} data Properties 'item', 'syncModTime'
+ */
+ function _processDownload(request, status, response, data) {
+ var item = data.item;
+ var syncModTime = data.syncModTime;
+ var zipFile = Zotero.getTempDirectory();
+ zipFile.append(item.key + '.zip.tmp');
+
+ Zotero.debug("Finished download of " + zipFile.path + " with status " + status);
+
+ var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Components.interfaces.nsIZipReader);
+ try {
+ zipReader.open(zipFile);
+ zipReader.test(null);
+
+ Zotero.debug("ZIP file is OK");
+ }
+ catch (e) {
+ Zotero.debug(zipFile.leafName + " is not a valid ZIP file", 2);
+ zipFile.remove(null);
+ _removeRequest(request);
+ _queueAdvance('download', Zotero.Sync.Storage.downloadFile, true);
+ return;
+ }
+
+ var parentDir = Zotero.Attachments.createDirectoryForItem(item.id);
+
+ // Delete existing files
+ var otherFiles = parentDir.directoryEntries;
+ while (otherFiles.hasMoreElements()) {
+ var file = otherFiles.getNext();
+ file.QueryInterface(Components.interfaces.nsIFile);
+ if (file.leafName.indexOf('.') == 0 || file.equals(zipFile)) {
+ continue;
+ }
+ Zotero.debug("Deleting existing file " + file.leafName);
+ file.remove(null);
+ }
+
+ var entries = zipReader.findEntries(null);
+ while (entries.hasMore()) {
+ var entryName = entries.getNext();
+ var b64re = /%ZB64$/;
+ if (entryName.match(b64re)) {
+ var fileName = Zotero.Utilities.Base64.decode(
+ entryName.replace(b64re, '')
+ );
+ }
+ else {
+ var fileName = entryName;
+ }
+
+ if (fileName.indexOf('/') != -1 || fileName.indexOf('.') == 0) {
+ Zotero.debug("Skipping " + fileName);
+ continue;
+ }
+
+ Zotero.debug("Extracting " + fileName);
+ var destFile = parentDir.clone();
+ destFile.QueryInterface(Components.interfaces.nsILocalFile);
+ destFile.setRelativeDescriptor(parentDir, fileName);
+ destFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
+ zipReader.extract(entryName, destFile);
+ destFile.permissions = 0644;
+ }
+ zipReader.close();
+ zipFile.remove(null);
+
+ var file = item.getFile();
+ if (!file) {
+ _error("File " + file.leafName + " not found for item "
+ + itemID + " after extracting ZIP");
+ }
+ file.lastModifiedTime = syncModTime * 1000;
+
+ 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();
+
+ _removeRequest(request);
+ _queueAdvance('download', Zotero.Sync.Storage.downloadFile, true);
+ }
+
+
+ /**
+ * Create zip file of attachment directory
+ *
+ * @param {Integer} itemID
+ * @return {Boolean} TRUE if zip process started,
+ * FALSE if storage was empty
+ */
+ function _createUploadFile(itemID) {
+ Zotero.debug('Creating zip file for item ' + itemID);
+ var item = Zotero.Items.get(itemID);
+
+ switch (item.attachmentLinkMode) {
+ case Zotero.Attachments.LINK_MODE_LINKED_FILE:
+ case Zotero.Attachments.LINK_MODE_LINKED_URL:
+ _error("Upload file must be an imported snapshot or file in "
+ + "Zotero.Sync.Storage.createUploadFile()");
+ }
+
+ var dir = Zotero.Attachments.getStorageDirectory(itemID);
+
+ var tmpFile = Zotero.getTempDirectory();
+ tmpFile.append(item.key + '.zip');
+
+ var zw = Components.classes["@mozilla.org/zipwriter;1"]
+ .createInstance(Components.interfaces.nsIZipWriter);
+ zw.open(tmpFile, 0x04 | 0x08 | 0x20); // open rw, create, truncate
+ var fileList = [];
+ dir = dir.directoryEntries;
+ while (dir.hasMoreElements()) {
+ var file = dir.getNext();
+ file.QueryInterface(Components.interfaces.nsILocalFile);
+ var fileName = file.getRelativeDescriptor(file.parent);
+
+ if (fileName.indexOf('.') == 0) {
+ Zotero.debug('Skipping file ' + fileName);
+ continue;
+ }
+
+ //Zotero.debug("Adding file " + fileName);
+
+ fileName = Zotero.Utilities.Base64.encode(fileName) + "%ZB64";
+ zw.addEntryFile(
+ fileName,
+ Components.interfaces.nsIZipWriter.COMPRESSION_DEFAULT,
+ file,
+ true
+ );
+ fileList.push(fileName);
+ }
+
+ if (fileList.length == 0) {
+ Zotero.debug('No files to add -- removing zip file');
+ tmpFile.remove(null);
+ _queueAdvance('upload', Zotero.Sync.Storage.uploadFile, true);
+ return false;
+ }
+
+ Zotero.debug('Creating ' + tmpFile.leafName + ' with ' + fileList.length + ' file(s)');
+
+ var observer = new Zotero.Sync.Storage.ZipWriterObserver(
+ zw, _processUploadFile, { itemID: itemID, files: fileList }
+ );
+ zw.processQueue(observer, null);
+ return true;
+ }
+
+
+ /**
+ * Upload the generated ZIP file to the server
+ *
+ * @param {Object} Object with 'itemID' property
+ * @return {void}
+ */
+ function _processUploadFile(data) {
+ _updateSizeMultiplier(
+ (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100
+ );
+
+ var item = Zotero.Items.get(data.itemID);
+
+ Zotero.Sync.Storage.getStorageModificationTime(item, function (item, mdate) {
+ // Check for conflict
+ if (mdate) {
+ var file = item.getFile();
+ if (Zotero.Date.toUnixTimestamp(mdate)
+ != Zotero.Sync.Storage.getSyncedModificationTime(item.id)) {
+ _error("Conflict! Last known mod time does not match remote time!")
+ }
+ }
+ 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;
+ if (_cachedCredentials.authHeader) {
+ channel.setRequestHeader(
+ 'Authorization', _cachedCredentials.authHeader, false
+ );
+ }
+ channel.setRequestHeader('Keep-Alive', '', false);
+ channel.setRequestHeader('Connection', '', false);
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onProgress: _updateProgress,
+ onStop: _onUploadComplete,
+ item: item
+ }
+ );
+ channel.notificationCallbacks = listener;
+ channel.asyncOpen(listener, null);
+ });
+ }
+
+
+ function _onUploadComplete(request, status, response, data) {
+ var item = data.item;
+ var url = request.name;
+
+ Zotero.debug("Upload of attachment " + item.id
+ + " finished with status code " + status);
+
+ switch (status) {
+ case 201:
+ case 204:
+ break;
+
+ default:
+ _error("File upload status was " + status
+ + " in Zotero.Sync.Storage._onUploadComplete()");
+ }
+
+ Zotero.Sync.Storage.setStorageModificationTime(item, function (item, mtime) {
+ Zotero.DB.beginTransaction();
+
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, mtime, true);
+
+ Zotero.DB.commitTransaction();
+
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+ file.remove(null);
+
+ _removeRequest(request);
+ _queueAdvance('upload', Zotero.Sync.Storage.uploadFile, true);
+ });
+ }
+
+
+ /**
+ * Get files marked as ready to upload
+ *
+ * @inner
+ * @return {Number[]} Array of attachment itemIDs
+ */
+ function _getFilesToDownload() {
+ var sql = "SELECT itemID FROM itemAttachments WHERE syncState=?";
+ return Zotero.DB.columnQuery(sql, Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD);
+ }
+
+
+ /**
+ * Get files marked as ready to upload
+ *
+ * @inner
+ * @return {Number[]} Array of attachment itemIDs
+ */
+ function _getFilesToUpload() {
+ var sql = "SELECT itemID FROM itemAttachments WHERE syncState=? "
+ + "AND linkMode IN (?,?)";
+ return Zotero.DB.columnQuery(sql,
+ [
+ Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD,
+ Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
+ Zotero.Attachments.LINK_MODE_IMPORTED_URL
+ ]
+ );
+ }
+
+
+ /**
+ * @inner
+ * @param {Integer} [days=pref:e.z.sync.storage.deleteDelayDays]
+ * Number of days old files have to be
+ * @return {String[]|FALSE} Array of keys, or FALSE if none
+ */
+ function _getDeletedFiles(days) {
+ if (!days) {
+ days = Zotero.Prefs.get("sync.storage.deleteDelayDays");
+ }
+
+ var ts = Zotero.Date.getUnixTimestamp();
+ ts = ts - (86400 * days);
+
+ var sql = "SELECT key FROM storageDeleteLog WHERE timestamp";
+ return Zotero.DB.columnQuery(sql, ts);
+ }
+
+
+ /**
+ * @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');
+ requestXML.D::prop = '';
+
+ var xmlstr = prolog + requestXML.toXMLString();
+
+ // Test whether URL is WebDAV-enabled
+ var request = Zotero.Utilities.HTTP.doOptions(uri, function (req) {
+ Zotero.debug(req.status);
+
+ // Timeout
+ if (req.status == 0) {
+ 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;
+ }
+
+ var headers = { Depth: 0 };
+
+ var authHeader = Zotero.Utilities.HTTP.getChannelAuthorization(req.channel);
+ if (authHeader) {
+ _cachedCredentials.authHeader = authHeader;
+ headers.Authorization = authHeader;
+ // Create a version without Depth
+ var authHeaders = { Authorization: authHeader };
+ var authRequired = true;
+ }
+ else {
+ var authRequired = false;
+ }
+
+ // Test whether Zotero directory exists
+ Zotero.Utilities.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.Utilities.HTTP.WebDAV.doPut(testFileURI, "", function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 201:
+ // Delete test file
+ Zotero.Utilities.HTTP.WebDAV.doDelete(
+ testFileURI,
+ function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 204:
+ callback(
+ uri,
+ Zotero.Sync.Storage.SUCCESS,
+ !authRequired
+ );
+ 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;
+
+ 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:
+ var parentURI = uri.clone();
+ parentURI.spec = parentURI.spec.replace(/zotero\/$/, '');
+
+ // Zotero directory wasn't found, so if at least
+ // the parent directory exists
+ Zotero.Utilities.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 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;
+ }
+ }, headers);
+ 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;
+ }
+
+
+ /**
+ * 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.rootURI;
+ uri.spec = uri.spec + item.key + '.zip';
+ return uri;
+ }
+
+
+ /**
+ * @inner
+ * @param {XMLHTTPRequest} req
+ * @throws
+ */
+ function _checkResponse(req) {
+ if (!req.responseXML ||
+ !req.responseXML.firstChild ||
+ !(req.responseXML.firstChild.namespaceURI == 'DAV:' &&
+ req.responseXML.firstChild.localName == 'multistatus')) {
+ Zotero.debug(req.responseText);
+ _error('Invalid response from server');
+ }
+
+ if (!req.responseXML.childNodes[0].firstChild) {
+ _error('Empty response from server');
+ }
+ }
+
+
+ //
+ // Queuing functions
+ //
+ function _queueAdd(queueName, id) {
+ Zotero.debug("Queuing " + queueName + " object " + id);
+ var q = _queues[queueName];
+ if (q.queue.indexOf(id) != -1) {
+ return;
+ }
+ q.queue.push(id);
+ }
+
+
+ function _queueAdvance(queueName, callback, decrement) {
+ var q = _queues[queueName];
+
+ if (decrement) {
+ q.current--;
+ }
+
+ if (q.queue.length == 0) {
+ Zotero.debug("No objects in " + queueName + " queue ("
+ + q.current + " current)");
+ return;
+ }
+
+ if (q.current >= _queueSimultaneous[queueName]) {
+ Zotero.debug(queueName + " queue is busy (" + q.current + ")");
+ return;
+ }
+
+ Zotero.debug("Processing next object in " + queueName + " queue");
+
+ var id = q.queue.shift();
+ q.current++;
+
+ callback(id);
+
+ // Wait a second, and then, if still under limit and there are more
+ // requests, process another
+ setTimeout(function () {
+ if (q.queue.length > 0 && q.current < _queueSimultaneous[queueName]) {
+ _queueAdvance(queueName, callback);
+ }
+ }, 1000);
+ }
+
+
+ function _queueReset(queueName) {
+ Zotero.debug("Resetting " + queueName + " queue");
+ var q = _queues[queueName];
+ q.queue = [];
+ q.current = 0;
+ }
+
+
+ //
+ // Progress management
+ //
+ /**
+ * @param {nsIRequest}
+ * @param {Integer} [size] Total size in bytes, which might be
+ * scaled by a compression multiplier
+ */
+ function _addRequest(request, size) {
+ var info = _getRequestInfo(request);
+ var queue = info.queue;
+ var name = info.name;
+
+ if (_requests[queue][name]) {
+ queue = queue.substr(0, 1).toUpperCase() + queue.substr(1);
+ _error(queue + " request already exists in Zotero.Sync.Storage._addRequest()");
+ }
+ _requests[queue][name] = {
+ state: 0, // 0: queued, 1: active, 2: done
+ progress: 0,
+ progressMax: 0,
+ size: size ? size : null
+ };
+ // Add estimated size
+ if (size) {
+ _totalProgressMax[queue] += Math.round(size * _requestSizeMultiplier);
+ }
+ _numRequests[queue].queued++;
+ }
+
+
+ /**
+ * 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();
+ }
+
+
+ /**
+ * Update counters for given request
+ *
+ * Also updates progress meter
+ *
+ * @param {nsIRequest} request
+ * @param {Integer} progress Bytes transferred so far
+ * @param {Integer} progressMax Total bytes in this request
+ */
+ function _updateProgress(request, progress, progressMax) {
+ //Zotero.debug("Updating progress");
+
+ var info = _getRequestInfo(request);
+ var queue = info.queue;
+ var name = info.name;
+
+ var r = _requests[queue][name];
+
+ switch (r.state) {
+ // Queued
+ case 0:
+ r.state = 1;
+ _numRequests[queue].queued--;
+ _numRequests[queue].active++;
+ // Remove estimated size
+ if (r.size) {
+ _totalProgressMax[queue] -=
+ Math.round(r.size * _requestSizeMultiplier);
+ }
+ break;
+
+ // Done
+ case 2:
+ _error("Trying to update a finished request in "
+ + "_Zotero.Sync.Storage._updateProgress()");
+ }
+
+ _totalProgress[queue] += progress - r.progress;
+ r.progress = progress;
+
+ _totalProgressMax[queue] += progressMax - r.progressMax;
+ r.progressMax = progressMax;
+
+ _updateProgressMeter();
+ }
+
+
+ /*
+ * Mark request as done, and, if last request, clear all requests
+ *
+ * Also updates progress meter
+ */
+ function _removeRequest(request) {
+ var info = _getRequestInfo(request);
+ var queue = info.queue;
+ var name = info.name;
+
+ var r = _requests[queue][name];
+
+ //Zotero.debug("Removing " + queue + " request " + name);
+ if (!r) {
+ _error("Existing " + queue + " request not found in "
+ + "Zotero.Sync.Storage._removeRequest()");
+ }
+
+ switch (r.state) {
+ // Active
+ case 1:
+ _numRequests[queue].active--;
+ _numRequests[queue].done++;
+ //_totalProgress[queue] -= r.progressMax;
+ //_totalProgressMax[queue] -= r.progressMax;
+ break;
+
+ // Queued
+ case 0:
+ _numRequests[queue].queued--;
+ _numRequests[queue].done++;
+ // Remove estimated size
+ //_totalProgressMax[queue] -= Math.round(r.size * _requestSizeMultiplier);
+ break;
+
+ // Done
+ case 2:
+ _error("Trying to remove a finished request in "
+ + "_Zotero.Sync.Storage._removeRequest()");
+ }
+
+ //r = undefined;
+ //delete _requests[queue][name];
+ r.state = 2; // Done
+
+ var done = _resetRequestsIfDone();
+ if (!done) {
+ _updateProgressMeter();
+ }
+ }
+
+
+ /**
+ * Check if all requests are done, and if so reset everything
+ *
+ * Also updates progress meter
+ */
+ function _resetRequestsIfDone() {
+ for (var queue in _requests) {
+ if (_numRequests[queue].active != 0 || _numRequests[queue].queued != 0) {
+ return false;
+ }
+ }
+ Zotero.debug("Resetting all requests");
+ for (var queue in _requests) {
+ _requests[queue] = {};
+ _numRequests[queue].done = 0;
+ _totalProgress[queue] = 0;
+ _totalProgressMax[queue] = 0;
+ _requestSizeMultiplier = 1;
+ }
+ _updateProgressMeter();
+
+ // TODO: Find a better place for this?
+ _syncInProgress = false;
+ Zotero.Sync.Runner.next();
+ return true;
+ }
+
+
+ function _updateProgressMeter() {
+ var totalRequests = 0;
+ for (var queue in _requests) {
+ totalRequests += _numRequests[queue].active;
+ totalRequests += _numRequests[queue].queued;
+ }
+
+ if (totalRequests > 0) {
+ var percentage = Math.round(
+ (
+ (_totalProgress.download + _totalProgress.upload) /
+ (_totalProgressMax.download + _totalProgressMax.upload)
+ ) * 100
+ );
+ //Zotero.debug("Percentage is " + percentage);
+
+ if (_totalProgressMax.download) {
+ var remaining = Math.round(
+ (_totalProgressMax.download - _totalProgress.download) / 1024
+ );
+ var downloadStatus =
+ Zotero.getString('sync.storage.kbRemaining', remaining);
+ }
+ else {
+ var downloadStatus = Zotero.getString('sync.storage.none');
+ }
+
+ if (_totalProgressMax.upload) {
+ remaining = Math.round(
+ (_totalProgressMax.upload - _totalProgress.upload) / 1024
+ );
+ var uploadStatus =
+ Zotero.getString('sync.storage.kbRemaining', remaining);
+ }
+ else {
+ var uploadStatus = Zotero.getString('sync.storage.none');
+ }
+ }
+
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var enumerator = wm.getEnumerator("navigator:browser");
+ while (enumerator.hasMoreElements()) {
+ var win = enumerator.getNext();
+ var doc = win.document;
+
+ //
+ // TODO: Move to overlay.js?
+ //
+ var meter = doc.getElementById("zotero-tb-syncProgress");
+
+ if (totalRequests == 0) {
+ meter.hidden = true;
+ continue;
+ }
+
+ meter.setAttribute("value", percentage);
+ meter.hidden = false;
+
+ var tooltip = doc.
+ getElementById("zotero-tb-syncProgress-tooltip-progress");
+ tooltip.setAttribute("value", percentage + "%");
+
+ var tooltip = doc.
+ getElementById("zotero-tb-syncProgress-tooltip-downloads");
+ tooltip.setAttribute("value", downloadStatus);
+
+ var tooltip = doc.
+ getElementById("zotero-tb-syncProgress-tooltip-uploads");
+ tooltip.setAttribute("value", uploadStatus);
+ }
+ }
+
+
+ function _getRequestInfo(request) {
+ request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ switch (request.requestMethod) {
+ case 'GET':
+ var queue = 'download';
+ break;
+
+ case 'POST':
+ case 'PUT':
+ var queue = 'upload';
+ break;
+
+ default:
+ _error("Unsupported method '" + request.requestMethod
+ + "' in Zotero.Sync.Storage._updateProgress()")
+ }
+
+ return {
+ queue: queue,
+ name: request.name
+ };
+ }
+
+
+
+ //
+ //
+ //
+ function _error(e) {
+ _syncInProgress = false;
+ Zotero.DB.rollbackAllTransactions();
+
+ Zotero.Sync.Server.setSyncIcon('error');
+
+ if (e.name) {
+ Zotero.Sync.Server.lastSyncError = e.name;
+ }
+ else {
+ Zotero.Sync.Server.lastSyncError = e;
+ }
+ Zotero.debug(e, 1);
+ Zotero.Sync.Runner.reset();
+ throw(e);
+ }
+}
+
+
+Zotero.Sync.Storage.ZipWriterObserver = function (zipWriter, callback, data) {
+ this._zipWriter = zipWriter;
+ this._callback = callback;
+ this._data = data;
+}
+
+Zotero.Sync.Storage.ZipWriterObserver.prototype = {
+ onStartRequest: function () {},
+
+ onStopRequest: function(req, context, status) {
+ var zipFileName = this._zipWriter.file.leafName;
+
+ var originalSize = 0;
+ for each(var fileName in this._data.files) {
+ var entry = this._zipWriter.getEntry(fileName);
+ originalSize += entry.realSize;
+ }
+ delete this._data.files;
+
+ this._zipWriter.close();
+
+ Zotero.debug("Zip of " + zipFileName + " finished with status " + status
+ + " (original " + Math.round(originalSize / 1024) + "KB, "
+ + "compressed " + Math.round(this._zipWriter.file.fileSize / 1024) + "KB, "
+ + Math.round(
+ ((originalSize - this._zipWriter.file.fileSize) / originalSize) * 100
+ ) + "% reduction)");
+
+ Zotero.Sync.Storage.compressionTracker.compressed += this._zipWriter.file.fileSize;
+ Zotero.Sync.Storage.compressionTracker.uncompressed += originalSize;
+ Zotero.debug("Ratio so far: " + Zotero.Sync.Storage.compressionTracker.ratio);
+
+ var item = Zotero.Items.get(this._data.itemID);
+ Zotero.debug("Original sum " + Zotero.Attachments.getTotalFileSize(item));
+ this._callback(this._data);
+ }
+}
+
+
+/**
+ * Possible properties of data object:
+ * - onStart f(request)
+ * - onProgress f(name, progess, progressMax)
+ * - onStop f(request, status, response, data)
+ * - 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) {
+ //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');
+
+ if (status != 0) {
+ throw ("Request status is " + status
+ + " in Zotero.Sync.Storage.StreamListener.onStopRequest()");
+ }
+
+ this._onDone(request, status);
+ },
+
+ // nsIWebProgressListener
+ onProgressChange: function (wp, request, curSelfProgress,
+ maxSelfProgress, curTotalProgress, maxTotalProgress) {
+ //Zotero.debug("onProgressChange with " + curTotalProgress + "/" + maxTotalProgress);
+
+ // onProgress gets called too, so this isn't necessary
+ //this._onProgress(request, curTotalProgress, maxTotalProgress);
+ },
+
+ onStateChange: function (wp, request, stateFlags, status) {
+ Zotero.debug("onStateChange");
+
+ if ((stateFlags & Components.interfaces.nsIWebProgressListener.STATE_START)
+ && (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_NETWORK)) {
+ this._onStart(request);
+ }
+ else if ((stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
+ && (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_NETWORK)) {
+ this._onDone(request, status);
+ }
+ },
+
+ onStatusChange: function (progress, request, status, message) {
+ Zotero.debug("onStatusChange with '" + message + "'");
+ },
+ onLocationChange: function () {},
+ onSecurityChange: function () {},
+
+ // nsIStreamListener
+ onDataAvailable: function (request, context, stream, sourceOffset, length) {
+ Zotero.debug('onDataAvailable');
+ var scriptableInputStream =
+ Components.classes["@mozilla.org/scriptableinputstream;1"]
+ .createInstance(Components.interfaces.nsIScriptableInputStream);
+ scriptableInputStream.init(stream);
+
+ this._response += scriptableInputStream.read(length);
+ },
+
+ // nsIChannelEventSink
+ onChannelRedirect: function (oldChannel, newChannel, flags) {
+ Zotero.debug('onRedirect');
+
+ // if redirecting, store the new channel
+ this._channel = newChannel;
+ },
+
+ // nsIHttpEventSink
+ onRedirect: function (oldChannel, newChannel) {
+ Zotero.debug('onRedirect');
+ },
+
+
+ //
+ // Private methods
+ //
+ _onStart: function (request) {
+ //Zotero.debug('Starting request');
+ if (this._data && this._data.onStart) {
+ this._data.onStart(request);
+ }
+ },
+
+ _onProgress: function (request, progress, progressMax) {
+ if (this._data && this._data.onProgress) {
+ this._data.onProgress(request, progress, progressMax);
+ }
+ },
+
+ _onDone: function (request, status) {
+ if (request instanceof Components.interfaces.nsIHttpChannel) {
+ request.QueryInterface(Components.interfaces.nsIHttpChannel);
+ status = request.responseStatus;
+ request.QueryInterface(Components.interfaces.nsIRequest);
+ }
+
+ if (this._data.onStop) {
+ // Remove callbacks before passing along
+ var passData = {};
+ for (var i in this._data) {
+ switch (i) {
+ case "onStart":
+ case "onProgress":
+ case "onStop":
+ continue;
+ }
+ passData[i] = this._data[i];
+ }
+ this._data.onStop(request, status, this._response, passData);
+ }
+
+ this._channel = null;
+ },
+
+
+ // 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/sync.js b/chrome/content/zotero/xpcom/sync.js
index 6c562e3bd0..98eeaebd39 100644
--- a/chrome/content/zotero/xpcom/sync.js
+++ b/chrome/content/zotero/xpcom/sync.js
@@ -241,7 +241,7 @@ Zotero.Sync = new function() {
/**
- * Notifier observer to add deleted objects to syncDeleteLog
+ * Notifier observer to add deleted objects to syncDeleteLog/storageDeleteLog
* plus related methods
*/
Zotero.Sync.EventListener = new function () {
@@ -313,17 +313,26 @@ Zotero.Sync.EventListener = new function () {
return;
}
+ var isItem = Zotero.Sync.getObjectTypeName(objectTypeID) == 'item';
+
var ZU = new Zotero.Utilities;
Zotero.DB.beginTransaction();
if (event == 'delete') {
var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?, ?)";
- var statement = Zotero.DB.getStatement(sql);
+ var syncStatement = Zotero.DB.getStatement(sql);
+
+ if (isItem && Zotero.Sync.Storage.active) {
+ var storageEnabled = true;
+ var sql = "INSERT INTO storageDeleteLog VALUES (?, ?)";
+ var storageStatement = Zotero.DB.getStatement(sql);
+ }
+ var storageBound = false;
var ts = Zotero.Date.getUnixTimestamp();
- for(var i=0, len=ids.length; i expiry) {
+ Components.utils.reportError("Build has expired -- syncing disabled");
return false;
}
@@ -469,6 +645,7 @@ Zotero.Sync.Server = new function () {
}
});
+ this.__defineGetter__("syncInProgress", function () _syncInProgress);
this.__defineGetter__("sessionIDComponent", function () {
return 'sessionid=' + _sessionID;
});
@@ -484,12 +661,6 @@ Zotero.Sync.Server = new function () {
this.__defineSetter__("lastLocalSyncTime", function (val) {
Zotero.DB.query("REPLACE INTO version VALUES ('lastlocalsync', ?)", { int: val });
});
- this.__defineGetter__("lastSyncError", function () {
- return _lastSyncError;
- });
- this.__defineSetter__("lastSyncError", function (val) {
- _lastSyncError = val ? val : '';
- });
this.nextLocalSyncDate = false;
this.apiVersion = 2;
@@ -508,13 +679,6 @@ Zotero.Sync.Server = new function () {
var _syncInProgress;
var _sessionID;
var _sessionLock;
- var _lastSyncError;
- var _autoSyncTimer;
-
-
- function init() {
- this.EventListener.init();
- }
function login(callback) {
@@ -572,8 +736,7 @@ Zotero.Sync.Server = new function () {
function sync() {
- Zotero.Sync.Server.clearSyncTimeout();
- Zotero.Sync.Server.setSyncIcon('animate');
+ Zotero.Sync.Runner.setSyncIcon('animate');
if (_attempts < 0) {
_error('Too many attempts in Zotero.Sync.Server.sync()');
@@ -594,6 +757,7 @@ Zotero.Sync.Server = new function () {
_error("Sync operation already in progress");
}
+ Zotero.debug("Beginning server sync");
_syncInProgress = true;
// Get updated data
@@ -682,8 +846,10 @@ Zotero.Sync.Server = new function () {
Zotero.Sync.Server.lastLocalSyncTime = nextLocalSyncTime;
Zotero.Sync.Server.nextLocalSyncDate = false;
Zotero.DB.commitTransaction();
- Zotero.Sync.Server.unlock();
- _syncInProgress = false;
+ Zotero.Sync.Server.unlock(function () {
+ _syncInProgress = false;
+ Zotero.Sync.Runner.next();
+ });
return;
}
@@ -722,8 +888,10 @@ Zotero.Sync.Server = new function () {
//throw('break2');
Zotero.DB.commitTransaction();
- Zotero.Sync.Server.unlock();
- _syncInProgress = false;
+ Zotero.Sync.Server.unlock(function () {
+ _syncInProgress = false;
+ Zotero.Sync.Runner.next();
+ });
}
var compress = Zotero.Prefs.get('sync.server.compressData');
@@ -894,12 +1062,6 @@ Zotero.Sync.Server = new function () {
if (callback) {
callback();
}
-
- // Reset sync icon and last error
- if (syncInProgress) {
- Zotero.Sync.Server.lastSyncError = '';
- Zotero.Sync.Server.setSyncIcon();
- }
});
}
@@ -1001,6 +1163,7 @@ Zotero.Sync.Server = new function () {
Zotero.DB.query(sql);
Zotero.DB.query("DELETE FROM syncDeleteLog");
+ Zotero.DB.query("DELETE FROM storageDeleteLog");
sql = "INSERT INTO version VALUES ('syncdeletelog', ?)";
Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp());
@@ -1037,61 +1200,6 @@ Zotero.Sync.Server = new function () {
}
- function setSyncTimeout() {
- // check if server/auto-sync are enabled
-
- var autoSyncTimeout = 15;
- Zotero.debug('Setting auto-sync timeout to ' + autoSyncTimeout + ' seconds');
-
- if (_autoSyncTimer) {
- _autoSyncTimer.cancel();
- }
- else {
- _autoSyncTimer = Components.classes["@mozilla.org/timer;1"].
- createInstance(Components.interfaces.nsITimer);
- }
-
- // {} implements nsITimerCallback
- _autoSyncTimer.initWithCallback({ notify: function (event, type, ids) {
- if (event == 'refresh') {
- return;
- }
- if (_syncInProgress) {
- Zotero.debug('Sync already in progress -- skipping auto-sync');
- return;
- }
- Zotero.Sync.Server.sync();
- }}, autoSyncTimeout * 1000, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
- }
-
-
- function clearSyncTimeout() {
- if (_autoSyncTimer) {
- _autoSyncTimer.cancel();
- }
- }
-
-
- function setSyncIcon(status) {
- status = status ? status : '';
-
- switch (status) {
- case '':
- case 'animate':
- case 'error':
- break;
-
- default:
- throw ("Invalid sync icon status '" + status + "' in Zotero.Sync.Server.setSyncIcon()");
- }
-
- var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
- .getService(Components.interfaces.nsIWindowMediator);
- var win = wm.getMostRecentWindow('navigator:browser');
- win.document.getElementById('zotero-tb-sync').setAttribute('status', status);
- }
-
-
function _checkResponse(xmlhttp) {
if (!xmlhttp.responseXML ||
!xmlhttp.responseXML.childNodes[0] ||
@@ -1130,14 +1238,15 @@ Zotero.Sync.Server = new function () {
Zotero.Sync.Server.unlock()
}
- Zotero.Sync.Server.setSyncIcon('error');
-
+ Zotero.Sync.Runner.setSyncIcon('error');
if (e.name) {
- Zotero.Sync.Server.lastSyncError = e.name;
+ Zotero.Sync.Runner.lastSyncError = e.name;
}
else {
- Zotero.Sync.Server.lastSyncError = e;
+ Zotero.Sync.Runner.lastSyncError = e;
}
+ Zotero.debug(e, 1);
+ Zotero.Sync.Runner.reset();
throw(e);
}
}
@@ -1182,26 +1291,6 @@ Zotero.BufferedInputListener.prototype = {
}
-// TODO: use prototype
-Zotero.Sync.Server.EventListener = {
- init: function () {
- Zotero.Notifier.registerObserver(this);
- },
-
- notify: function (event, type, ids, extraData) {
- // TODO: skip others
- if (type == 'refresh') {
- return;
- }
-
- if (Zotero.Prefs.get('sync.server.autoSync') && Zotero.Sync.Server.enabled) {
- Zotero.Sync.Server.setSyncTimeout();
- }
- }
-}
-
-
-
Zotero.Sync.Server.Data = new function() {
this.processUpdatedXML = processUpdatedXML;
this.buildUploadXML = buildUploadXML;
@@ -1299,6 +1388,7 @@ Zotero.Sync.Server.Data = new function() {
var remoteCreatorStore = {};
var relatedItemsStore = {};
+ var itemStorageModTimes = {};
Zotero.DB.beginTransaction();
@@ -1527,10 +1617,10 @@ Zotero.Sync.Server.Data = new function() {
// Create or overwrite locally
obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj);
- // If a local tag matches the name of a different remote tag,
- // delete the local tag and add items linked to it to the
- // matching remote tag
if (isNewObject && type == 'tag') {
+ // If a local tag matches the name of a different remote tag,
+ // delete the local tag and add items linked to it to the
+ // matching remote tag
var tagName = xmlNode.@name.toString();
var tagType = xmlNode.@type.toString()
? parseInt(xmlNode.@type) : 0;
@@ -1562,6 +1652,25 @@ Zotero.Sync.Server.Data = new function() {
// Don't use assigned-but-unsaved ids for new ids
Zotero.ID.skip(types, obj.id);
+
+ if (type == 'item' && obj.isAttachment() &&
+ (obj.attachmentLinkMode ==
+ Zotero.Attachments.LINK_MODE_IMPORTED_FILE ||
+ obj.attachmentLinkMode ==
+ Zotero.Attachments.LINK_MODE_IMPORTED_URL)) {
+ // Mark new attachments for download
+ if (isNewObject) {
+ obj.attachmentSyncState =
+ Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
+ }
+ // Set existing attachments mtime update check
+ else {
+ var mtime = xmlNode.@storageModTime.toString();
+ if (mtime) {
+ itemStorageModTimes[obj.id] = parseInt(mtime);
+ }
+ }
+ }
}
@@ -1738,6 +1847,19 @@ Zotero.Sync.Server.Data = new function() {
Zotero[Types].erase(toDeleteParents);
Zotero.Sync.EventListener.unignoreDeletions(type, toDeleteParents);
}
+
+
+ // Check mod times of updated items against stored time to see
+ // if they've been updated elsewhere and mark for download if so
+ if (type == 'item') {
+ var ids = [];
+ for (var id in itemStorageModTimes) {
+ ids.push(id);
+ }
+ if (ids.length > 0) {
+ Zotero.Sync.Storage.checkForUpdatedFiles(ids, itemStorageModTimes);
+ }
+ }
}
var xmlstr = Zotero.Sync.Server.Data.buildUploadXML(uploadIDs);
@@ -1888,10 +2010,18 @@ Zotero.Sync.Server.Data = new function() {
if (item.primary.itemType == 'attachment') {
xml.@linkMode = item.attachment.linkMode;
xml.@mimeType = item.attachment.mimeType;
- xml.@charset = item.attachment.charset;
+ var charset = item.attachment.charset;
+ if (charset) {
+ xml.@charset = charset;
+ }
- // Don't include paths for links
+ // Include storage sync time and paths for non-links
if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
+ var mtime = Zotero.Sync.Storage.getSyncedModificationTime(item.primary.itemID);
+ if (mtime) {
+ xml.@storageModTime = mtime;
+ }
+
var path = {item.attachment.path};
xml.path += path;
}
@@ -2018,8 +2148,8 @@ Zotero.Sync.Server.Data = new function() {
// Attachment metadata
if (item.isAttachment()) {
item.attachmentLinkMode = parseInt(xmlItem.@linkMode);
- item.attachmentMIMEType = xmlItem.@mimeType;
- item.attachmentCharset = parseInt(xmlItem.@charsetID);
+ item.attachmentMIMEType = xmlItem.@mimeType.toString();
+ item.attachmentCharset = xmlItem.@charset.toString();
if (item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
item.attachmentPath = xmlItem.path.toString();
}
diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js
index 3dc486ea8f..503add2df2 100644
--- a/chrome/content/zotero/xpcom/utilities.js
+++ b/chrome/content/zotero/xpcom/utilities.js
@@ -296,6 +296,31 @@ Zotero.Utilities.prototype.isInt = function(x) {
}
+/**
+ * Generate a random integer between min and max inclusive
+ *
+ * @param {Integer} min
+ * @param {Integer} max
+ * @return {Integer}
+ */
+Zotero.Utilities.prototype.rand = function (min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+
+/**
+ * Return true according to a given probability
+ *
+ * @param {Integer} x Will return true every x times on average
+ * @return {Boolean} On average, TRUE every x times
+ * the function is called
+ */
+Zotero.Utilities.prototype.probability = function (x) {
+ return this.rand(1, x) == this.rand(1, x);
+}
+
+
+
/**
* Determine the necessary data type for SQLite parameter binding
*
@@ -643,9 +668,9 @@ Zotero.Utilities.HTTP = new function() {
this.doGet = doGet;
this.doPost = doPost;
this.doHead = doHead;
- this.doOptions = doOptions;
this.browserIsOffline = browserIsOffline;
+ this.WebDAV = {};
/**
* Send an HTTP GET request via XMLHTTPRequest
@@ -665,8 +690,9 @@ Zotero.Utilities.HTTP = new function() {
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
-
- var test = xmlhttp.open('GET', url, true);
+ // Prevent certificate/authentication dialogs from popping up
+ xmlhttp.mozBackgroundRequest = true;
+ xmlhttp.open('GET', url, true);
xmlhttp.onreadystatechange = function(){
_stateChange(xmlhttp, onDone, responseCharset);
@@ -716,7 +742,8 @@ Zotero.Utilities.HTTP = new function() {
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
-
+ // Prevent certificate/authentication dialogs from popping up
+ xmlhttp.mozBackgroundRequest = true;
xmlhttp.open('POST', url, true);
xmlhttp.setRequestHeader("Content-Type", (requestContentType ? requestContentType : "application/x-www-form-urlencoded" ));
@@ -750,8 +777,9 @@ Zotero.Utilities.HTTP = new function() {
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
-
- var test = xmlhttp.open('HEAD', url, true);
+ // Prevent certificate/authentication dialogs from popping up
+ xmlhttp.mozBackgroundRequest = true;
+ xmlhttp.open('HEAD', url, true);
xmlhttp.onreadystatechange = function(){
_stateChange(xmlhttp, onDone);
@@ -776,26 +804,30 @@ Zotero.Utilities.HTTP = new function() {
/**
- * Send an HTTP OPTIONS request via XMLHTTPRequest
- *
- * doOptions can be called as:
- * Zotero.Utilities.HTTP.doOptions(url, body, onDone)
- *
- * Returns the XMLHTTPRequest object
- **/
- function doOptions(url, body, onDone) {
- Zotero.debug("HTTP OPTIONS "+url);
- if (this.browserIsOffline()){
+ * Send an HTTP OPTIONS request via XMLHTTPRequest
+ *
+ * @param {nsIURI} url
+ * @param {Function} onDone
+ * @return {XMLHTTPRequest}
+ */
+ this.doOptions = function (uri, callback) {
+ // Don't display password in console
+ var disp = uri.clone();
+ disp.password = "********";
+ Zotero.debug("HTTP OPTIONS to " + disp.spec);
+
+ if (Zotero.Utilities.HTTP.browserIsOffline()){
return false;
}
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
+ // Prevent certificate/authentication dialogs from popping up
+ xmlhttp.mozBackgroundRequest = true;
+ xmlhttp.open('OPTIONS', uri.spec, true);
- xmlhttp.open('OPTIONS', url, true);
-
- xmlhttp.onreadystatechange = function(){
- _stateChange(xmlhttp, onDone);
+ xmlhttp.onreadystatechange = function() {
+ _stateChange(xmlhttp, callback);
};
// Temporarily set cookieBehavior to 0 for Firefox 3
@@ -806,7 +838,7 @@ Zotero.Utilities.HTTP = new function() {
var cookieBehavior = prefService.getIntPref("network.cookie.cookieBehavior");
prefService.setIntPref("network.cookie.cookieBehavior", 0);
- xmlhttp.send(body);
+ xmlhttp.send(null);
}
finally {
prefService.setIntPref("network.cookie.cookieBehavior", cookieBehavior);
@@ -816,36 +848,231 @@ Zotero.Utilities.HTTP = new function() {
}
+ //
+ // WebDAV methods
+ //
+
+
+ /**
+ * Send a WebDAV PROP* request via XMLHTTPRequest
+ *
+ * Returns false if browser is offline
+ *
+ * @param {String} method PROPFIND or PROPPATCH
+ * @param {nsIURI} uri
+ * @param {String} body XML string
+ * @param {Function} callback
+ * @param {Object} requestHeaders e.g. { Depth: 0 }
+ */
+ this.WebDAV.doProp = function (method, uri, body, callback, requestHeaders) {
+ switch (method) {
+ case 'PROPFIND':
+ case 'PROPPATCH':
+ break;
+
+ default:
+ throw ("Invalid method '" + method
+ + "' in Zotero.Utilities.HTTP.doProp");
+ }
+
+ if (requestHeaders && requestHeaders.depth != undefined) {
+ var depth = requestHeaders.depth;
+ }
+
+ // Don't display password in console
+ var disp = uri.clone();
+ disp.password = "********";
+
+ var bodyStart = body.substr(0, 1024);
+ Zotero.debug("HTTP " + method + " "
+ + (depth != undefined ? "(depth " + depth + ") " : "")
+ + (body.length > 1024 ?
+ bodyStart + "... (" + body.length + " chars)" : bodyStart)
+ + " to " + disp.spec);
+
+ if (Zotero.Utilities.HTTP.browserIsOffline()) {
+ Zotero.debug("Browser is offline", 2);
+ return false;
+ }
+
+ var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance();
+ // Prevent certificate/authentication dialogs from popping up
+ xmlhttp.mozBackgroundRequest = true;
+ xmlhttp.open(method, uri.spec, true);
+
+ if (requestHeaders) {
+ for (var header in requestHeaders) {
+ xmlhttp.setRequestHeader(header, requestHeaders[header]);
+ }
+ }
+
+ xmlhttp.setRequestHeader("Content-Type", 'text/xml; charset="utf-8"');
+
+ xmlhttp.onreadystatechange = function() {
+ _stateChange(xmlhttp, callback);
+ };
+
+ xmlhttp.send(body);
+
+ return xmlhttp;
+ }
+
+
+ /**
+ * Send a WebDAV MKCOL request via XMLHTTPRequest
+ *
+ * @param {nsIURI} url
+ * @param {Function} onDone
+ * @return {XMLHTTPRequest}
+ */
+ this.WebDAV.doMkCol = function (uri, callback) {
+ // Don't display password in console
+ var disp = uri.clone();
+ disp.password = "********";
+ Zotero.debug("HTTP MKCOL to " + disp.spec);
+
+ if (Zotero.Utilities.HTTP.browserIsOffline()) {
+ return false;
+ }
+
+ var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance();
+ // Prevent certificate/authentication dialogs from popping up
+ xmlhttp.mozBackgroundRequest = true;
+ xmlhttp.open('MKCOL', uri.spec, true);
+ xmlhttp.onreadystatechange = function() {
+ _stateChange(xmlhttp, callback);
+ };
+ xmlhttp.send(null);
+ return xmlhttp;
+ }
+
+
+ /**
+ * Send a WebDAV PUT request via XMLHTTPRequest
+ *
+ * @param {nsIURI} url
+ * @param {String} body String body to PUT
+ * @param {Function} onDone
+ * @return {XMLHTTPRequest}
+ */
+ this.WebDAV.doPut = function (uri, body, callback) {
+ // Don't display password in console
+ var disp = uri.clone();
+ disp.password = "********";
+
+ var bodyStart = "'" + body.substr(0, 1024) + "'";
+ Zotero.debug("HTTP PUT "
+ + (body.length > 1024 ?
+ bodyStart + "... (" + body.length + " chars)" : bodyStart)
+ + " to " + disp.spec);
+
+ if (Zotero.Utilities.HTTP.browserIsOffline()) {
+ return false;
+ }
+
+ var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance();
+ // Prevent certificate/authentication dialogs from popping up
+ xmlhttp.mozBackgroundRequest = true;
+ xmlhttp.open("PUT", uri.spec, true);
+ xmlhttp.onreadystatechange = function() {
+ _stateChange(xmlhttp, callback);
+ };
+ xmlhttp.send(body);
+ return xmlhttp;
+ }
+
+
+ /**
+ * Send a WebDAV PUT request via XMLHTTPRequest
+ *
+ * @param {nsIURI} url
+ * @param {Function} onDone
+ * @return {XMLHTTPRequest}
+ */
+ this.WebDAV.doDelete = function (uri, callback) {
+ // Don't display password in console
+ var disp = uri.clone();
+ disp.password = "********";
+
+ Zotero.debug("WebDAV DELETE to " + disp.spec);
+
+ if (Zotero.Utilities.HTTP.browserIsOffline()) {
+ return false;
+ }
+
+ var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance();
+ // Prevent certificate/authentication dialogs from popping up
+ xmlhttp.mozBackgroundRequest = true;
+ xmlhttp.open("DELETE", uri.spec, true);
+ xmlhttp.onreadystatechange = function() {
+ _stateChange(xmlhttp, callback);
+ };
+ xmlhttp.send(null);
+ return xmlhttp;
+ }
+
+
+ /**
+ * Get the Authorization header used by a channel
+ *
+ * As of Firefox 3.0.1 subsequent requests to higher-level directories
+ * seem not to authenticate properly and just return 401s, so this
+ * can be used to manually include the Authorization header in a request
+ *
+ * It can also be used to check whether a request was forced to
+ * use authentication
+ *
+ * @param {nsIChannel} channel
+ * @return {String|FALSE} Authorization header, or FALSE if none
+ */
+ this.getChannelAuthorization = function (channel) {
+ try {
+ channel.QueryInterface(Components.interfaces.nsIHttpChannel);
+ var authHeader = channel.getRequestHeader("Authorization");
+ return authHeader;
+ }
+ catch (e) {
+ Zotero.debug(e);
+ return false;
+ }
+ }
+
+
function browserIsOffline() {
return Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService).offline;
}
- function _stateChange(xmlhttp, onDone, responseCharset){
+ function _stateChange(xmlhttp, callback, responseCharset, data) {
switch (xmlhttp.readyState){
// Request not yet made
case 1:
- break;
-
- // Called multiple while downloading in progress
+ break;
+
+ case 2:
+ break;
+
+ // Called multiple times while downloading in progress
case 3:
- break;
-
+ break;
+
// Download complete
case 4:
- if(onDone){
+ if (callback) {
// Override the content charset
if (responseCharset) {
xmlhttp.channel.contentCharset = responseCharset;
}
- onDone(xmlhttp);
+ callback(xmlhttp, data);
}
break;
}
}
-
-
}
// Downloads and processes documents with processor()
@@ -952,4 +1179,143 @@ Zotero.Utilities.AutoComplete = new function(){
}
return false;
}
-}
\ No newline at end of file
+}
+
+
+/**
+ * Base64 encode / decode
+ * From http://www.webtoolkit.info/
+ */
+Zotero.Utilities.Base64 = {
+ // private property
+ _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
+
+ // public method for encoding
+ encode : function (input) {
+ var output = "";
+ var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
+ var i = 0;
+
+ input = this._utf8_encode(input);
+
+ while (i < input.length) {
+
+ chr1 = input.charCodeAt(i++);
+ chr2 = input.charCodeAt(i++);
+ chr3 = input.charCodeAt(i++);
+
+ enc1 = chr1 >> 2;
+ enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+ enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+ enc4 = chr3 & 63;
+
+ if (isNaN(chr2)) {
+ enc3 = enc4 = 64;
+ } else if (isNaN(chr3)) {
+ enc4 = 64;
+ }
+
+ output = output +
+ this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
+ this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
+
+ }
+
+ return output;
+ },
+
+ // public method for decoding
+ decode : function (input) {
+ var output = "";
+ var chr1, chr2, chr3;
+ var enc1, enc2, enc3, enc4;
+ var i = 0;
+
+ input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
+
+ while (i < input.length) {
+
+ enc1 = this._keyStr.indexOf(input.charAt(i++));
+ enc2 = this._keyStr.indexOf(input.charAt(i++));
+ enc3 = this._keyStr.indexOf(input.charAt(i++));
+ enc4 = this._keyStr.indexOf(input.charAt(i++));
+
+ chr1 = (enc1 << 2) | (enc2 >> 4);
+ chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+ chr3 = ((enc3 & 3) << 6) | enc4;
+
+ output = output + String.fromCharCode(chr1);
+
+ if (enc3 != 64) {
+ output = output + String.fromCharCode(chr2);
+ }
+ if (enc4 != 64) {
+ output = output + String.fromCharCode(chr3);
+ }
+
+ }
+
+ output = this._utf8_decode(output);
+
+ return output;
+
+ },
+
+ // private method for UTF-8 encoding
+ _utf8_encode : function (string) {
+ string = string.replace(/\r\n/g,"\n");
+ var utftext = "";
+
+ for (var n = 0; n < string.length; n++) {
+
+ var c = string.charCodeAt(n);
+
+ if (c < 128) {
+ utftext += String.fromCharCode(c);
+ }
+ else if((c > 127) && (c < 2048)) {
+ utftext += String.fromCharCode((c >> 6) | 192);
+ utftext += String.fromCharCode((c & 63) | 128);
+ }
+ else {
+ utftext += String.fromCharCode((c >> 12) | 224);
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+ utftext += String.fromCharCode((c & 63) | 128);
+ }
+
+ }
+
+ return utftext;
+ },
+
+ // private method for UTF-8 decoding
+ _utf8_decode : function (utftext) {
+ var string = "";
+ var i = 0;
+ var c = c1 = c2 = 0;
+
+ while ( i < utftext.length ) {
+
+ c = utftext.charCodeAt(i);
+
+ if (c < 128) {
+ string += String.fromCharCode(c);
+ i++;
+ }
+ else if((c > 191) && (c < 224)) {
+ c2 = utftext.charCodeAt(i+1);
+ string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
+ i += 2;
+ }
+ else {
+ c2 = utftext.charCodeAt(i+1);
+ c3 = utftext.charCodeAt(i+2);
+ string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+ i += 3;
+ }
+
+ }
+
+ return string;
+ }
+ }
\ No newline at end of file
diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js
index 104fe00b05..aaf04135e1 100644
--- a/chrome/content/zotero/xpcom/zotero.js
+++ b/chrome/content/zotero/xpcom/zotero.js
@@ -41,6 +41,7 @@ var Zotero = new function(){
this.getZoteroDirectory = getZoteroDirectory;
this.getStorageDirectory = getStorageDirectory;
this.getZoteroDatabase = getZoteroDatabase;
+ this.getTempDirectory = getTempDirectory;
this.chooseZoteroDirectory = chooseZoteroDirectory;
this.debug = debug;
this.log = log;
@@ -249,6 +250,9 @@ var Zotero = new function(){
if (typeof e == 'string' && e.match('newer than SQL file')) {
_startupError = e;
}
+ else {
+ _startupError = "Database upgrade error";
+ }
Components.utils.reportError(_startupError);
return false;
}
@@ -265,7 +269,8 @@ var Zotero = new function(){
Zotero.Zeroconf.init();
Zotero.Sync.init();
- Zotero.Sync.Server.init();
+ Zotero.Sync.Runner.init();
+ Zotero.Sync.Storage.init();
this.initialized = true;
@@ -357,6 +362,22 @@ var Zotero = new function(){
}
+ /**
+ * @return {nsIFile}
+ */
+ function getTempDirectory() {
+ var tmp = this.getZoteroDirectory();
+ tmp.append('tmp');
+ if (!tmp.exists() || !tmp.isDirectory()) {
+ if (tmp.exists() && !tmp.isDirectory()) {
+ tmp.remove(null);
+ }
+ tmp.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
+ }
+ return tmp;
+ }
+
+
function chooseZoteroDirectory(forceRestartNow, useProfileDir) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
@@ -1284,6 +1305,66 @@ Zotero.Date = new function(){
}
+ /**
+ * Convert a JS Date object to an ISO 8601 UTC date/time
+ *
+ * @param {Date} date JS Date object
+ * @return {String} ISO 8601 UTC date/time
+ * e.g. 2008-08-15T20:00:00Z
+ */
+ this.dateToISO = function (date) {
+ var year = date.getUTCFullYear();
+ var month = date.getUTCMonth();
+ var day = date.getUTCDate();
+ var hours = date.getUTCHours();
+ var minutes = date.getUTCMinutes();
+ var seconds = date.getUTCSeconds();
+
+ var utils = new Zotero.Utilities();
+ year = utils.lpad(year, '0', 4);
+ month = utils.lpad(month + 1, '0', 2);
+ day = utils.lpad(day, '0', 2);
+ hours = utils.lpad(hours, '0', 2);
+ minutes = utils.lpad(minutes, '0', 2);
+ seconds = utils.lpad(seconds, '0', 2);
+
+ return year + '-' + month + '-' + day + 'T'
+ + hours + ':' + minutes + ':' + seconds + 'Z';
+ }
+
+
+ /**
+ * Convert an ISO 8601–formatted UTC date/time to a JS Date
+ *
+ * Adapted from http://delete.me.uk/2005/03/iso8601.html (AFL-licensed)
+ *
+ * @param {String} isoDate ISO 8601 date
+ * @return {Date} JS Date
+ */
+ this.isoToDate = function (isoDate) {
+ var re8601 = /([0-9]{4})(-([0-9]{2})(-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?/;
+ var d = isoDate.match(re8601);
+
+ var offset = 0;
+ var date = new Date(d[1], 0, 1);
+
+ if (d[3]) { date.setMonth(d[3] - 1); }
+ if (d[5]) { date.setDate(d[5]); }
+ if (d[7]) { date.setHours(d[7]); }
+ if (d[8]) { date.setMinutes(d[8]); }
+ if (d[10]) { date.setSeconds(d[10]); }
+ if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
+ if (d[14]) {
+ offset = (Number(d[16]) * 60) + Number(d[17]);
+ offset *= ((d[15] == '-') ? 1 : -1);
+ }
+
+ offset -= date.getTimezoneOffset();
+ var time = (Number(date) + (offset * 60 * 1000));
+ return new Date(time);
+ }
+
+
/*
* converts a string to an object containing:
* day: integer form of the day
@@ -1494,7 +1575,7 @@ Zotero.Date = new function(){
return string;
}
- function strToISO(str){
+ function strToISO(str) {
var date = Zotero.Date.strToDate(str);
if(date.year) {
diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd
index c5cd815b04..235a157c40 100644
--- a/chrome/locale/en-US/zotero/zotero.dtd
+++ b/chrome/locale/en-US/zotero/zotero.dtd
@@ -156,8 +156,12 @@
+
+
+
+
-
\ No newline at end of file
+
diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties
index 45bb25f11d..91fd50ba99 100644
--- a/chrome/locale/en-US/zotero/zotero.properties
+++ b/chrome/locale/en-US/zotero/zotero.properties
@@ -501,6 +501,9 @@ styles.installed = The style "%S" was installed successfully.
styles.installError = %S does not appear to be a valid CSL file.
styles.deleteStyle = Are you sure you want to delete the style "%1$S"?
+sync.storage.kbRemaining = %SKB remaining
+sync.storage.none = None
+
proxies.multiSite = Multi-Site
proxies.error = Information Validation Error
proxies.error.scheme.noHTTP = Valid proxy schemes must start with "http://" or "https://"
@@ -513,4 +516,4 @@ proxies.enableTransparentWarning.title = Warning
proxies.enableTransparentWarning.description = Please ensure that the proxies listed below belong to a library, school, or other institution with which you are affiliated. A malicious proxy could pose a security risk.
recognizePDF.couldNotRecognize.title = Could Not Retrieve Metada
-recognizePDF.couldNotRecognize.message = Zotero could not retrieve metadata for "%1$S".
\ No newline at end of file
+recognizePDF.couldNotRecognize.message = Zotero could not retrieve metadata for "%1$S".
diff --git a/chrome/skin/default/zotero/drive_network.png b/chrome/skin/default/zotero/drive_network.png
new file mode 100755
index 0000000000..63d2d5d5b1
Binary files /dev/null and b/chrome/skin/default/zotero/drive_network.png differ
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
index ee3b79e923..9e83ef9b31 100644
--- a/chrome/skin/default/zotero/overlay.css
+++ b/chrome/skin/default/zotero/overlay.css
@@ -191,11 +191,28 @@
list-style-image: url('chrome://zotero/skin/toolbar-advanced-search.png');
}
+#zotero-tb-syncProgress
+{
+ min-width: 50px;
+ width: 50px;
+ height: 10px;
+}
+
+#zotero-tb-syncProgress-tooltip row label:first-child
+{
+ text-align: right;
+ font-weight: bold;
+}
+
+#zotero-tb-storage-sync
+{
+ list-style-image: url(chrome://zotero/skin/drive_network.png);
+}
+
#zotero-tb-sync {
- margin-top: -2px;
+ list-style-image: url(chrome://zotero/skin/arrow_rotate_static.png);
margin-left: -2px;
margin-right: -2px;
- list-style-image: url(chrome://zotero/skin/arrow_rotate_static.png);
}
#zotero-tb-sync[status=animate] {
diff --git a/chrome/skin/default/zotero/preferences.css b/chrome/skin/default/zotero/preferences.css
index a43a5e15ac..a0b891882e 100644
--- a/chrome/skin/default/zotero/preferences.css
+++ b/chrome/skin/default/zotero/preferences.css
@@ -3,14 +3,8 @@ prefwindow .chromeclass-toolbar
display: -moz-box !important; /* Ignore toolbar collapse button on OS X */
}
-/* Prevent bugs in automatic prefpane sizing in Firefox 2.0
- From http://forums.mozillazine.org/viewtopic.php?p=2883233&sid=e1285f81ea9c824363802ea5ca96c9b2
-*/
prefwindow {
- width: 45em;
-}
-prefwindow > prefpane > vbox.content-box {
- height: 42em;
+ min-width: 600px;
}
radio[pane]
@@ -75,6 +69,50 @@ grid row hbox:first-child
}
+/*
+ * Sync pane
+ */
+#zotero-prefpane-sync row, #zotero-prefpane-sync row hbox
+{
+ -moz-box-align: center;
+}
+#zotero-prefpane-sync row label:first-child
+{
+ text-align: right;
+}
+#zotero-prefpane-sync row hbox
+{
+ margin-left: 4px;
+}
+#zotero-prefpane-sync row hbox label:first-child
+{
+ margin-left: 0;
+ margin-right: 0;
+}
+#zotero-prefpane-sync row hbox textbox
+{
+ margin-left: 3px;
+ margin-right: 3px;
+}
+#zotero-prefpane-sync row hbox label:last-child
+{
+ margin-left: 0;
+ margin-right: 10px;
+}
+
+#storage-settings
+{
+ margin-left: 10px;
+ margin-right: 5px;
+}
+
+#storage-verify, #storage-abort, #storage-clean
+{
+ margin-left: 0;
+ min-width: 8em;
+}
+
+
/*
* Search pane
*/
diff --git a/components/zotero-service.js b/components/zotero-service.js
index 9a298cdca3..ec0016c109 100644
--- a/components/zotero-service.js
+++ b/components/zotero-service.js
@@ -51,6 +51,7 @@ var xpcomFiles = [
'schema',
'search',
'sync',
+ 'storage',
'timeline',
'translate',
'utilities',
diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js
index 6d16414090..4d43978f78 100644
--- a/defaults/preferences/zotero.js
+++ b/defaults/preferences/zotero.js
@@ -82,10 +82,17 @@ pref("extensions.zotero.zeroconf.server.enabled", false);
// Annotation settings
pref("extensions.zotero.annotations.warnOnClose", true);
-// Server
-pref("extensions.zotero.sync.server.autoSync", true);
+// Sync
+pref("extensions.zotero.sync.autoSync", true);
pref("extensions.zotero.sync.server.username", '');
pref("extensions.zotero.sync.server.compressData", true);
+pref("extensions.zotero.sync.storage.enabled", false);
+pref("extensions.zotero.sync.storage.verified", false);
+pref("extensions.zotero.sync.storage.url", '');
+pref("extensions.zotero.sync.storage.username", '');
+pref("extensions.zotero.sync.storage.maxDownloads", 4);
+pref("extensions.zotero.sync.storage.maxUploads", 4);
+pref("extensions.zotero.sync.storage.deleteDelayDays", 30);
// Proxy
pref("extensions.zotero.proxies.autoRecognize", true);
diff --git a/userdata.sql b/userdata.sql
index 4b649a67be..8a3a9448fa 100644
--- a/userdata.sql
+++ b/userdata.sql
@@ -1,4 +1,4 @@
--- 39
+-- 40
-- This file creates tables containing user-specific data -- any changes made
-- here must be mirrored in transition steps in schema.js::_migrateSchema()
@@ -62,11 +62,14 @@ CREATE TABLE itemAttachments (
charsetID INT,
path TEXT,
originalPath TEXT,
+ syncState INT DEFAULT 0,
+ storageModTime INT,
FOREIGN KEY (itemID) REFERENCES items(itemID),
FOREIGN KEY (sourceItemID) REFERENCES items(sourceItemID)
);
CREATE INDEX itemAttachments_sourceItemID ON itemAttachments(sourceItemID);
CREATE INDEX itemAttachments_mimeType ON itemAttachments(mimeType);
+CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState);
-- Individual entries for each tag
CREATE TABLE tags (
@@ -202,6 +205,12 @@ CREATE TABLE syncDeleteLog (
);
CREATE INDEX syncDeleteLog_timestamp ON syncDeleteLog(timestamp);
+CREATE TABLE storageDeleteLog (
+ key TEXT PRIMARY KEY,
+ timestamp INT NOT NULL
+);
+CREATE INDEX storageDeleteLog_timestamp ON storageDeleteLog(timestamp);
+
CREATE TABLE translators (
translatorID TEXT PRIMARY KEY,
minVersion TEXT,