commit 9e955bde9931f50065e7f2e61ecbac7d7fdac438
parent bc141ce36b771da11de28b8e46b77dd128acc3dc
Author: Dan Stillman <dstillman@zotero.org>
Date: Sat, 31 Mar 2018 08:03:28 -0400
Add Zotero.Item.prototype.moveToLibrary()
Move an item and its attachments to another library. Attachments are
removed as necessary if linked files or all files aren't supported in
the target library.
Diffstat:
3 files changed, 209 insertions(+), 4 deletions(-)
diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js
@@ -1115,13 +1115,75 @@ Zotero.Attachments = new function(){
/**
- * Copy attachment item, including files, to another library
+ * Move attachment item, including file, to another library
*/
- this.copyAttachmentToLibrary = Zotero.Promise.coroutine(function* (attachment, libraryID, parentItemID) {
- var linkMode = attachment.attachmentLinkMode;
+ this.moveAttachmentToLibrary = async function (attachment, libraryID, parentItemID) {
+ if (attachment.libraryID == libraryID) {
+ throw new Error("Attachment is already in library " + libraryID);
+ }
+
+ Zotero.DB.requireTransaction();
+
+ var newAttachment = attachment.clone(libraryID);
+ if (attachment.isImportedAttachment()) {
+ // Attachment path isn't copied over by clone() if libraryID is different
+ newAttachment.attachmentPath = attachment.attachmentPath;
+ }
+ if (parentItemID) {
+ newAttachment.parentID = parentItemID;
+ }
+ await newAttachment.save();
+
+ // Move files over if they exist
+ var oldDir;
+ var newDir;
+ if (newAttachment.isImportedAttachment()) {
+ oldDir = this.getStorageDirectory(attachment).path;
+ if (await OS.File.exists(oldDir)) {
+ newDir = this.getStorageDirectory(newAttachment).path;
+ // Target directory shouldn't exist, but remove it if it does
+ //
+ // Testing for directories in OS.File, used by removeDir(), is broken on Travis,
+ // so use nsIFile
+ if (Zotero.automatedTest) {
+ let nsIFile = Zotero.File.pathToFile(newDir);
+ if (nsIFile.exists()) {
+ nsIFile.remove(true);
+ }
+ }
+ else {
+ await OS.File.removeDir(newDir, { ignoreAbsent: true });
+ }
+ await OS.File.move(oldDir, newDir);
+ }
+ }
+ try {
+ await attachment.erase();
+ }
+ catch (e) {
+ // Move files back if old item can't be deleted
+ if (newAttachment.isImportedAttachment()) {
+ try {
+ await OS.File.move(newDir, oldDir);
+ }
+ catch (e) {
+ Zotero.logError(e);
+ }
+ }
+ throw e;
+ }
+
+ return newAttachment.id;
+ };
+
+
+ /**
+ * Copy attachment item, including file, to another library
+ */
+ this.copyAttachmentToLibrary = Zotero.Promise.coroutine(function* (attachment, libraryID, parentItemID) {
if (attachment.libraryID == libraryID) {
- throw ("Attachment is already in library " + libraryID);
+ throw new Error("Attachment is already in library " + libraryID);
}
Zotero.DB.requireTransaction();
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
@@ -3989,6 +3989,89 @@ Zotero.Item.prototype.clone = function (libraryID, options = {}) {
}
+/**
+ * @param {Zotero.Item} item
+ * @param {Integer} libraryID
+ * @return {Zotero.Item} - New item
+ */
+Zotero.Item.prototype.moveToLibrary = async function (libraryID, onSkippedAttachment) {
+ if (!this.isEditable) {
+ throw new Error("Can't move item in read-only library");
+ }
+ var library = Zotero.Libraries.get(libraryID);
+ Zotero.debug("Moving item to " + library.name);
+ if (!library.editable) {
+ throw new Error("Can't move item to read-only library");
+ }
+ var filesEditable = library.filesEditable;
+ var allowsLinkedFiles = library.allowsLinkedFiles;
+
+ var newItem = await Zotero.DB.executeTransaction(async function () {
+ // Create new clone item in target library
+ var newItem = this.clone(libraryID);
+ var newItemID = await newItem.save({
+ skipSelect: true
+ });
+
+ if (this.isNote()) {
+ // Delete old item
+ await this.erase();
+ return newItem;
+ }
+
+ // For regular items, add child items
+
+ // Child notes
+ var noteIDs = this.getNotes();
+ var notes = Zotero.Items.get(noteIDs);
+ for (let note of notes) {
+ let newNote = note.clone(libraryID);
+ newNote.parentID = newItemID;
+ await newNote.save({
+ skipSelect: true
+ });
+ }
+
+ // Child attachments
+ var attachmentIDs = this.getAttachments();
+ var attachments = Zotero.Items.get(attachmentIDs);
+ for (let attachment of attachments) {
+ let linkMode = attachment.attachmentLinkMode;
+
+ // Skip linked files if not allowed in destination
+ if (!allowsLinkedFiles && linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
+ Zotero.debug("Target library doesn't support linked files -- skipping attachment");
+ if (onSkippedAttachment) {
+ await onSkippedAttachment(attachment);
+ }
+ continue;
+ }
+
+ // Skip files if not allowed in destination
+ if (!filesEditable && linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
+ Zotero.debug("Target library doesn't allow file editing -- skipping attachment");
+ if (onSkippedAttachment) {
+ await onSkippedAttachment(attachment);
+ }
+ continue;
+ }
+
+ await Zotero.Attachments.moveAttachmentToLibrary(
+ attachment, libraryID, newItemID
+ );
+ }
+
+ return newItem;
+ }.bind(this));
+
+ // Delete old item. Do this outside of a transaction so we don't leave stranded files
+ // in the target library if deleting fails.
+ await this.eraseTx();
+
+ return newItem;
+};
+
+
Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
Zotero.DB.requireTransaction();
diff --git a/test/tests/itemTest.js b/test/tests/itemTest.js
@@ -1258,6 +1258,66 @@ describe("Zotero.Item", function () {
})
})
+ describe("#moveToLibrary()", function () {
+ it("should move items from My Library to a filesEditable group", async function () {
+ var group = await createGroup();
+
+ var item = await createDataObject('item');
+ var attachment1 = await importFileAttachment('test.png', { parentID: item.id });
+ var file = getTestDataDirectory();
+ file.append('test.png');
+ var attachment2 = await Zotero.Attachments.linkFromFile({
+ file,
+ parentItemID: item.id
+ });
+ var note = await createDataObject('item', { itemType: 'note', parentID: item.id });
+
+ var originalIDs = [item.id, attachment1.id, attachment2.id, note.id];
+ var originalAttachmentFile = attachment1.getFilePath();
+ var originalAttachmentHash = await attachment1.attachmentHash
+
+ assert.isTrue(await OS.File.exists(originalAttachmentFile));
+
+ var newItem = await item.moveToLibrary(group.libraryID);
+
+ // Old items and file should be gone
+ assert.isTrue(originalIDs.every(id => !Zotero.Items.get(id)));
+ assert.isFalse(await OS.File.exists(originalAttachmentFile));
+
+ // New items and stored file should exist; linked file should be gone
+ assert.equal(newItem.libraryID, group.libraryID);
+ assert.lengthOf(newItem.getAttachments(), 1);
+ var newAttachment = Zotero.Items.get(newItem.getAttachments()[0]);
+ assert.equal(await newAttachment.attachmentHash, originalAttachmentHash);
+ assert.lengthOf(newItem.getNotes(), 1);
+ });
+
+ it("should move items from My Library to a non-filesEditable group", async function () {
+ var group = await createGroup({
+ filesEditable: false
+ });
+
+ var item = await createDataObject('item');
+ var attachment = await importFileAttachment('test.png', { parentID: item.id });
+
+ var originalIDs = [item.id, attachment.id];
+ var originalAttachmentFile = attachment.getFilePath();
+ var originalAttachmentHash = await attachment.attachmentHash
+
+ assert.isTrue(await OS.File.exists(originalAttachmentFile));
+
+ var newItem = await item.moveToLibrary(group.libraryID);
+
+ // Old items and file should be gone
+ assert.isTrue(originalIDs.every(id => !Zotero.Items.get(id)));
+ assert.isFalse(await OS.File.exists(originalAttachmentFile));
+
+ // Parent should exist, but attachment should not
+ assert.equal(newItem.libraryID, group.libraryID);
+ assert.lengthOf(newItem.getAttachments(), 0);
+ });
+ });
+
describe("#toJSON()", function () {
describe("default mode", function () {
it("should output only fields with values", function* () {