www

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

commit bd070f7b63e85454afd50c9bfc9d9a4422297fb0
parent a1277ccaa9a227fac0c85285f5b4728e3092bff4
Author: Dan Stillman <dstillman@zotero.org>
Date:   Thu, 15 Jan 2009 06:58:06 +0000

- Adds file conflict resolution -- not particularly attractive at the moment, and no Apply to All button, but possibly functional
- Fixes file syncing after editing a file locally
- Fixes a few storage bugs that could result in eternal spinning, invalid percentages, and other unpleasantries
- Made the attachment display box more flexible, including a filename field that we may or may not want to keep in the main view


Diffstat:
Mchrome/content/zotero/bindings/attachmentbox.xml | 156++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mchrome/content/zotero/bindings/merge.xml | 6++++++
Achrome/content/zotero/bindings/storagefilebox.xml | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/merge.js | 3+++
Mchrome/content/zotero/xpcom/storage.js | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mchrome/skin/default/zotero/zotero.css | 4++++
6 files changed, 396 insertions(+), 55 deletions(-)

diff --git a/chrome/content/zotero/bindings/attachmentbox.xml b/chrome/content/zotero/bindings/attachmentbox.xml @@ -41,6 +41,7 @@ <field name="displayGoButtons">false</field> <field name="clickableLink">false</field> <field name="displayButton">false</field> + <field name="displayNote">false</field> <field name="buttonCaption"/> <field name="clickHandler"/> @@ -52,31 +53,52 @@ <![CDATA[ this.editable = false; this.displayGoButtons = false; + this.displayURL = false; + this.displayFileName = false; this.clickableLink = false; - this.displayIndexed = false; + this.displayAccessed = false; this.displayPages = false; + this.displayDateModified = false; + this.displayIndexed = false; + this.displayNote = false; switch (val) { case 'view': this.displayGoButtons = true; + this.displayURL = true; + this.displayFileName = true; this.clickableLink = true; - this.displayIndexed = true; + this.displayAccessed = true; this.displayPages = true; + this.displayIndexed = true; + this.displayNote = true; break; case 'edit': this.editable = true; this.displayGoButtons = true; + this.displayURL = true; + this.displayFileName = true; this.clickableLink = true; - this.displayIndexed = true; + this.displayAccessed = true; this.displayPages = true; + this.displayIndexed = true; + this.displayNote = true; break; case 'merge': + this.displayURL = true; + this.displayFileName = true; + this.displayAccessed = true; + this.displayNote = true; this.displayButton = true; break; case 'mergeedit': + this.displayURL = true; + this.displayFileName = true; + this.displayAccessed = true; + this.displayNote = true; break; default: @@ -114,9 +136,11 @@ var goButtons = this._id('go-buttons'); var viewButton = this._id('view'); var showButton = this._id('show'); + var fileNameRow = this._id('fileNameRow'); var urlField = this._id('url'); var accessed = this._id('accessed'); var pagesRow = this._id('pages'); + var dateModifiedRow = this._id('dateModified'); var indexBox = this._id('index-box'); var selectButton = this._id('select-button'); @@ -180,36 +204,74 @@ var str = Zotero.getString('pane.item.attachments.view.link'); } - showButton.setAttribute('hidden', !isImportedURL); + showButton.hidden = !isImportedURL; // URL - var urlSpec = this.item.getField('url'); - urlField.setAttribute('value', urlSpec); - urlField.setAttribute('hidden', false); - if (this.clickableLink) { - urlField.onclick = function (event) { - ZoteroPane.loadURI(this.value, event) - }; - urlField.className = 'text-link'; + if (this.displayURL) { + var urlSpec = this.item.getField('url'); + urlField.setAttribute('value', urlSpec); + urlField.setAttribute('hidden', false); + if (this.clickableLink) { + urlField.onclick = function (event) { + ZoteroPane.loadURI(this.value, event) + }; + urlField.className = 'text-link'; + } + else { + urlField.className = ''; + } + urlField.hidden = false; } else { - urlField.className = ''; + urlField.hidden = true; } // Access date - accessed.setAttribute('value', - Zotero.getString('itemFields.accessDate') + ': ' - + Zotero.Date.sqlToDate(this.item.getField('accessDate'), true).toLocaleString()); - accessed.setAttribute('hidden', false); + if (this.displayAccessed) { + accessed.value = Zotero.localeJoin([ + Zotero.getString('itemFields.accessDate'), + ': ', + Zotero.Date.sqlToDate( + this.item.getField('accessDate'), true + ).toLocaleString() + ], ''); + accessed.hidden = false; + } + else { + accessed.hidden = true; + } } // Metadata for files else { var str = Zotero.getString('pane.item.attachments.view.file'); - showButton.setAttribute('hidden', false); - urlField.setAttribute('hidden', true); - accessed.setAttribute('hidden', true); + showButton.hidden = false; + urlField.hidden = true; + accessed.hidden = true; } + if (this.item.attachmentLinkMode + != Zotero.Attachments.LINK_MODE_LINKED_URL + && this.displayFileName) { + // TODO: localize + var file = this.item.getFile(false, true); + var fileName = file.leafName; + if (fileName) { + fileNameRow.value = Zotero.localeJoin([ + 'Filename', // TODO: localize + ': ', // TODO: probably needs to be in localized string + fileName + ], ''); + fileNameRow.hidden = false; + } + else { + fileNameRow.hidden = true; + } + } + else { + fileNameRow.hidden = true; + } + + viewButton.setAttribute('label', str); // Page count @@ -217,16 +279,35 @@ var pages = Zotero.Fulltext.getPages(this.item.id); var pages = pages ? pages.total : null; if (pages) { - var str = Zotero.getString('itemFields.pages') + ': ' + pages; - pagesRow.setAttribute('value', str); - pagesRow.setAttribute('hidden', false); + var str = Zotero.localeJoin([ + Zotero.getString('itemFields.pages'), + ': ', + pages + ], ''); + pagesRow.value = str; + pagesRow.hidden = false; } else { - pagesRow.setAttribute('hidden', true); + pagesRow.hidden = true; } } else { - pagesRow.setAttribute('hidden', true); + pagesRow.hidden = true; + } + + if (this.displayDateModified) { + var str = Zotero.localeJoin([ + Zotero.getString('itemFields.dateModified'), + ': ', + Zotero.Date.sqlToDate( + this.item.getField('dateModified'), true + ).toLocaleString() + ], ''); + dateModifiedRow.value = str; + dateModifiedRow.hidden = false; + } + else { + dateModifiedRow.hidden = true; } // Full-text index information @@ -240,16 +321,24 @@ // Note editor var noteEditor = this._id('note-editor'); - // Don't make note editable (at least for now) - if (this.mode == 'merge' || this.mode == 'mergeedit') { - noteEditor.mode = 'merge'; - noteEditor.displayButton = false; + if (this.displayNote) { + noteEditor.hidden = false; + + // Don't make note editable (at least for now) + if (this.mode == 'merge' || this.mode == 'mergeedit') { + noteEditor.mode = 'merge'; + noteEditor.displayButton = false; + } + else { + noteEditor.mode = this.mode; + } + noteEditor.parent = null; + noteEditor.item = this.item; } else { - noteEditor.mode = this.mode; + noteEditor.hidden = true; } - noteEditor.parent = null; - noteEditor.item = this.item; + if (this.displayButton) { selectButton.label = this.buttonCaption; @@ -412,9 +501,10 @@ <button id="show" label="&zotero.item.attachment.file.show;" flex="1"/> </hbox> <label id="url" crop="end"/> - + <label id="fileNameRow"/> <label id="accessed"/> <label id="pages"/> + <label id="dateModified"/> <hbox id="index-box"> <label id="index-status"/> diff --git a/chrome/content/zotero/bindings/merge.xml b/chrome/content/zotero/bindings/merge.xml @@ -134,6 +134,7 @@ if (this._leftpane.ref != 'deleted' && this._rightpane.ref != 'deleted') { + var dm1 = this._leftpane.ref.getField('dateModified'); if (dm1) { dm1 = Zotero.Date.sqlToDate(dm1); @@ -318,6 +319,10 @@ elementName = 'zoteronoteeditor'; break; + case 'storagefile': + elementName = 'zoterostoragefilebox'; + break; + default: throw ("Object type '" + this.type + "' not supported in <zoteromergepane>.ref"); @@ -349,6 +354,7 @@ switch (this.type) { case 'attachment': case 'note': + case 'storagefile': objbox.buttonCaption = 'Choose this version'; break; } diff --git a/chrome/content/zotero/bindings/storagefilebox.xml b/chrome/content/zotero/bindings/storagefilebox.xml @@ -0,0 +1,67 @@ +<?xml version="1.0"?> +<!-- + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +--> + +<!DOCTYPE bindings SYSTEM "chrome://zotero/locale/zotero.dtd"> + +<bindings xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="storage-file-box" + extends="chrome://zotero/content/bindings/attachmentbox.xml#attachment-box"> + + <implementation> + <property name="mode" onget="return this._mode;"> + <setter> + <![CDATA[ + this.editable = false; + this.displayGoButtons = false; + this.clickableLink = false; + this.displayIndexed = false; + this.displayPages = false; + this.displayNote = false; + + switch (val) { + case 'merge': + this.displayFileName = true; + this.displayButton = true; + this.displayDateModified = true; + break; + + case 'mergeedit': + this.displayFileName = true; + this.displayDateModified = true; + break; + + default: + throw ("Invalid mode '" + val + "' in storagefilebox.xml"); + } + + this._mode = val; + document.getAnonymousNodes(this)[0].setAttribute('mode', val); + ]]> + </setter> + </property> + </implementation> + </binding> +</bindings> diff --git a/chrome/content/zotero/merge.js b/chrome/content/zotero/merge.js @@ -42,6 +42,7 @@ var Zotero_Merge_Window = new function () { switch (_mergeGroup.type) { case 'item': + case 'storagefile': break; default: @@ -127,6 +128,8 @@ var Zotero_Merge_Window = new function () { } } catch (e) { + Zotero.debug(e); + var prompt = Components.classes["@mozilla.org/network/default-prompt;1"] .createInstance(Components.interfaces.nsIPrompt); prompt.alert(Zotero.getString('general.error'), e); diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js @@ -5,6 +5,8 @@ Zotero.Sync.Storage = new function () { this.SYNC_STATE_TO_UPLOAD = 0; this.SYNC_STATE_TO_DOWNLOAD = 1; this.SYNC_STATE_IN_SYNC = 2; + this.SYNC_STATE_FORCE_UPLOAD = 3; + this.SYNC_STATE_FORCE_DOWNLOAD = 4; this.SUCCESS = 1; this.ERROR_NO_URL = -1; @@ -22,7 +24,6 @@ Zotero.Sync.Storage = new function () { this.ERROR_NOT_ALLOWED = -14; this.ERROR_UNKNOWN = -15; - // // Public properties // @@ -242,7 +243,6 @@ Zotero.Sync.Storage = new function () { } Zotero.debug("Beginning storage sync"); - Zotero.Sync.Runner.setSyncIcon('animate'); _syncInProgress = true; _changesMade = false; @@ -297,6 +297,8 @@ Zotero.Sync.Storage = new function () { case this.SYNC_STATE_TO_UPLOAD: case this.SYNC_STATE_TO_DOWNLOAD: case this.SYNC_STATE_IN_SYNC: + case this.SYNC_STATE_FORCE_UPLOAD: + case this.SYNC_STATE_FORCE_DOWNLOAD: break; default: @@ -608,6 +610,10 @@ Zotero.Sync.Storage = new function () { * @return {Boolean} */ this.downloadFiles = function () { + if (!_syncInProgress) { + _syncInProgress = true; + } + // Check for active operations? var queue = Zotero.Sync.Storage.QueueManager.get('download'); if (queue.isRunning()) { @@ -624,7 +630,9 @@ Zotero.Sync.Storage = new function () { for each(var itemID in downloadFileIDs) { var item = Zotero.Items.get(itemID); - if (this.isFileModified(itemID)) { + if (Zotero.Sync.Storage.getSyncState(itemID) != + Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD + && this.isFileModified(itemID)) { Zotero.debug("File for attachment " + itemID + " has been modified"); this.setSyncState(itemID, this.SYNC_STATE_TO_UPLOAD); continue; @@ -671,6 +679,24 @@ Zotero.Sync.Storage = new function () { try { var syncModTime = Zotero.Date.toUnixTimestamp(mdate); + + // Skip download if local file exists and matches mod time + var file = item.getFile(); + if (file && file.exists() + && syncModTime == Math.round(file.lastModifiedTime / 1000)) { + Zotero.debug("Stored file mod time matches remote file -- skipping download"); + + Zotero.DB.beginTransaction(); + var syncState = Zotero.Sync.Storage.getSyncState(item.id); + var updateItem = syncState != 1; + Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, true); + Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC); + Zotero.DB.commitTransaction(); + _changesMade = true; + request.finish(); + return; + } + var uri = _getItemURI(item); var destFile = Zotero.getTempDirectory(); destFile.append(item.key + '.zip.tmp'); @@ -715,6 +741,10 @@ Zotero.Sync.Storage = new function () { * @return {Boolean} */ this.uploadFiles = function () { + if (!_syncInProgress) { + _syncInProgress = true; + } + // Check for active operations? var queue = Zotero.Sync.Storage.QueueManager.get('upload'); if (queue.isRunning()) { @@ -946,9 +976,24 @@ Zotero.Sync.Storage = new function () { } - this.resetAllSyncStates = function () { + this.resetAllSyncStates = function (syncState) { + if (!syncState) { + syncState = this.SYNC_STATE_TO_UPLOAD; + } + + switch (syncState) { + case this.SYNC_STATE_TO_UPLOAD: + case this.SYNC_STATE_TO_DOWNLOAD: + case this.SYNC_STATE_IN_SYNC: + break; + + default: + throw ("Invalid sync state '" + syncState + "' in " + + "Zotero.Sync.Storage.resetAllSyncStates()"); + } + var sql = "UPDATE itemAttachments SET syncState=?"; - Zotero.DB.query(sql, [this.SYNC_STATE_TO_UPLOAD]); + Zotero.DB.query(sql, [syncState]); } @@ -1217,18 +1262,28 @@ Zotero.Sync.Storage = new function () { try { // Check for conflict - if (mdate) { - var file = item.getFile(); - var mtime = Zotero.Date.toUnixTimestamp(mdate); - var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id); - if (mtime != smtime) { - request.error("Conflict! Last known mod time does not match remote time!" - + " (" + mtime + " != " + smtime + ")"); - return; + if (Zotero.Sync.Storage.getSyncState(item.id) + != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) { + if (mdate) { + var file = item.getFile(); + var mtime = Zotero.Date.toUnixTimestamp(mdate); + var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id); + if (mtime != smtime) { + var localData = { modTime: smtime }; + var remoteData = { modTime: mtime }; + Zotero.Sync.Storage.QueueManager.addConflict( + request.name, localData, remoteData + ); + Zotero.debug("File conflict -- last known mod time " + + "does not match remote time" + + " (" + mtime + " != " + smtime + ")"); + request.finish(); + return; + } + } + else { + Zotero.debug("Remote file not found for item " + item.id); } - } - else { - Zotero.debug("Remote file not found for item " + item.id); } var file = Zotero.getTempDirectory(); @@ -1364,8 +1419,13 @@ Zotero.Sync.Storage = new function () { * @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); + var sql = "SELECT itemID FROM itemAttachments WHERE syncState IN (?,?)"; + return Zotero.DB.columnQuery(sql, + [ + Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD, + Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD + ] + ); } @@ -1376,11 +1436,12 @@ Zotero.Sync.Storage = new function () { * @return {Number[]} Array of attachment itemIDs */ function _getFilesToUpload() { - var sql = "SELECT itemID FROM itemAttachments WHERE syncState=? " + var sql = "SELECT itemID FROM itemAttachments WHERE syncState IN (?,?) " + "AND linkMode IN (?,?)"; return Zotero.DB.columnQuery(sql, [ Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD, + Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD, Zotero.Attachments.LINK_MODE_IMPORTED_FILE, Zotero.Attachments.LINK_MODE_IMPORTED_URL ] @@ -1890,6 +1951,13 @@ Zotero.Sync.Storage = new function () { Zotero.debug("Storage sync is complete"); _syncInProgress = false; + if (!cancelled && this.resyncOnFinish) { + Zotero.debug("Force-resyncing items in conflict"); + this.resyncOnFinish = false; + this.sync(); + return; + } + if (cancelled || !_changesMade) { if (!_changesMade) { Zotero.debug("No changes made during storage sync"); @@ -1991,6 +2059,7 @@ Zotero.Sync.Storage = new function () { Zotero.Sync.Storage.QueueManager = new function () { var _queues = {}; + var _conflicts = []; /** @@ -2028,6 +2097,7 @@ Zotero.Sync.Storage.QueueManager = new function () { for each(var queue in _queues) { queue.stop(); } + _conflicts = []; } @@ -2035,6 +2105,14 @@ Zotero.Sync.Storage.QueueManager = new function () { * Tell the storage system that we're finished */ this.finish = function () { + if (_conflicts.length) { + var data = _reconcileConflicts(); + if (data) { + _processMergeData(data); + } + _conflicts = []; + } + Zotero.Sync.Storage.finish(this._cancelled); this._cancelled = false; } @@ -2075,8 +2153,10 @@ Zotero.Sync.Storage.QueueManager = new function () { //Zotero.debug("Total percentage is " + percentage); // Remaining KB - var downloadStatus = _getQueueStatus(_queues.download); - var uploadStatus = _getQueueStatus(_queues.upload); + var downloadStatus = _queues.download ? + _getQueueStatus(_queues.download) : 0; + var uploadStatus = _queues.upload ? + _getQueueStatus(_queues.upload) : 0; this.updateProgressMeters( activeRequests, percentage, downloadStatus, uploadStatus @@ -2124,6 +2204,15 @@ Zotero.Sync.Storage.QueueManager = new function () { } + this.addConflict = function (requestName, localData, remoteData) { + _conflicts.push({ + name: requestName, + localData: localData, + remoteData: remoteData + }); + } + + /** * Get a status string for a queue * @@ -2155,6 +2244,77 @@ Zotero.Sync.Storage.QueueManager = new function () { var status = Zotero.localeJoin([kbRemaining, '(' + filesRemaining + ')']); return status; } + + + function _reconcileConflicts() { + var objectPairs = []; + for each(var conflict in _conflicts) { + var item = Zotero.Items.getByKey(conflict.name); + var item1 = item.clone(); + item1.setField('dateModified', + Zotero.Date.dateToSQL(new Date(conflict.localData.modTime * 1000), true)); + var item2 = item.clone(); + item2.setField('dateModified', + Zotero.Date.dateToSQL(new Date(conflict.remoteData.modTime * 1000), true)); + objectPairs.push([item1, item2]); + } + + var io = { + dataIn: { + type: 'storagefile', + captions: [ + // TODO: localize + 'Local File', + 'Remote File', + 'Saved File' + ], + objects: objectPairs + } + }; + + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var lastWin = wm.getMostRecentWindow("navigator:browser"); + lastWin.openDialog('chrome://zotero/content/merge.xul', '', 'chrome,modal,centerscreen', io); + + if (!io.dataOut) { + return false; + } + + // Since we're only putting cloned items into the merge window, + // we have to manually set the ids + for (var i=0; i<_conflicts.length; i++) { + io.dataOut[i].id = Zotero.Items.getByKey(_conflicts[i].name).id; + } + + return io.dataOut; + } + + + function _processMergeData(data) { + if (!data.length) { + return false; + } + + Zotero.Sync.Storage.resyncOnFinish = true; + + for each(var mergeItem in data) { + var itemID = mergeItem.id; + var dateModified = mergeItem.ref.getField('dateModified'); + // Local + if (dateModified == mergeItem.left.getField('dateModified')) { + Zotero.Sync.Storage.setSyncState( + itemID, Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD + ); + } + // Remote + else { + Zotero.Sync.Storage.setSyncState( + itemID, Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD + ); + } + } + } } @@ -2229,6 +2389,10 @@ Zotero.Sync.Storage.Queue = function (name) { return remaining; }); this.__defineGetter__('percentage', function () { + if (this.totalRequests == 0) { + return 0; + } + var completedRequests = 0; for each(var request in this._requests) { completedRequests += request.percentage / 100; @@ -2374,8 +2538,14 @@ Zotero.Sync.Storage.Queue.prototype.stop = function () { Zotero.debug(this.Name + " queue is already finished"); return; } - this._stopping = true; + // If no requests, finish manually + if (this.activeRequests == 0) { + this._finishedRequests = this._finishedRequests; + return; + } + + this._stopping = true; for each(var request in this._requests) { if (!request.isFinished()) { request.stop(); @@ -2461,6 +2631,7 @@ Zotero.Sync.Storage.Request.prototype.__defineGetter__('percentage', function () if (this.progressMax == 0) { return 0; } + var percentage = Math.round((this.progress / this.progressMax) * 100); if (percentage < this._percentage) { Zotero.debug(percentage + " is less than last percentage of " diff --git a/chrome/skin/default/zotero/zotero.css b/chrome/skin/default/zotero/zotero.css @@ -73,6 +73,10 @@ zoteroattachmentbox -moz-binding: url('chrome://zotero/content/bindings/attachmentbox.xml#attachment-box'); } +zoterostoragefilebox +{ + -moz-binding: url('chrome://zotero/content/bindings/storagefilebox.xml#storage-file-box'); +} zoteronoteeditor {