From 229f01513c6b345fefb5daf3853fe2bc5c9e6388 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 12 Feb 2022 13:35:57 -0800 Subject: [PATCH] fix: Export includes documents that have identical names (#3098) --- server/utils/zip.ts | 68 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/server/utils/zip.ts b/server/utils/zip.ts index 57ce24722..c2126df31 100644 --- a/server/utils/zip.ts +++ b/server/utils/zip.ts @@ -23,7 +23,10 @@ export type Item = { item: JSZipObject; }; -async function addToArchive(zip: JSZip, documents: NavigationNode[]) { +async function addDocumentTreeToArchive( + zip: JSZip, + documents: NavigationNode[] +) { for (const doc of documents) { const document = await Document.findByPk(doc.id); @@ -44,8 +47,9 @@ async function addToArchive(zip: JSZip, documents: NavigationNode[]) { text = text.replace(attachment.redirectUrl, encodeURI(attachment.key)); } - const title = serializeFilename(document.title) || "Untitled"; - zip.file(`${title}.md`, text, { + let title = serializeFilename(document.title) || "Untitled"; + + title = safeAddFileToArchive(zip, `${title}.md`, text, { date: document.updatedAt, comment: JSON.stringify({ createdAt: document.createdAt, @@ -54,15 +58,21 @@ async function addToArchive(zip: JSZip, documents: NavigationNode[]) { }); if (doc.children && doc.children.length) { - const folder = zip.folder(title); + const folder = zip.folder(path.parse(title).name); if (folder) { - await addToArchive(folder, doc.children); + await addDocumentTreeToArchive(folder, doc.children); } } } } +/** + * Adds the content of a file in remote storage to the given zip file. + * + * @param zip JSZip object to add to + * @param key path to file in S3 storage + */ async function addImageToArchive(zip: JSZip, key: string) { try { const img = await getFileByKey(key); @@ -78,6 +88,52 @@ async function addImageToArchive(zip: JSZip, key: string) { } } +/** + * Adds content to a zip file, if the given filename already exists in the zip + * then it will automatically increment numbers at the end of the filename. + * + * @param zip JSZip object to add to + * @param key filename with extension + * @param content the content to add + * @param options options for added content + * @returns The new title + */ +function safeAddFileToArchive( + zip: JSZip, + key: string, + content: string | Uint8Array | ArrayBuffer | Blob, + options: JSZip.JSZipFileOptions +) { + // @ts-expect-error root exists + const root = zip.root; + + // Filenames in the directory already + const keysInDirectory = Object.keys(zip.files) + .filter((k) => k.includes(root)) + .filter((k) => !k.endsWith("/")) + .map((k) => path.basename(k).replace(/\s\((\d+)\)\./, ".")); + + // The number of duplicate filenames + const existingKeysCount = keysInDirectory.filter((t) => t === key).length; + const filename = path.parse(key).name; + const extension = path.extname(key); + + // Construct the new de-duplicated filename (if any) + const safeKey = + existingKeysCount > 0 + ? `${filename} (${existingKeysCount})${extension}` + : key; + + zip.file(safeKey, content, options); + return safeKey; +} + +/** + * Write a zip file to a temporary disk location + * + * @param zip JSZip object + * @returns pathname of the temporary file where the zip was written to disk + */ async function archiveToPath(zip: JSZip) { return new Promise((resolve, reject) => { tmp.file( @@ -110,7 +166,7 @@ export async function archiveCollections(collections: Collection[]) { const folder = zip.folder(collection.name); if (folder) { - await addToArchive(folder, collection.documentStructure); + await addDocumentTreeToArchive(folder, collection.documentStructure); } } }