From 11eb1d3bb380654bda14858aa1e1d0705eb3c2a0 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Sun, 19 Jul 2026 18:57:25 +0700 Subject: [PATCH 1/2] Fix export sync endpoint in case of exporting several spaces Signed-off-by: Artyom Savchenko --- services/export/pod-export/src/server.ts | 27 ++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/services/export/pod-export/src/server.ts b/services/export/pod-export/src/server.ts index 56dc5b004f6..46b1191e7d9 100644 --- a/services/export/pod-export/src/server.ts +++ b/services/export/pod-export/src/server.ts @@ -399,6 +399,7 @@ export function createServer ( const txOperations = new TxOperations(platformClient, socialId) const exportDir = await fs.mkdtemp(join(tmpdir(), 'export-')) + let archiveDir: string | undefined try { const exporter = new WorkspaceExporter(measureCtx, txOperations, storageAdapter, wsIds, config) await exporter.export(_class, exportDir, { format, attributesOnly: attributesOnly ?? false, query }) @@ -408,17 +409,35 @@ export function createServer ( throw new ApiError(400, 'No data to export') } - if (files.length !== 1) { - throw new ApiError(400, 'Unexpected number of files exported') + let exportedFile: string + if (files.length === 1) { + // Single space exported: return its file directly. + exportedFile = join(exportDir, files[0]) + } else { + // Pack all spaces into a single archive so the sync endpoint can still return exactly one downloadable file. + archiveDir = await fs.mkdtemp(join(tmpdir(), 'export-archive-')) + const archiveName = `export-${wsIds.uuid}-${format}-${Date.now()}.zip` + exportedFile = join(archiveDir, archiveName) + await saveToArchive(exportDir, exportedFile) } - const exportedFile = join(exportDir, files[0]) - res.download(exportedFile, basename(exportedFile), () => {}) + await new Promise((resolve, reject) => { + res.download(exportedFile, basename(exportedFile), (err) => { + if (err != null && !res.headersSent) { + reject(err) + } else { + resolve() + } + }) + }) } catch (err: any) { measureCtx.error('Export failed:', err) throw err } finally { void fs.rm(exportDir, { recursive: true, force: true }) + if (archiveDir !== undefined) { + void fs.rm(archiveDir, { recursive: true, force: true }) + } } }) ) From 7fe2817e9f1b809d54ab1ec8272ec974aa78dcb4 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Sun, 19 Jul 2026 19:53:18 +0700 Subject: [PATCH 2/2] Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Artyom Savchenko --- services/export/pod-export/src/server.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/export/pod-export/src/server.ts b/services/export/pod-export/src/server.ts index 46b1191e7d9..48583268765 100644 --- a/services/export/pod-export/src/server.ts +++ b/services/export/pod-export/src/server.ts @@ -264,6 +264,15 @@ function parseExportFormat (rawFormat: unknown): ExportFormat { return rawFormat as ExportFormat } +function toSafeFormatFileToken (format: ExportFormat): 'json' | 'csv' { + switch (format) { + case ExportFormat.JSON: + return 'json' + case ExportFormat.CSV: + return 'csv' + } +} + export function createServer ( storageConfig: StorageConfiguration, dbUrl: string, @@ -416,7 +425,8 @@ export function createServer ( } else { // Pack all spaces into a single archive so the sync endpoint can still return exactly one downloadable file. archiveDir = await fs.mkdtemp(join(tmpdir(), 'export-archive-')) - const archiveName = `export-${wsIds.uuid}-${format}-${Date.now()}.zip` + const safeFormatToken = toSafeFormatFileToken(format) + const archiveName = `export-${wsIds.uuid}-${safeFormatToken}-${Date.now()}.zip` exportedFile = join(archiveDir, archiveName) await saveToArchive(exportDir, exportedFile) }