fix: Duplicate paths in export ZIP (#12674)

This commit is contained in:
Tom Moor
2026-06-12 20:04:18 -04:00
committed by GitHub
parent 9113501906
commit 394c6e3b03
2 changed files with 108 additions and 31 deletions
+47 -31
View File
@@ -150,26 +150,12 @@ export default abstract class ExportDocumentTreeTask extends ExportTask {
includeAttachments = true includeAttachments = true
) { ) {
const pathMap = this.createPathMap(collections, format); const pathMap = this.createPathMap(collections, format);
Logger.debug( await this.addDocumentsToArchive({
"task", zip,
`Start adding ${Object.values(pathMap).length} documents to archive` pathMap,
); format,
includeAttachments,
for (const path of pathMap) { });
const documentId = path[0].replace("/doc/", "");
const pathInZip = path[1];
await this.processDocument({
zip,
pathInZip,
documentId,
includeAttachments,
format,
pathMap,
});
}
Logger.debug("task", "Completed adding documents to archive");
return await ZipHelper.toTmpFile(zip); return await ZipHelper.toTmpFile(zip);
} }
@@ -200,28 +186,58 @@ export default abstract class ExportDocumentTreeTask extends ExportTask {
format format
); );
Logger.debug( await this.addDocumentsToArchive({
"task", zip,
`Start adding ${Object.values(pathMap).length} documents to archive` pathMap,
); format,
includeAttachments: true,
});
for (const entry of pathMap) { return await ZipHelper.toTmpFile(zip);
const documentId = entry[0].replace("/doc/", ""); }
const pathInZip = entry[1];
/**
* Processes each unique document in the path map and adds it to the zip.
*
* @param zip The yazl ZipFile to add files to
* @param pathMap Map of document urls to their path in the zip
* @param format The format to export in
* @param includeAttachments Whether to include attachments in the export
*/
private async addDocumentsToArchive({
zip,
pathMap,
format,
includeAttachments,
}: {
zip: ZipFile;
pathMap: Map<string, string>;
format: FileOperationFormat;
includeAttachments: boolean;
}) {
const processedPaths = new Set<string>();
Logger.debug("task", `Start adding documents to archive`);
for (const [url, pathInZip] of pathMap) {
// A document may be keyed by multiple urls in the path map, only
// process each file in the zip once.
if (processedPaths.has(pathInZip)) {
continue;
}
processedPaths.add(pathInZip);
await this.processDocument({ await this.processDocument({
zip, zip,
pathInZip, pathInZip,
documentId, documentId: url.replace("/doc/", ""),
includeAttachments: true, includeAttachments,
format, format,
pathMap, pathMap,
}); });
} }
Logger.debug("task", "Completed adding documents to archive"); Logger.debug("task", "Completed adding documents to archive");
return await ZipHelper.toTmpFile(zip);
} }
/** /**
@@ -0,0 +1,61 @@
import fs from "fs-extra";
import ZipHelper from "@server/utils/ZipHelper";
import {
buildCollection,
buildDocument,
buildFileOperation,
buildTeam,
buildUser,
} from "@server/test/factories";
import ExportMarkdownZipTask from "./ExportMarkdownZipTask";
describe("ExportMarkdownZipTask", () => {
it("should not duplicate documents in the zip file", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const collection = await buildCollection({
teamId: team.id,
createdById: user.id,
});
const documents = await Promise.all([
buildDocument({
teamId: team.id,
userId: user.id,
collectionId: collection.id,
title: "Test1",
}),
buildDocument({
teamId: team.id,
userId: user.id,
collectionId: collection.id,
title: "Test2",
}),
]);
for (const document of documents) {
await collection.addDocumentToStructure(document);
}
const fileOperation = await buildFileOperation({
teamId: team.id,
userId: user.id,
});
const task = new ExportMarkdownZipTask();
const filePath = await task.exportCollections([collection], fileOperation);
try {
const fileNames: string[] = [];
await ZipHelper.walk(filePath, (entry) => {
if (!entry.isDirectory) {
fileNames.push(entry.fileName);
}
});
expect(fileNames.sort()).toEqual([
`${collection.name}/Test1.md`,
`${collection.name}/Test2.md`,
]);
} finally {
await fs.remove(filePath);
}
});
});