- 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
This commit is contained in:
Dan Stillman 2009-01-15 06:58:06 +00:00
parent a1277ccaa9
commit bd070f7b63
6 changed files with 396 additions and 55 deletions

View file

@ -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,9 +204,10 @@
var str = Zotero.getString('pane.item.attachments.view.link');
}
showButton.setAttribute('hidden', !isImportedURL);
showButton.hidden = !isImportedURL;
// URL
if (this.displayURL) {
var urlSpec = this.item.getField('url');
urlField.setAttribute('value', urlSpec);
urlField.setAttribute('hidden', false);
@ -195,21 +220,58 @@
else {
urlField.className = '';
}
urlField.hidden = false;
}
else {
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,6 +321,9 @@
// Note editor
var noteEditor = this._id('note-editor');
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';
@ -250,6 +334,11 @@
}
noteEditor.parent = null;
noteEditor.item = this.item;
}
else {
noteEditor.hidden = true;
}
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"/>

View file

@ -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;
}

View file

@ -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>

View file

@ -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);

View file

@ -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,19 +1262,29 @@ Zotero.Sync.Storage = new function () {
try {
// Check for conflict
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) {
request.error("Conflict! Last known mod time does not match remote time!"
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);
}
}
var file = Zotero.getTempDirectory();
file.append(item.key + '.zip');
@ -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 "

View file

@ -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
{