File sync: Rename single HTML file within multi-file ZIP to known name

If a file was renamed remotely and a new copy wasn't uploaded for some
reason, the ZIP wouldn't contain the new filename. We already renamed a
single file within the ZIP to match the new filename, but now we also
rename a single HTML file in old multi-file snapshot ZIPs. If there are
multiple HTML files for some reason (old-style ZIP with iframes?), we
let the user fix it.

And then we can stop reuploading files after renames.
This commit is contained in:
Dan Stillman 2025-08-28 02:22:57 -04:00
parent 33bd1327a0
commit bab0237727
2 changed files with 90 additions and 6 deletions

View file

@ -622,11 +622,11 @@ Zotero.Sync.Storage.Local = {
}
if (!path) {
// This can happen if an HTML snapshot filename was changed and synced
// elsewhere but the renamed file wasn't synced, so the ZIP doesn't
// contain a file with the known name
Components.utils.reportError("File '" + item.attachmentFilename
+ "' not found after processing download " + item.libraryKey);
// This generally shouldn't happen, since if the ZIP doesn't contain the primary file,
// and there's only one HTML file within it, we rename it to the current filename, but
// it could occur if there are multiple HTML files or there's an error renaming the file.
Zotero.logError("File '" + item.attachmentFilename + "' not found after processing "
+ "download " + item.libraryKey);
return new Zotero.Sync.Storage.Result({
localChanges: false
});
@ -795,6 +795,7 @@ Zotero.Sync.Storage.Local = {
var itemFileName = item.attachmentFilename;
var createdFiles = new Set();
var entries = zipReader.findEntries(null);
while (entries.hasMore()) {
var entryName = entries.getNext();
@ -835,7 +836,7 @@ Zotero.Sync.Storage.Local = {
filtered = true;
}
var destPath = OS.Path.join(parentDir, ...filePath.split('/'));
let destPath = OS.Path.join(parentDir, ...filePath.split('/'));
// If only one file in zip and it doesn't match the known filename,
// take our chances and use that name
@ -910,6 +911,7 @@ Zotero.Sync.Storage.Local = {
try {
zipReader.extract(entryName, Zotero.File.pathToFile(destPath));
createdFiles.add(PathUtils.filename(destPath));
}
catch (e) {
try {
@ -968,6 +970,23 @@ Zotero.Sync.Storage.Local = {
zipFile.remove(false);
}
// If multiple files and none match known filename, but there's only one HTML file, rename it
if (!createdFiles.has(itemFileName)) {
Zotero.debug(`${itemFileName} not found among extracted files`);
let htmlFiles = [...createdFiles].filter(x => /\.html?$/.test(x));
if (htmlFiles.length == 1) {
let destPath = PathUtils.join(parentDir, itemFileName);
try {
Zotero.debug(`Renaming ${htmlFiles[0]} to ${itemFileName}`);
await IOUtils.move(PathUtils.join(parentDir, htmlFiles[0]), destPath);
returnFile = destPath;
}
catch (e) {
Zotero.logError(e);
}
}
}
return returnFile;
},

View file

@ -549,6 +549,71 @@ describe("Zotero.Sync.Storage.Local", function () {
);
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);
});
});
})