mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Sincef21e1b2d32, a full local file scan no longer runs periodically and on every manual sync, so locally missed attachments stayed marked for upload and were skipped as unavailable instead of being downloaded. "Reset File Sync History" marked every attachment for upload, including files that had never been downloaded, so the forced download check added in404fc41b88found nothing to download. Missing files are now marked for download when the upload queue is filled, and in at-sync-time mode they're downloaded in the same sync. The reset marks them for download directly, and downloads are no longer skipped just because there were no remote storage changes. https://forums.zotero.org/discussion/133414/
821 lines
29 KiB
JavaScript
821 lines
29 KiB
JavaScript
"use strict";
|
|
|
|
describe("Zotero.Sync.Storage.Local", function () {
|
|
describe("#checkForUpdatedFiles()", function () {
|
|
it("should flag modified file for upload and return it", async function () {
|
|
// Create attachment
|
|
let item = await importTextAttachment();
|
|
var hash = await item.attachmentHash;
|
|
// Set file mtime to the past (without milliseconds, which aren't used on OS X)
|
|
var mtime = (Math.floor(new Date().getTime() / 1000) * 1000) - 1000;
|
|
await OS.File.setDates(((await item.getFilePathAsync())), null, mtime);
|
|
|
|
// Mark as synced, so it will be checked
|
|
item.attachmentSyncedModificationTime = mtime;
|
|
item.attachmentSyncedHash = hash;
|
|
item.attachmentSyncState = "in_sync";
|
|
await item.saveTx({ skipAll: true });
|
|
|
|
// Update mtime and contents
|
|
var path = await item.getFilePathAsync();
|
|
await OS.File.setDates(path);
|
|
await Zotero.File.putContentsAsync(path, Zotero.Utilities.randomString());
|
|
|
|
// File should be returned
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var changed = await Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID, [item.id]);
|
|
|
|
await item.eraseTx();
|
|
|
|
assert.equal(changed, true);
|
|
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_UPLOAD);
|
|
});
|
|
|
|
it("should skip a file if mod time hasn't changed", async function () {
|
|
// Create attachment
|
|
let item = await importTextAttachment();
|
|
var hash = await item.attachmentHash;
|
|
var mtime = await item.attachmentModificationTime;
|
|
|
|
// Mark as synced, so it will be checked
|
|
item.attachmentSyncedModificationTime = mtime;
|
|
item.attachmentSyncedHash = hash;
|
|
item.attachmentSyncState = "in_sync";
|
|
await item.saveTx({ skipAll: true });
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var changed = await Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID, [item.id]);
|
|
var syncState = item.attachmentSyncState;
|
|
|
|
await item.eraseTx();
|
|
|
|
assert.isFalse(changed);
|
|
assert.equal(syncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC);
|
|
})
|
|
|
|
it("should skip a file if mod time has changed but contents haven't", async function () {
|
|
// Create attachment
|
|
let item = await importTextAttachment();
|
|
var hash = await item.attachmentHash;
|
|
// Set file mtime to the past (without milliseconds, which aren't used on OS X)
|
|
var mtime = (Math.floor(new Date().getTime() / 1000) * 1000) - 1000;
|
|
await OS.File.setDates(((await item.getFilePathAsync())), null, mtime);
|
|
|
|
// Mark as synced, so it will be checked
|
|
item.attachmentSyncedModificationTime = mtime;
|
|
item.attachmentSyncedHash = hash;
|
|
item.attachmentSyncState = "in_sync";
|
|
await item.saveTx({ skipAll: true });
|
|
|
|
// Update mtime, but not contents
|
|
var path = await item.getFilePathAsync();
|
|
await OS.File.setDates(path);
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var changed = await Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID, [item.id]);
|
|
var syncState = item.attachmentSyncState;
|
|
var syncedModTime = item.attachmentSyncedModificationTime;
|
|
var newModTime = await item.attachmentModificationTime;
|
|
|
|
await item.eraseTx();
|
|
|
|
assert.isFalse(changed);
|
|
assert.equal(syncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_SYNC);
|
|
assert.equal(syncedModTime, newModTime);
|
|
});
|
|
|
|
it("should flag a missing local file for download if already marked for upload", async function () {
|
|
// Create attachment
|
|
let item = await importFileAttachment('test.png');
|
|
var hash = await item.attachmentHash;
|
|
var mtime = await item.attachmentModificationTime;
|
|
|
|
// Mark as synced, so it will be checked
|
|
item.attachmentSyncedModificationTime = mtime;
|
|
item.attachmentSyncedHash = hash;
|
|
item.attachmentSyncState = "to_upload";
|
|
await item.saveTx({ skipAll: true });
|
|
|
|
// Delete local file
|
|
await IOUtils.remove(item.getFilePath());
|
|
|
|
// File should be marked for download and not returned
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var changed = await Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID, [item.id]);
|
|
|
|
await item.eraseTx();
|
|
|
|
assert.isTrue(changed);
|
|
assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD);
|
|
});
|
|
})
|
|
|
|
describe("#updateSyncStates()", function () {
|
|
it("should update attachment sync states to 'to_upload'", async function () {
|
|
var attachment1 = await importFileAttachment('test.png');
|
|
attachment1.attachmentSyncState = 'in_sync';
|
|
await attachment1.saveTx();
|
|
var attachment2 = await importFileAttachment('test.png');
|
|
attachment2.attachmentSyncState = 'in_sync';
|
|
await attachment2.saveTx();
|
|
|
|
var local = Zotero.Sync.Storage.Local;
|
|
await local.updateSyncStates([attachment1, attachment2], 'to_upload');
|
|
|
|
for (let attachment of [attachment1, attachment2]) {
|
|
assert.strictEqual(attachment.attachmentSyncState, local.SYNC_STATE_TO_UPLOAD);
|
|
let state = await Zotero.DB.valueQueryAsync(
|
|
"SELECT syncState FROM itemAttachments WHERE itemID=?", attachment.id
|
|
);
|
|
assert.strictEqual(state, local.SYNC_STATE_TO_UPLOAD);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("#resetAllSyncStates()", function () {
|
|
it("should reset attachment sync states to 'to_upload'", async function () {
|
|
var attachment = await importFileAttachment('test.png');
|
|
attachment.attachmentSyncState = 'in_sync';
|
|
await attachment.saveTx();
|
|
|
|
var local = Zotero.Sync.Storage.Local;
|
|
await local.resetAllSyncStates(attachment.libraryID)
|
|
assert.strictEqual(attachment.attachmentSyncState, local.SYNC_STATE_TO_UPLOAD);
|
|
var state = await Zotero.DB.valueQueryAsync(
|
|
"SELECT syncState FROM itemAttachments WHERE itemID=?", attachment.id
|
|
);
|
|
assert.strictEqual(state, local.SYNC_STATE_TO_UPLOAD);
|
|
});
|
|
|
|
it("should leave an attachment with a missing file marked for download", async function () {
|
|
var attachment = await importFileAttachment('test.png');
|
|
attachment.attachmentSyncState = 'to_download';
|
|
await attachment.saveTx();
|
|
await IOUtils.remove(attachment.getFilePath());
|
|
|
|
var local = Zotero.Sync.Storage.Local;
|
|
await local.resetAllSyncStates(attachment.libraryID);
|
|
assert.strictEqual(attachment.attachmentSyncState, local.SYNC_STATE_TO_DOWNLOAD);
|
|
var state = await Zotero.DB.valueQueryAsync(
|
|
"SELECT syncState FROM itemAttachments WHERE itemID=?", attachment.id
|
|
);
|
|
assert.strictEqual(state, local.SYNC_STATE_TO_DOWNLOAD);
|
|
});
|
|
});
|
|
|
|
describe("#processDownload()", function () {
|
|
describe("single file", function () {
|
|
it("should download a single file into the attachment directory", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var parentItem = await createDataObject('item');
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
var fileContents = Zotero.Utilities.randomString();
|
|
|
|
var oldFilename = "Old File";
|
|
var tmpDir = Zotero.getTempDirectory().path;
|
|
var tmpFile = OS.Path.join(tmpDir, key + '.tmp');
|
|
await Zotero.File.putContentsAsync(tmpFile, fileContents);
|
|
|
|
// Create an existing attachment directory to replace
|
|
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
|
|
await OS.File.makeDir(
|
|
dir,
|
|
{
|
|
unixMode: 0o755
|
|
}
|
|
);
|
|
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldFilename), '');
|
|
|
|
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(tmpFile));
|
|
var mtime = 1445667239000;
|
|
|
|
var json = {
|
|
key,
|
|
version: 10,
|
|
itemType: 'attachment',
|
|
linkMode: 'imported_url',
|
|
url: 'https://example.com/foo.txt',
|
|
filename: 'foo.txt',
|
|
contentType: 'text/plain',
|
|
charset: 'utf-8',
|
|
md5,
|
|
mtime
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
|
|
|
|
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
|
|
await Zotero.Sync.Storage.Local.processDownload({
|
|
item,
|
|
md5,
|
|
mtime
|
|
});
|
|
await OS.File.remove(tmpFile);
|
|
|
|
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
|
|
|
|
// Make sure previous files don't exist
|
|
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldFilename)));
|
|
|
|
// Make sure main file matches attachment hash and mtime
|
|
assert.equal(
|
|
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
|
|
);
|
|
assert.equal(await item.attachmentModificationTime, mtime);
|
|
});
|
|
|
|
|
|
it("should download and rename a single file with invalid filename into the attachment directory", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var parentItem = await createDataObject('item');
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
var fileContents = Zotero.Utilities.randomString();
|
|
|
|
var oldFilename = "Old File";
|
|
var newFilename = " ab — c :.txt.";
|
|
var filteredFilename = " ab — c .txt.";
|
|
var tmpDir = Zotero.getTempDirectory().path;
|
|
var tmpFile = OS.Path.join(tmpDir, key + '.tmp');
|
|
await Zotero.File.putContentsAsync(tmpFile, fileContents);
|
|
|
|
// Create an existing attachment directory to replace
|
|
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
|
|
await OS.File.makeDir(
|
|
dir,
|
|
{
|
|
unixMode: 0o755
|
|
}
|
|
);
|
|
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldFilename), '');
|
|
|
|
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(tmpFile));
|
|
var mtime = 1445667239000;
|
|
|
|
var json = {
|
|
key,
|
|
version: 10,
|
|
itemType: 'attachment',
|
|
linkMode: 'imported_url',
|
|
url: 'https://example.com/foo.txt',
|
|
filename: newFilename,
|
|
contentType: 'text/plain',
|
|
charset: 'utf-8',
|
|
md5,
|
|
mtime
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
|
|
|
|
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
|
|
await Zotero.Sync.Storage.Local.processDownload({
|
|
item,
|
|
md5,
|
|
mtime
|
|
});
|
|
await OS.File.remove(tmpFile);
|
|
|
|
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
|
|
|
|
// Make sure previous file doesn't exist
|
|
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldFilename)));
|
|
// And new one does
|
|
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, filteredFilename)));
|
|
|
|
// Make sure main file matches attachment hash and mtime
|
|
assert.equal(
|
|
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
|
|
);
|
|
assert.equal(await item.attachmentModificationTime, mtime);
|
|
});
|
|
|
|
|
|
it("should download and rename a single file with invalid filename using Windows parsing rules into the attachment directory", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var parentItem = await createDataObject('item');
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
var fileContents = Zotero.Utilities.randomString();
|
|
|
|
var oldFilename = "Old File";
|
|
var newFilename = "a:b.txt";
|
|
var filteredFilename = "ab.txt";
|
|
var tmpDir = Zotero.getTempDirectory().path;
|
|
var tmpFile = OS.Path.join(tmpDir, key + '.tmp');
|
|
await Zotero.File.putContentsAsync(tmpFile, fileContents);
|
|
|
|
// Create an existing attachment directory to replace
|
|
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
|
|
await OS.File.makeDir(
|
|
dir,
|
|
{
|
|
unixMode: 0o755
|
|
}
|
|
);
|
|
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldFilename), '');
|
|
|
|
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(tmpFile));
|
|
var mtime = 1445667239000;
|
|
|
|
var json = {
|
|
key,
|
|
version: 10,
|
|
itemType: 'attachment',
|
|
linkMode: 'imported_url',
|
|
url: 'https://example.com/foo.txt',
|
|
filename: 'a:b.txt',
|
|
contentType: 'text/plain',
|
|
charset: 'utf-8',
|
|
md5,
|
|
mtime
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
|
|
|
|
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
|
|
|
|
// Stub functions to simulate OS.Path.basename() behavior on Windows
|
|
var basenameOrigFunc = OS.Path.basename.bind(OS.Path);
|
|
var basenameStub = sinon.stub(OS.Path, "basename").callsFake((path) => {
|
|
// Split on colon
|
|
if (path.endsWith("a:b.txt")) {
|
|
return "b.txt";
|
|
}
|
|
return basenameOrigFunc(path);
|
|
});
|
|
var pathToFileOrigFunc = Zotero.File.pathToFile.bind(Zotero.File);
|
|
var pathToFileStub = sinon.stub(Zotero.File, "pathToFile").callsFake((path) => {
|
|
if (path.includes(":")) {
|
|
throw new Error("Path contains colon");
|
|
}
|
|
return pathToFileOrigFunc(path);
|
|
});
|
|
|
|
await Zotero.Sync.Storage.Local.processDownload({
|
|
item,
|
|
md5,
|
|
mtime
|
|
});
|
|
await OS.File.remove(tmpFile);
|
|
|
|
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
|
|
|
|
basenameStub.restore();
|
|
pathToFileStub.restore();
|
|
|
|
// Make sure path is set correctly
|
|
assert.equal(item.getFilePath(), OS.Path.join(storageDir, filteredFilename));
|
|
// Make sure previous files don't exist
|
|
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldFilename)));
|
|
// And new one does
|
|
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, filteredFilename)));
|
|
|
|
// Make sure main file matches attachment hash and mtime
|
|
assert.equal(
|
|
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
|
|
);
|
|
assert.equal(await item.attachmentModificationTime, mtime);
|
|
});
|
|
});
|
|
|
|
describe("ZIP", function () {
|
|
it("should download and extract a ZIP file into the attachment directory", async function () {
|
|
var file1Name = 'index.html';
|
|
var file1Contents = '<html><body>Test</body></html>';
|
|
var file2Name = 'aux1.txt';
|
|
var file2Contents = 'Test 1';
|
|
var subDirName = 'sub';
|
|
var file3Name = 'aux2';
|
|
var file3Contents = 'Test 2';
|
|
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var parentItem = await createDataObject('item');
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
var tmpDir = Zotero.getTempDirectory().path;
|
|
var zipFile = OS.Path.join(tmpDir, key + '.tmp');
|
|
|
|
// Create ZIP file with subdirectory
|
|
var tmpDir = Zotero.getTempDirectory().path;
|
|
var zipDir = await getTempDirectory();
|
|
await Zotero.File.putContentsAsync(OS.Path.join(zipDir, file1Name), file1Contents);
|
|
await Zotero.File.putContentsAsync(OS.Path.join(zipDir, file2Name), file2Contents);
|
|
var subDir = OS.Path.join(zipDir, subDirName);
|
|
await OS.File.makeDir(subDir);
|
|
await Zotero.File.putContentsAsync(OS.Path.join(subDir, file3Name), file3Contents);
|
|
await Zotero.File.zipDirectory(zipDir, zipFile);
|
|
await removeDir(zipDir);
|
|
|
|
// Create an existing attachment directory (and subdirectory) to replace
|
|
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
|
|
await OS.File.makeDir(
|
|
OS.Path.join(dir, 'subdir'),
|
|
{
|
|
from: Zotero.DataDirectory.dir,
|
|
unixMode: 0o755
|
|
}
|
|
);
|
|
await Zotero.File.putContentsAsync(OS.Path.join(dir, 'A'), '');
|
|
await Zotero.File.putContentsAsync(OS.Path.join(dir, 'subdir', 'B'), '');
|
|
|
|
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(zipFile));
|
|
var mtime = 1445667239000;
|
|
|
|
var json = {
|
|
key,
|
|
version: 10,
|
|
itemType: 'attachment',
|
|
linkMode: 'imported_url',
|
|
url: 'https://example.com',
|
|
filename: file1Name,
|
|
contentType: 'text/html',
|
|
charset: 'utf-8',
|
|
md5,
|
|
mtime
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
|
|
|
|
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
|
|
await Zotero.Sync.Storage.Local.processDownload({
|
|
item,
|
|
md5,
|
|
mtime,
|
|
compressed: true
|
|
});
|
|
await OS.File.remove(zipFile);
|
|
|
|
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
|
|
|
|
// Make sure previous files don't exist
|
|
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, 'A')));
|
|
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, 'subdir')));
|
|
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, 'subdir', 'B')));
|
|
|
|
// Make sure main file matches attachment hash and mtime
|
|
assert.equal(
|
|
await item.attachmentHash, Zotero.Utilities.Internal.md5(file1Contents)
|
|
);
|
|
assert.equal(await item.attachmentModificationTime, mtime);
|
|
|
|
// Check second file
|
|
assert.equal(
|
|
await Zotero.File.getContentsAsync(OS.Path.join(storageDir, file2Name)),
|
|
file2Contents
|
|
);
|
|
|
|
// Check subdirectory and file
|
|
assert.isTrue(((await OS.File.stat(OS.Path.join(storageDir, subDirName)))).isDir);
|
|
assert.equal(
|
|
await Zotero.File.getContentsAsync(OS.Path.join(storageDir, subDirName, file3Name)),
|
|
file3Contents
|
|
);
|
|
});
|
|
|
|
|
|
it("should download and rename a ZIP file with invalid filename using Windows parsing rules into the attachment directory", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var parentItem = await createDataObject('item');
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
var oldFilename = "Old File";
|
|
var oldAuxFilename = "a.gif";
|
|
var newFilename = "a:b.html";
|
|
var fileContents = Zotero.Utilities.randomString();
|
|
var newAuxFilename = "b.gif";
|
|
var filteredFilename = "ab.html";
|
|
var tmpDir = Zotero.getTempDirectory().path;
|
|
var zipFile = OS.Path.join(tmpDir, key + '.tmp');
|
|
|
|
// Create ZIP file
|
|
var tmpDir = Zotero.getTempDirectory().path;
|
|
var zipDir = await getTempDirectory();
|
|
await Zotero.File.putContentsAsync(OS.Path.join(zipDir, newFilename), fileContents);
|
|
await Zotero.File.putContentsAsync(OS.Path.join(zipDir, newAuxFilename), '');
|
|
await Zotero.File.zipDirectory(zipDir, zipFile);
|
|
await removeDir(zipDir);
|
|
|
|
// Create an existing attachment directory to replace
|
|
var dir = Zotero.Attachments.getStorageDirectoryByLibraryAndKey(libraryID, key).path;
|
|
await OS.File.makeDir(
|
|
dir,
|
|
{
|
|
unixMode: 0o755
|
|
}
|
|
);
|
|
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldFilename), '');
|
|
await Zotero.File.putContentsAsync(OS.Path.join(dir, oldAuxFilename), '');
|
|
|
|
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(zipFile));
|
|
var mtime = 1445667239000;
|
|
|
|
var json = {
|
|
key,
|
|
version: 10,
|
|
itemType: 'attachment',
|
|
linkMode: 'imported_url',
|
|
url: 'https://example.com/foo.html',
|
|
filename: 'a:b.html',
|
|
contentType: 'text/plain',
|
|
charset: 'utf-8',
|
|
md5,
|
|
mtime
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
|
|
|
|
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
|
|
|
|
// Stub functions to simulate OS.Path.basename() behavior on Windows
|
|
var basenameOrigFunc = OS.Path.basename.bind(OS.Path);
|
|
var basenameStub = sinon.stub(OS.Path, "basename").callsFake((path) => {
|
|
// Split on colon
|
|
if (path.endsWith("a:b.html")) {
|
|
return "b.html";
|
|
}
|
|
return basenameOrigFunc(path);
|
|
});
|
|
var pathToFileOrigFunc = Zotero.File.pathToFile.bind(Zotero.File);
|
|
var pathToFileStub = sinon.stub(Zotero.File, "pathToFile").callsFake((path) => {
|
|
if (path.includes(":")) {
|
|
throw new Error("Path contains colon");
|
|
}
|
|
return pathToFileOrigFunc(path);
|
|
});
|
|
|
|
await Zotero.Sync.Storage.Local.processDownload({
|
|
item,
|
|
md5,
|
|
mtime,
|
|
compressed: true
|
|
});
|
|
await OS.File.remove(zipFile);
|
|
|
|
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
|
|
|
|
basenameStub.restore();
|
|
pathToFileStub.restore();
|
|
|
|
// Make sure path is set correctly
|
|
assert.equal(item.getFilePath(), OS.Path.join(storageDir, filteredFilename));
|
|
// Make sure previous files don't exist
|
|
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldFilename)));
|
|
assert.isFalse(await OS.File.exists(OS.Path.join(storageDir, oldAuxFilename)));
|
|
// And new ones do
|
|
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, filteredFilename)));
|
|
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, newAuxFilename)));
|
|
|
|
// Make sure main file matches attachment hash and mtime
|
|
assert.equal(
|
|
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
|
|
);
|
|
assert.equal(await item.attachmentModificationTime, mtime);
|
|
});
|
|
|
|
|
|
it("should rename a single HTML file in an old multi-file ZIP to match the current primary filename", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
var parentItem = await createDataObject('item');
|
|
var key = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
var oldFilename = "a.html";
|
|
var auxFilename = "a.gif";
|
|
var newFilename = "b.html";
|
|
var fileContents = Zotero.Utilities.randomString();
|
|
var tmpDir = Zotero.getTempDirectory().path;
|
|
var zipFile = OS.Path.join(tmpDir, key + '.tmp');
|
|
|
|
// Create ZIP file
|
|
var tmpDir = Zotero.getTempDirectory().path;
|
|
var zipDir = await getTempDirectory();
|
|
await Zotero.File.putContentsAsync(PathUtils.join(zipDir, oldFilename), fileContents);
|
|
await Zotero.File.putContentsAsync(PathUtils.join(zipDir, auxFilename), '');
|
|
await Zotero.File.zipDirectory(zipDir, zipFile);
|
|
await removeDir(zipDir);
|
|
|
|
var md5 = Zotero.Utilities.Internal.md5(Zotero.File.pathToFile(zipFile));
|
|
var mtime = 1445667239000;
|
|
|
|
var json = {
|
|
key,
|
|
version: 10,
|
|
itemType: 'attachment',
|
|
linkMode: 'imported_url',
|
|
url: 'https://example.com/foo.html',
|
|
filename: 'b.html',
|
|
contentType: 'text/plain',
|
|
charset: 'utf-8',
|
|
md5,
|
|
mtime
|
|
};
|
|
await Zotero.Sync.Data.Local.processObjectsFromJSON('item', libraryID, [json]);
|
|
|
|
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
|
|
|
|
await Zotero.Sync.Storage.Local.processDownload({
|
|
item,
|
|
md5,
|
|
mtime,
|
|
compressed: true
|
|
});
|
|
await OS.File.remove(zipFile);
|
|
|
|
var storageDir = Zotero.Attachments.getStorageDirectory(item).path;
|
|
|
|
// Make sure path is set correctly
|
|
assert.equal(item.getFilePath(), PathUtils.join(storageDir, newFilename));
|
|
// Make sure previous file doesn't exist
|
|
assert.isFalse(await IOUtils.exists(PathUtils.join(storageDir, oldFilename)));
|
|
// And new ones do
|
|
assert.isTrue(await IOUtils.exists(PathUtils.join(storageDir, newFilename)));
|
|
assert.isTrue(await IOUtils.exists(PathUtils.join(storageDir, auxFilename)));
|
|
|
|
// Make sure main file matches attachment hash and mtime
|
|
assert.equal(
|
|
await item.attachmentHash, Zotero.Utilities.Internal.md5(fileContents)
|
|
);
|
|
assert.equal(await item.attachmentModificationTime, mtime);
|
|
});
|
|
});
|
|
})
|
|
|
|
describe("#getConflicts()", function () {
|
|
it("should return an array of objects for attachments in conflict", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var item1 = await importFileAttachment('test.png');
|
|
item1.version = 10;
|
|
await item1.saveTx();
|
|
var item2 = await importTextAttachment();
|
|
var item3 = await importHTMLAttachment();
|
|
item3.version = 11;
|
|
await item3.saveTx();
|
|
|
|
var json1 = item1.toJSON();
|
|
var json3 = item3.toJSON();
|
|
// Change remote mtimes
|
|
// Round to nearest second because OS X doesn't support ms resolution
|
|
var now = Math.round(new Date().getTime() / 1000) * 1000;
|
|
json1.mtime = now - 10000;
|
|
json3.mtime = now - 20000;
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1, json3]);
|
|
|
|
item1.attachmentSyncState = "in_conflict";
|
|
await item1.saveTx({ skipAll: true });
|
|
item3.attachmentSyncState = "in_conflict";
|
|
await item3.saveTx({ skipAll: true });
|
|
|
|
var conflicts = await Zotero.Sync.Storage.Local.getConflicts(libraryID);
|
|
assert.lengthOf(conflicts, 2);
|
|
|
|
var item1Conflict = conflicts.find(x => x.left.key == item1.key);
|
|
assert.equal(
|
|
item1Conflict.left.dateModified,
|
|
Zotero.Date.dateToISO(new Date(await item1.attachmentModificationTime))
|
|
);
|
|
assert.equal(
|
|
item1Conflict.right.dateModified,
|
|
Zotero.Date.dateToISO(new Date(json1.mtime))
|
|
);
|
|
|
|
var item3Conflict = conflicts.find(x => x.left.key == item3.key);
|
|
assert.equal(
|
|
item3Conflict.left.dateModified,
|
|
Zotero.Date.dateToISO(new Date(await item3.attachmentModificationTime))
|
|
);
|
|
assert.equal(
|
|
item3Conflict.right.dateModified,
|
|
Zotero.Date.dateToISO(new Date(json3.mtime))
|
|
);
|
|
})
|
|
})
|
|
|
|
describe("#resolveConflicts()", function () {
|
|
var win;
|
|
|
|
before(async function () {
|
|
await resetDB({
|
|
thisArg: this,
|
|
skipBundledFiles: true
|
|
});
|
|
|
|
win = await loadZoteroWindow();
|
|
});
|
|
|
|
after(function () {
|
|
if (win) {
|
|
win.close();
|
|
}
|
|
});
|
|
|
|
|
|
it("should show the conflict resolution window on attachment conflicts", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var item1 = await importFileAttachment('test.png');
|
|
item1.version = 10;
|
|
await item1.saveTx();
|
|
var item2 = await importTextAttachment();
|
|
var item3 = await importHTMLAttachment();
|
|
item3.version = 11;
|
|
await item3.saveTx();
|
|
|
|
var json1 = item1.toJSON();
|
|
var json3 = item3.toJSON();
|
|
// Change remote mtimes and hashes
|
|
json1.mtime = new Date().getTime() + 10000;
|
|
json1.md5 = 'f4ce1167f3a854896c257a0cc1ac387f';
|
|
json3.mtime = new Date().getTime() - 10000;
|
|
json3.md5 = 'fcd080b1c2cad562237823ec27671bbd';
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1, json3]);
|
|
|
|
item1.attachmentSyncState = "in_conflict";
|
|
await item1.saveTx({ skipAll: true });
|
|
item3.attachmentSyncState = "in_conflict";
|
|
await item3.saveTx({ skipAll: true });
|
|
|
|
var promise = waitForWindow('chrome://zotero/content/merge.xhtml', async function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
|
|
// 1 (remote)
|
|
// Later remote version should be selected
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
|
|
// Check checkbox text
|
|
assert.equal(
|
|
doc.getElementById('resolve-all').label,
|
|
Zotero.getString('sync.conflict.resolveAllRemote')
|
|
);
|
|
|
|
// Select local object
|
|
mergeGroup.leftPane.click();
|
|
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
|
|
|
|
wizard.getButton('next').click();
|
|
|
|
// 2 (local)
|
|
// Later local version should be selected
|
|
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
|
|
// Select remote object
|
|
mergeGroup.rightPane.click();
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
|
|
if (Zotero.isMac) {
|
|
assert.isTrue(wizard.getButton('next').hidden);
|
|
assert.isFalse(wizard.getButton('finish').hidden);
|
|
}
|
|
else {
|
|
// TODO
|
|
}
|
|
wizard.getButton('finish').click();
|
|
})
|
|
await Zotero.Sync.Storage.Local.resolveConflicts(libraryID);
|
|
await promise;
|
|
|
|
assert.equal(item1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD);
|
|
assert.equal(item1.attachmentSyncedModificationTime, json1.mtime);
|
|
assert.equal(item1.attachmentSyncedHash, json1.md5);
|
|
assert.equal(item3.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_DOWNLOAD);
|
|
assert.isNull(item3.attachmentSyncedModificationTime);
|
|
assert.isNull(item3.attachmentSyncedHash);
|
|
});
|
|
|
|
it("should handle attachment conflicts with no remote mtime/md5", async function () {
|
|
var libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
var item1 = await importFileAttachment('test.png');
|
|
item1.version = 10;
|
|
await item1.saveTx();
|
|
|
|
var json1 = item1.toJSON();
|
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1]);
|
|
|
|
item1.attachmentSyncState = "in_conflict";
|
|
await item1.saveTx({ skipAll: true });
|
|
|
|
var promise = waitForWindow('chrome://zotero/content/merge.xhtml', async function (dialog) {
|
|
var doc = dialog.document;
|
|
var wizard = doc.querySelector('wizard');
|
|
var mergeGroup = wizard.getElementsByTagName('merge-group')[0];
|
|
|
|
// 1 (remote)
|
|
// Identical, so remote version should be selected
|
|
assert.equal(mergeGroup.rightPane.getAttribute('selected'), 'true');
|
|
|
|
// Select local object
|
|
mergeGroup.leftPane.click();
|
|
assert.equal(mergeGroup.leftPane.getAttribute('selected'), 'true');
|
|
|
|
if (Zotero.isMac) {
|
|
assert.isTrue(wizard.getButton('next').hidden);
|
|
assert.isFalse(wizard.getButton('finish').hidden);
|
|
}
|
|
else {
|
|
// TODO
|
|
}
|
|
wizard.getButton('finish').click();
|
|
});
|
|
await Zotero.Sync.Storage.Local.resolveConflicts(libraryID);
|
|
await promise;
|
|
|
|
assert.equal(item1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD);
|
|
assert.isNull(item1.attachmentSyncedModificationTime);
|
|
assert.isNull(item1.attachmentSyncedHash);
|
|
});
|
|
})
|
|
})
|