Fix replacement of existing item storage directories

- Make Zotero.Attachments.createDirectoryForItem() delete existing
  directory instead of moving it to orphaned-files; also now returns a
  string path instead of an nsIFile
- Use above function during file sync instead of
  _deleteExistingAttachmentFiles(), which was partly broken
- Fix throwing on errors when saving some attachment types
This commit is contained in:
Dan Stillman 2016-12-12 03:26:35 -05:00
parent 60ed6d447e
commit 80f888f374
6 changed files with 133 additions and 185 deletions

View File

@ -61,7 +61,8 @@ Zotero.Attachments = new function(){
} }
var attachmentItem, itemID, newFile, contentType; var attachmentItem, itemID, newFile, contentType;
return Zotero.DB.executeTransaction(function* () { try {
yield Zotero.DB.executeTransaction(function* () {
// Create a new attachment // Create a new attachment
attachmentItem = new Zotero.Item('attachment'); attachmentItem = new Zotero.Item('attachment');
if (parentItemID) { if (parentItemID) {
@ -84,8 +85,7 @@ Zotero.Attachments = new function(){
destDir = yield this.createDirectoryForItem(attachmentItem); destDir = yield this.createDirectoryForItem(attachmentItem);
// Point to copied file // Point to copied file
newFile = destDir.clone(); newFile = OS.Path.join(destDir, newName);
newFile.append(newName);
// Copy file to unique filename, which automatically shortens long filenames // Copy file to unique filename, which automatically shortens long filenames
newFile = Zotero.File.copyToUnique(file, newFile); newFile = Zotero.File.copyToUnique(file, newFile);
@ -95,29 +95,28 @@ Zotero.Attachments = new function(){
attachmentItem.attachmentContentType = contentType; attachmentItem.attachmentContentType = contentType;
attachmentItem.attachmentPath = newFile.path; attachmentItem.attachmentPath = newFile.path;
yield attachmentItem.save(saveOptions); yield attachmentItem.save(saveOptions);
}.bind(this)) }.bind(this));
.then(function () { yield _postProcessFile(attachmentItem, newFile, contentType);
return _postProcessFile(attachmentItem, newFile, contentType); }
}) catch (e) {
.catch(function (e) {
Zotero.logError(e); Zotero.logError(e);
var msg = "Failed importing file " + file.path; Zotero.logError("Failed importing file " + file.path);
Zotero.logError(msg);
// Clean up // Clean up
try { try {
if (destDir && destDir.exists()) { if (destDir && (yield OS.File.exists(destDir))) {
destDir.remove(true); yield OS.File.removeDir(destDir);
} }
} }
catch (e) { catch (e) {
Zotero.logError(e); Zotero.logError(e);
} }
}.bind(this))
.then(function () { throw e;
}
return attachmentItem; return attachmentItem;
}); });
});
/** /**
@ -174,7 +173,8 @@ Zotero.Attachments = new function(){
} }
var attachmentItem, itemID, destDir, newFile; var attachmentItem, itemID, destDir, newFile;
return Zotero.DB.executeTransaction(function* () { try {
yield Zotero.DB.executeTransaction(function* () {
// Create a new attachment // Create a new attachment
attachmentItem = new Zotero.Item('attachment'); attachmentItem = new Zotero.Item('attachment');
let {libraryID, key: parentKey} = Zotero.Items.getLibraryAndKeyFromID(parentItemID); let {libraryID, key: parentKey} = Zotero.Items.getLibraryAndKeyFromID(parentItemID);
@ -193,7 +193,7 @@ Zotero.Attachments = new function(){
var storageDir = Zotero.getStorageDirectory(); var storageDir = Zotero.getStorageDirectory();
destDir = this.getStorageDirectory(attachmentItem); destDir = this.getStorageDirectory(attachmentItem);
yield _moveOrphanedDirectory(destDir); yield OS.File.removeDir(destDir.path);
file.parent.copyTo(storageDir, destDir.leafName); file.parent.copyTo(storageDir, destDir.leafName);
// Point to copied file // Point to copied file
@ -202,11 +202,10 @@ Zotero.Attachments = new function(){
attachmentItem.attachmentPath = newFile.path; attachmentItem.attachmentPath = newFile.path;
yield attachmentItem.save(); yield attachmentItem.save();
}.bind(this)) }.bind(this));
.then(function () { yield _postProcessFile(attachmentItem, newFile, contentType, charset);
return _postProcessFile(attachmentItem, newFile, contentType, charset); }
}) catch (e) {
.catch(function (e) {
Zotero.logError(e); Zotero.logError(e);
// Clean up // Clean up
@ -218,11 +217,11 @@ Zotero.Attachments = new function(){
catch (e) { catch (e) {
Zotero.logError(e, 1); Zotero.logError(e, 1);
} }
}.bind(this))
.then(function () { throw e;
}
return attachmentItem; return attachmentItem;
}); });
});
/** /**
@ -787,21 +786,18 @@ Zotero.Attachments = new function(){
/** /**
* Create directory for attachment files within storage directory * Create directory for attachment files within storage directory
* *
* If a directory exists with the same name, move it to orphaned-files * If a directory exists, delete and recreate
* *
* @param {Number} itemID - Item id * @param {Number} itemID - Item id
* @return {Promise<nsIFile>} * @return {Promise<String>} - Path of new directory
*/ */
this.createDirectoryForItem = Zotero.Promise.coroutine(function* (item) { this.createDirectoryForItem = Zotero.Promise.coroutine(function* (item) {
if (!(item instanceof Zotero.Item)) { if (!(item instanceof Zotero.Item)) {
throw new Error("'item' must be a Zotero.Item"); throw new Error("'item' must be a Zotero.Item");
} }
var dir = this.getStorageDirectory(item); var dir = this.getStorageDirectory(item).path;
yield _moveOrphanedDirectory(dir); yield OS.File.removeDir(dir, { ignoreAbsent: true });
if (!dir.exists()) { yield Zotero.File.createDirectoryIfMissingAsync(dir);
Zotero.debug("Creating directory " + dir.path);
dir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
}
return dir; return dir;
}); });
@ -1160,55 +1156,6 @@ Zotero.Attachments = new function(){
} }
/**
* If directory exists and is non-empty, move it to orphaned-files directory
*
* If empty, just remove it
*/
var _moveOrphanedDirectory = Zotero.Promise.coroutine(function* (dir) {
if (!dir.exists()) {
return;
}
dir = dir.clone();
// If directory is empty or has only hidden files, delete it
var files = dir.directoryEntries;
files.QueryInterface(Components.interfaces.nsIDirectoryEnumerator);
var empty = true;
while (files.hasMoreElements()) {
var file = files.getNext();
file.QueryInterface(Components.interfaces.nsIFile);
if (file.leafName[0] == '.') {
continue;
}
empty = false;
break;
}
files.close();
if (empty) {
dir.remove(true);
return;
}
// Create orphaned-files directory if it doesn't exist
var orphaned = OS.Path.join(Zotero.DataDirectory.dir, 'orphaned-files');
yield Zotero.File.createDirectoryIfMissingAsync(orphaned);
// Find unique filename for orphaned file
var orphanTarget = OS.Path.join(orphaned, dir.leafName);
var newName = null;
if (yield OS.File.exists(orphanTarget)) {
let newFile = yield OS.File.openUnique(orphanTarget, { humanReadable: true })
newName = OS.Path.basename(newFile.path);
newFile.file.close();
}
// Move target to orphaned files directory
dir.moveTo(Zotero.File.pathToFile(orphaned), newName);
});
/** /**
* Create a new item of type 'attachment' and add to the itemAttachments table * Create a new item of type 'attachment' and add to the itemAttachments table
* *

View File

@ -2536,7 +2536,7 @@ Zotero.Item.prototype.relinkAttachmentFile = Zotero.Promise.coroutine(function*
} }
// Create storage directory if necessary // Create storage directory if necessary
else if (!(yield OS.File.exists(storageDir))) { else if (!(yield OS.File.exists(storageDir))) {
Zotero.Attachments.createDirectoryForItem(this); yield Zotero.Attachments.createDirectoryForItem(this);
} }
let newFile; let newFile;

View File

@ -659,7 +659,15 @@ Zotero.Fulltext = Zotero.FullText = new function(){
// If file is stored outside of Zotero, create a directory for the item // If file is stored outside of Zotero, create a directory for the item
// in the storage directory and save the cache file there // in the storage directory and save the cache file there
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
var parentDirPath = (yield Zotero.Attachments.createDirectoryForItem(item)).path; let path = item.getFilePath();
if (!path) {
Zotero.debug("Invalid path for item " + itemID);
return false;
}
var parentDirPath = OS.Path.dirname(path);
if (!(yield OS.File.exists(parentDirPath))) {
yield Zotero.Attachments.createDirectoryForItem(item);
}
} }
else { else {
var parentDirPath = OS.Path.dirname(filePath); var parentDirPath = OS.Path.dirname(filePath);

View File

@ -626,12 +626,7 @@ Zotero.Sync.Storage.Local = {
throw new Error("Downloaded file not found"); throw new Error("Downloaded file not found");
} }
var parentDirPath = Zotero.Attachments.getStorageDirectory(item).path;
if (!(yield OS.File.exists(parentDirPath))) {
yield Zotero.Attachments.createDirectoryForItem(item); yield Zotero.Attachments.createDirectoryForItem(item);
}
yield this._deleteExistingAttachmentFiles(item);
var path = item.getFilePath(); var path = item.getFilePath();
if (!path) { if (!path) {
@ -741,16 +736,12 @@ Zotero.Sync.Storage.Local = {
} }
var parentDir = Zotero.Attachments.getStorageDirectory(item).path; var parentDir = Zotero.Attachments.getStorageDirectory(item).path;
if (!(yield OS.File.exists(parentDir))) {
yield Zotero.Attachments.createDirectoryForItem(item);
}
try { try {
yield this._deleteExistingAttachmentFiles(item); yield Zotero.Attachments.createDirectoryForItem(item);
} }
catch (e) { catch (e) {
zipReader.close(); zipReader.close();
throw (e); throw e;
} }
var returnFile = null; var returnFile = null;
@ -906,17 +897,6 @@ Zotero.Sync.Storage.Local = {
}), }),
_deleteExistingAttachmentFiles: Zotero.Promise.method(function (item) {
var parentDir = Zotero.Attachments.getStorageDirectory(item).path;
// OS.File.DirectoryIterator, used by OS.File.removeDir(), isn't reliable on Travis,
// returning entry.isDir == false for subdirectories, so use nsIFile instead
if (Zotero.automatedTest) {
Zotero.File.pathToFile(parentDir).remove(true);
}
return OS.File.removeDir(parentDir);
}),
/** /**
* @return {Promise<Object[]>} - A promise for an array of conflict objects * @return {Promise<Object[]>} - A promise for an array of conflict objects
*/ */

View File

@ -274,5 +274,36 @@ describe("Zotero.Attachments", function() {
assert.isTrue(yield Zotero.Attachments.hasMultipleFiles(item)); assert.isTrue(yield Zotero.Attachments.hasMultipleFiles(item));
assert.equal((yield Zotero.Attachments.getNumFiles(item)), 2); assert.equal((yield Zotero.Attachments.getNumFiles(item)), 2);
}) })
}) });
describe("#createDirectoryForItem()", function () {
it("should create missing directory", function* () {
var item = yield importFileAttachment('test.png');
var path = OS.Path.dirname(item.getFilePath());
yield OS.File.removeDir(path);
yield Zotero.Attachments.createDirectoryForItem(item);
assert.isTrue(yield OS.File.exists(path));
});
it("should delete all existing files", function* () {
var item = yield importFileAttachment('test.html');
var path = OS.Path.dirname(item.getFilePath());
var files = ['a', 'b', 'c', 'd'];
for (let file of files) {
yield Zotero.File.putContentsAsync(OS.Path.join(path, file), file);
}
yield Zotero.Attachments.createDirectoryForItem(item);
assert.isTrue(yield Zotero.File.directoryIsEmpty(path));
assert.isTrue(yield OS.File.exists(path));
});
it("should handle empty directory", function* () {
var item = yield importFileAttachment('test.png');
var file = item.getFilePath();
var dir = OS.Path.dirname(item.getFilePath());
yield OS.File.remove(file);
yield Zotero.Attachments.createDirectoryForItem(item);
assert.isTrue(yield OS.File.exists(dir));
});
});
}) })

View File

@ -218,24 +218,6 @@ describe("Zotero.Sync.Storage.Local", function () {
}) })
}) })
describe("#_deleteExistingAttachmentFiles()", function () {
it("should delete all files", function* () {
var item = yield importFileAttachment('test.html');
var path = OS.Path.dirname(item.getFilePath());
var files = ['a', 'b', 'c', 'd'];
for (let file of files) {
yield Zotero.File.putContentsAsync(OS.Path.join(path, file), file);
}
yield Zotero.Sync.Storage.Local._deleteExistingAttachmentFiles(item);
for (let file of files) {
assert.isFalse(
(yield OS.File.exists(OS.Path.join(path, file))),
`File '${file}' doesn't exist`
);
}
})
})
describe("#getConflicts()", function () { describe("#getConflicts()", function () {
it("should return an array of objects for attachments in conflict", function* () { it("should return an array of objects for attachments in conflict", function* () {
var libraryID = Zotero.Libraries.userLibraryID; var libraryID = Zotero.Libraries.userLibraryID;