www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

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:
Mchrome/content/zotero/browser.js | 8++++----
Mchrome/content/zotero/xpcom/attachments.js | 15++++++++++-----
Mchrome/content/zotero/xpcom/connector/translate_item.js | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mchrome/content/zotero/xpcom/server_connector.js | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mchrome/content/zotero/xpcom/translation/translate_item.js | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
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) {