commit 221d1da34030992810e83021e394f3450f0f0894
parent f2d03014b0136ea4a4ea391e91ddabffe5f425f8
Author: Simon Kornblith <simon@simonster.com>
Date: Sat, 2 Jun 2012 16:58:14 -0400
Attachment progress notifications. These are already hooked up to the UI in the connector, but still need to be hooked up to the UI in Firefox.
Addresses #3
Diffstat:
5 files changed, 218 insertions(+), 34 deletions(-)
diff --git a/chrome/content/zotero/browser.js b/chrome/content/zotero/browser.js
@@ -551,9 +551,9 @@ var Zotero_Browser = new function() {
function _constructLookupFunction(tab, success) {
return function(e) {
tab.page.translate.setTranslator(tab.page.translators[0]);
- tab.page.translate.clearHandlers("done");
+ tab.page.translate.clearHandlers("itemsDone");
tab.page.translate.clearHandlers("itemDone");
- tab.page.translate.setHandler("done", function(obj, status) {
+ tab.page.translate.setHandler("itemsDone", function(obj, status) {
if(status) {
success(e, obj);
Zotero_Browser.progress.close();
@@ -730,10 +730,10 @@ Zotero_Browser.Tab.prototype.translate = function(libraryID, collectionID, trans
// use first translator available
this.page.translate.setTranslator(translator ? translator : this.page.translators[0]);
- this.page.translate.clearHandlers("done");
+ this.page.translate.clearHandlers("itemsDone");
this.page.translate.clearHandlers("itemDone");
- this.page.translate.setHandler("done", function(obj, item) { Zotero_Browser.finishScraping(obj, item) });
+ this.page.translate.setHandler("itemsDone", function(obj, item) { Zotero_Browser.finishScraping(obj, item) });
this.page.translate.setHandler("itemDone", function(obj, dbItem, item) { Zotero_Browser.itemDone(obj, dbItem, item, collection) });
this.page.translate.translate(libraryID);
diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js
@@ -220,6 +220,7 @@ Zotero.Attachments = new function(){
var urlRe = /^https?:\/\/[^\s]*$/;
var matches = urlRe.exec(url);
if (!matches) {
+ callback(false);
throw ("Invalid URL '" + url + "' in Zotero.Attachments.importFromURL()");
}
@@ -297,9 +298,11 @@ Zotero.Attachments = new function(){
if (mimeType == 'application/pdf' &&
Zotero.MIME.sniffForMIMEType(str) != 'application/pdf') {
- Zotero.debug("Downloaded PDF did not have MIME type "
- + "'application/pdf' in Attachments.importFromURL()", 2);
+ var errString = "Downloaded PDF did not have MIME type "
+ + "'application/pdf' in Attachments.importFromURL()";
+ Zotero.debug(errString, 2);
attachmentItem.erase();
+ callback(false, new Error(errString));
return;
}
@@ -311,6 +314,8 @@ Zotero.Attachments = new function(){
Zotero.Notifier.trigger('add', 'item', itemID);
Zotero.Notifier.trigger('modify', 'item', sourceItemID);
+
+ if(callback) callback(attachmentItem);
// We don't have any way of knowing that the file
// is flushed to disk, so we just wait a second
@@ -325,6 +330,7 @@ Zotero.Attachments = new function(){
catch (e) {
// Clean up
attachmentItem.erase();
+ callback(false, e);
throw (e);
}
@@ -346,8 +352,6 @@ Zotero.Attachments = new function(){
nsIURL.spec = url;
wbp.saveURI(nsIURL, null, null, null, null, file);
- if(callback) callback(attachmentItem);
-
return attachmentItem;
}
catch (e){
@@ -553,7 +557,7 @@ Zotero.Attachments = new function(){
Zotero.Fulltext.indexDocument(document, itemID);
Zotero.Notifier.trigger('refresh', 'item', itemID);
if (callback) {
- callback();
+ callback(attachmentItem);
}
};
}
@@ -612,6 +616,7 @@ Zotero.Attachments = new function(){
// Clean up
var item = Zotero.Items.get(itemID);
item.erase();
+ callback(false, e);
throw (e);
}
diff --git a/chrome/content/zotero/xpcom/connector/translate_item.js b/chrome/content/zotero/xpcom/connector/translate_item.js
@@ -79,10 +79,23 @@ Zotero.Translate.ItemSaver.prototype = {
payload.cookie = this._cookie;
}
- Zotero.Connector.callMethod("saveItems", payload, function(success, status) {
- if(success !== false) {
+ Zotero.Connector.callMethod("saveItems", payload, function(data, status) {
+ if(data !== false) {
Zotero.debug("Translate: Save via Standalone succeeded");
+ var haveAttachments = false;
+ if(data.items) {
+ for(var i=0; i<data.items.length; i++) {
+ var attachments = items[i].attachments = data.items[i].attachments;
+ for(var j=0; j<attachments.length; j++) {
+ if(attachments[j].id) {
+ attachmentCallback(attachments[j], 0);
+ haveAttachments = true;
+ }
+ }
+ }
+ }
callback(true, items);
+ if(haveAttachments) me._pollForProgress(items, attachmentCallback);
} else if(Zotero.isFx) {
callback(false, new Error("Save via Standalone failed with "+status));
} else {
@@ -92,6 +105,60 @@ Zotero.Translate.ItemSaver.prototype = {
},
/**
+ * Polls for updates to attachment progress
+ * @param items Items in Zotero.Item.toArray() format
+ * @param {Function} attachmentCallback A callback that receives information about attachment
+ * save progress. The callback will be called as attachmentCallback(attachment, false, error)
+ * on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
+ * attachmentCallback() will be called with all attachments that will be saved
+ */
+ "_pollForProgress":function(items, attachmentCallback) {
+ var attachments = [];
+ var progressIDs = [];
+ var previousStatus = [];
+ for(var i=0; i<items.length; i++) {
+ var itemAttachments = items[i].attachments;
+ for(var j=0; j<itemAttachments.length; j++) {
+ if(itemAttachments[j].id) {
+ attachments.push(itemAttachments[j]);
+ progressIDs.push(itemAttachments[j].id);
+ previousStatus.push(0);
+ }
+ }
+ }
+
+ var nPolls = 0;
+ var poll = function() {
+ Zotero.Connector.callMethod("attachmentProgress", progressIDs, function(currentStatus, status) {
+ if(currentStatus) {
+ for(var i=0; i<attachments.length; i++) {
+ if(currentStatus[i] === 100 || currentStatus[i] === false) {
+ attachmentCallback(attachments[i], currentStatus[i]);
+ attachments.splice(i, 1);
+ progressIDs.splice(i, 1);
+ previousStatus.splice(i, 1);
+ currentStatus.splice(i, 1);
+ i--;
+ } else if(currentStatus[i] !== previousStatus[i]) {
+ attachmentCallback(attachments[i], currentStatus[i]);
+ previousStatus[i] = currentStatus[i];
+ }
+ }
+
+ if(nPolls++ < 60 && attachments.length) {
+ setTimeout(poll, 1000);
+ }
+ } else {
+ for(var i=0; i<attachments.length; i++) {
+ attachmentCallback(attachments[i], false, "Lost connection to Zotero Standalone");
+ }
+ }
+ });
+ };
+ poll();
+ },
+
+ /**
* Saves items to server
* @param items Items in Zotero.Item.toArray() format
* @param {Function} callback A callback to be executed when saving is complete. If saving
@@ -101,6 +168,7 @@ Zotero.Translate.ItemSaver.prototype = {
* @param {Function} attachmentCallback A callback that receives information about attachment
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
+ * attachmentCallback() will be called with all attachments that will be saved
*/
"_saveToServer":function(items, callback, attachmentCallback) {
var newItems = [], typedArraysSupported = false;
diff --git a/chrome/content/zotero/xpcom/server_connector.js b/chrome/content/zotero/xpcom/server_connector.js
@@ -27,6 +27,31 @@ const CONNECTOR_API_VERSION = 2;
Zotero.Server.Connector = function() {};
Zotero.Server.Connector._waitingForSelection = {};
Zotero.Server.Connector.Data = {};
+Zotero.Server.Connector.AttachmentProgressManager = new function() {
+ var attachmentsInProgress = new WeakMap(),
+ attachmentProgress = {},
+ i = 1;
+
+ /**
+ * Called on attachment progress
+ */
+ this.onProgress = function(attachment, progress, error) {
+ var progressID = attachmentsInProgress.get(attachment);
+ if(!progressID) {
+ progressID = attachment.id = i++;
+ attachmentsInProgress.set(attachment, progressID);
+ }
+
+ attachmentProgress[progressID] = progress;
+ };
+
+ /**
+ * Gets progress for a given progressID
+ */
+ this.getProgressForID = function(progressID) {
+ return progressID in attachmentProgress ? attachmentProgress[progressID] : 0;
+ };
+};
/**
* Lists all available translators, including code for translators that should be run on every page
@@ -169,7 +194,7 @@ Zotero.Server.Connector.Detect.prototype = {
* cookie - document.cookie or equivalent
*
* Returns:
- * If a single item, sends response code 201 with no body.
+ * If a single item, sends response code 201 with item in body.
* If multiple items, sends response code 300 with the following content:
* items - list of items in the format typically passed to the selectItems handler
* instanceID - an ID that must be maintained for the subsequent Zotero.Connector.Select call
@@ -246,9 +271,13 @@ Zotero.Server.Connector.SavePage.prototype = {
if(collection) {
collection.addItem(item.id);
}
+
jsonItems.push(jsonItem);
});
- translate.setHandler("done", function(obj, item) {
+ translate.setHandler("attachmentProgress", function(obj, attachment, progress, error) {
+ Zotero.Server.Connector.AttachmentProgressManager.onProgress(attachment, progress, error);
+ });
+ translate.setHandler("itemsDone", function(obj, item) {
Zotero.Browser.deleteHiddenBrowser(me._browser);
if(jsonItems.length || me.selectedItems === false) {
me.sendResponse(201, "application/json", JSON.stringify({"items":jsonItems}));
@@ -269,7 +298,7 @@ Zotero.Server.Connector.SavePage.prototype = {
* Accepts:
* items - an array of JSON format items
* Returns:
- * 201 response code with empty body
+ * 201 response code with item in body.
*/
Zotero.Server.Connector.SaveItem = function() {};
Zotero.Server.Endpoints["/connector/saveItems"] = Zotero.Server.Connector.SaveItem;
@@ -299,22 +328,23 @@ Zotero.Server.Connector.SaveItem.prototype = {
// save items
var itemSaver = new Zotero.Translate.ItemSaver(libraryID,
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD, 1, undefined, cookieSandbox);
- itemSaver.saveItems(data.items, function(returnValue, data) {
+ itemSaver.saveItems(data.items, function(returnValue, newItems) {
if(returnValue) {
try {
- for each(var item in data) {
+ for each(var item in newItems) {
if(collection) collection.addItem(item.id);
}
- sendResponseCallback(201);
+
+ sendResponseCallback(201, "application/json", JSON.stringify({"items":data.items}));
} catch(e) {
Zotero.logError(e);
sendResponseCallback(500);
}
} else {
sendResponseCallback(500);
- throw data;
+ throw newItems;
}
- });
+ }, Zotero.Server.Connector.AttachmentProgressManager.onProgress);
}
}
@@ -435,6 +465,31 @@ Zotero.Server.Connector.SelectItems.prototype = {
}
/**
+ * Gets progress for an attachment that is currently being saved
+ *
+ * Accepts:
+ * Array of attachment IDs returned by savePage, saveItems, or saveSnapshot
+ * Returns:
+ * 200 response code with current progress in body. Progress is either a number
+ * between 0 and 100 or "false" to indicate that saving failed.
+ */
+Zotero.Server.Connector.Progress = function() {};
+Zotero.Server.Endpoints["/connector/attachmentProgress"] = Zotero.Server.Connector.Progress;
+Zotero.Server.Connector.Progress.prototype = {
+ "supportedMethods":["POST"],
+ "supportedDataTypes":["application/json"],
+
+ /**
+ * @param {String} data POST data or GET query string
+ * @param {Function} sendResponseCallback function to send HTTP response
+ */
+ "init":function(data, sendResponseCallback) {
+ sendResponseCallback(200, "application/json",
+ JSON.stringify([Zotero.Server.Connector.AttachmentProgressManager.getProgressForID(id) for each(id in data)]));
+ }
+};
+
+/**
* Get code for a translator
*
* Accepts:
diff --git a/chrome/content/zotero/xpcom/translation/translate_item.js b/chrome/content/zotero/xpcom/translation/translate_item.js
@@ -87,7 +87,18 @@ Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1;
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2;
Zotero.Translate.ItemSaver.prototype = {
- "saveItems":function(items, callback) {
+ /**
+ * Saves items to Standalone or the server
+ * @param items Items in Zotero.Item.toArray() format
+ * @param {Function} callback A callback to be executed when saving is complete. If saving
+ * succeeded, this callback will be passed true as the first argument and a list of items
+ * saved as the second. If saving failed, the callback will be passed false as the first
+ * argument and an error object as the second
+ * @param {Function} [attachmentCallback] A callback that receives information about attachment
+ * save progress. The callback will be called as attachmentCallback(attachment, false, error)
+ * on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
+ */
+ "saveItems":function(items, callback, attachmentCallback) {
// if no open transaction, open a transaction and add a timer call to close it
var openedTransaction = false;
if(!Zotero.DB.transactionInProgress()) {
@@ -110,8 +121,8 @@ Zotero.Translate.ItemSaver.prototype = {
newItem = Zotero.Items.get(myID);
} else {
if(type == "attachment") { // handle attachments differently
- newItem = this._saveAttachment(item);
- if(!newItem) return;
+ newItem = this._saveAttachment(item, null, attachmentCallback);
+ if(!newItem) continue;
var myID = newItem.id;
} else {
var typeID = Zotero.ItemTypes.getID(type);
@@ -137,8 +148,12 @@ Zotero.Translate.ItemSaver.prototype = {
// handle attachments
if(item.attachments) {
for(var i=0; i<item.attachments.length; i++) {
- var newAttachment = this._saveAttachment(item.attachments[i], myID);
- if(newAttachment) this._saveTags(item.attachments[i], newAttachment);
+ var newAttachment = this._saveAttachment(item.attachments[i], myID, attachmentCallback);
+ if(typeof newAttachment === "object") {
+ this._saveTags(item.attachments[i], newAttachment);
+ } else if(!newAttachment) {
+ item.attachments.splice(i--, 1);
+ }
}
}
}
@@ -209,7 +224,7 @@ Zotero.Translate.ItemSaver.prototype = {
if(!attachment.url && !attachment.path) {
Zotero.debug("Translate: Ignoring attachment: no path or URL specified", 2);
- return;
+ return false;
}
if(!attachment.path) {
@@ -221,34 +236,40 @@ Zotero.Translate.ItemSaver.prototype = {
attachment.url = false;
} else if(protocol != "http" && protocol != "https") {
Zotero.debug("Translate: Unrecognized protocol "+protocol, 2);
- return;
+ return false;
}
}
if(!attachment.path) {
// create from URL
+ attachment.linkMode = "linked_file";
try {
var myID = Zotero.Attachments.linkFromURL(attachment.url, parentID,
(attachment.mimeType ? attachment.mimeType : undefined),
(attachment.title ? attachment.title : undefined));
} catch(e) {
Zotero.debug("Translate: Error adding attachment "+attachment.url, 2);
- return;
+ attachmentCallback(attachment, false, e);
+ return false;
}
Zotero.debug("Translate: Created attachment; id is "+myID, 4);
+ attachmentCallback(attachment, 100);
var newItem = Zotero.Items.get(myID);
} else {
var file = this._parsePath(attachment.path);
if(!file || !file.exists()) return;
if (attachment.url) {
+ attachment.linkMode = "imported_url";
var myID = Zotero.Attachments.importSnapshotFromFile(file,
attachment.url, attachment.title, attachment.mimeType, attachment.charset,
parentID);
}
else {
+ attachment.linkMode = "imported_file";
var myID = Zotero.Attachments.importFromFile(file, parentID);
}
+ attachmentCallback(attachment, 100);
}
var newItem = Zotero.Items.get(myID);
@@ -306,7 +327,7 @@ Zotero.Translate.ItemSaver.prototype = {
return file;
},
- "_saveAttachmentDownload":function(attachment, parentID) {
+ "_saveAttachmentDownload":function(attachment, parentID, attachmentCallback) {
Zotero.debug("Translate: Adding attachment", 4);
// determine whether to save attachments at all
@@ -319,7 +340,7 @@ Zotero.Translate.ItemSaver.prototype = {
var shouldAttach = ((attachment.document
|| (attachment.mimeType && attachment.mimeType == "text/html")) && automaticSnapshots)
|| downloadAssociatedFiles;
- if(!shouldAttach) return;
+ if(!shouldAttach) return false;
if(attachment.document && "__wrappedDOMObject" in attachment.document) {
attachment.document = attachment.document.__wrappedDOMObject;
@@ -327,10 +348,18 @@ Zotero.Translate.ItemSaver.prototype = {
if(attachment.snapshot === false || !this._saveFiles) {
// if snapshot is explicitly set to false, attach as link
+ attachment.linkMode = "linked_url";
if(attachment.document) {
- Zotero.Attachments.linkFromURL(attachment.document.location.href, parentID,
- (attachment.mimeType ? attachment.mimeType : attachment.document.contentType),
- (attachment.title ? attachment.title : attachment.document.title));
+ try {
+ Zotero.Attachments.linkFromURL(attachment.document.location.href, parentID,
+ (attachment.mimeType ? attachment.mimeType : attachment.document.contentType),
+ (attachment.title ? attachment.title : attachment.document.title));
+ attachmentCallback(attachment, 100);
+ } catch(e) {
+ Zotero.debug("Translate: Error adding attachment "+attachment.url, 2);
+ attachmentCallback(attachment, false, e);
+ }
+ return true;
} else {
if(!attachment.mimeType || !attachment.title) {
Zotero.debug("Translate: Either mimeType or title is missing; attaching file will be slower", 3);
@@ -340,19 +369,33 @@ Zotero.Translate.ItemSaver.prototype = {
Zotero.Attachments.linkFromURL(attachment.url, parentID,
(attachment.mimeType ? attachment.mimeType : undefined),
(attachment.title ? attachment.title : undefined));
+ attachmentCallback(attachment, 100);
} catch(e) {
Zotero.debug("Translate: Error adding attachment "+attachment.url, 2);
+ attachmentCallback(attachment, false, e);
}
+ return true;
}
} else {
// if snapshot is not explicitly set to false, retrieve snapshot
if(attachment.document) {
if(automaticSnapshots) {
try {
- Zotero.Attachments.importFromDocument(attachment.document, parentID, attachment.title);
+ attachment.linkMode = "imported_url";
+ Zotero.Attachments.importFromDocument(attachment.document,
+ parentID, attachment.title, function(status, err) {
+ if(status) {
+ attachmentCallback(attachment, 100);
+ } else {
+ attachmentCallback(attachment, false, err);
+ }
+ }, this._libraryID);
+ attachmentCallback(attachment, 0);
} catch(e) {
Zotero.debug("Translate: Error attaching document", 2);
+ attachmentCallback(attachment, false, e);
}
+ return true;
}
// Save attachment if snapshot pref enabled or not HTML
// (in which case downloadAssociatedFiles applies)
@@ -364,14 +407,27 @@ Zotero.Translate.ItemSaver.prototype = {
var fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentID);
try {
Zotero.debug('Importing attachment from URL');
+ attachment.linkMode = "imported_url";
Zotero.Attachments.importFromURL(attachment.url, parentID, title,
- fileBaseName, null, mimeType, this._libraryID, null, this._cookieSandbox);
+ fileBaseName, null, mimeType, this._libraryID, function(status, err) {
+ // TODO: actually indicate progress during download
+ if(status) {
+ attachmentCallback(attachment, 100);
+ } else {
+ attachmentCallback(attachment, false, err);
+ }
+ }, this._cookieSandbox);
+ attachmentCallback(attachment, 0);
} catch(e) {
Zotero.debug("Translate: Error adding attachment "+attachment.url, 2);
+ attachmentCallback(attachment, false, e);
}
+ return true;
}
}
}
+
+ return false;
},
"_saveFields":function(item, newItem) {