Skip to content

Commit b29fe18

Browse files
committed
fix(files): fix race condition in asset filename deduplication
1 parent 4f60de4 commit b29fe18

1 file changed

Lines changed: 32 additions & 20 deletions

File tree

  • apps/sim/app/api/files/export/[id]

apps/sim/app/api/files/export/[id]/route.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -87,30 +87,42 @@ export const GET = withRouteHandler(
8787

8888
logger.info('Exporting markdown with embedded images', { id, imageCount: imageIds.length })
8989

90-
const assetMap = new Map<string, { filename: string; buffer: Buffer }>()
91-
const usedFilenames = new Set<string>()
92-
93-
await Promise.allSettled(
90+
// Fetch all images in parallel, then deduplicate filenames serially to avoid
91+
// a race where two concurrent callbacks both pass the "not yet seen" check.
92+
const fetchResults = await Promise.allSettled(
9493
imageIds.map(async (imageId) => {
95-
try {
96-
const imgRecord = await getFileMetadataById(imageId)
97-
if (!imgRecord) return
98-
const imgHasAccess = await verifyFileAccess(imgRecord.key, authResult.userId)
99-
if (!imgHasAccess) return
100-
const imgBuffer = await downloadFile({
101-
key: imgRecord.key,
102-
context: imgRecord.context as StorageContext,
103-
})
104-
const preferred = safeFilename(imgRecord.originalName)
105-
const filename = deduplicatedFilename(preferred, usedFilenames, imageId)
106-
usedFilenames.add(filename)
107-
assetMap.set(imageId, { filename, buffer: imgBuffer })
108-
} catch (err) {
109-
logger.warn('Failed to fetch asset for export', { imageId, error: toError(err).message })
110-
}
94+
const imgRecord = await getFileMetadataById(imageId)
95+
if (!imgRecord) return null
96+
const imgHasAccess = await verifyFileAccess(imgRecord.key, authResult.userId)
97+
if (!imgHasAccess) return null
98+
const imgBuffer = await downloadFile({
99+
key: imgRecord.key,
100+
context: imgRecord.context as StorageContext,
101+
})
102+
return { imageId, originalName: imgRecord.originalName, buffer: imgBuffer }
111103
})
112104
)
113105

106+
const assetMap = new Map<string, { filename: string; buffer: Buffer }>()
107+
const usedFilenames = new Set<string>()
108+
109+
for (let i = 0; i < fetchResults.length; i++) {
110+
const result = fetchResults[i]
111+
if (result.status === 'rejected') {
112+
logger.warn('Failed to fetch asset for export', {
113+
imageId: imageIds[i],
114+
error: toError(result.reason).message,
115+
})
116+
continue
117+
}
118+
if (!result.value) continue
119+
const { imageId, originalName, buffer } = result.value
120+
const preferred = safeFilename(originalName)
121+
const filename = deduplicatedFilename(preferred, usedFilenames, imageId)
122+
usedFilenames.add(filename)
123+
assetMap.set(imageId, { filename, buffer })
124+
}
125+
114126
for (const [imageId, asset] of assetMap) {
115127
const escapedId = imageId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
116128
mdContent = mdContent.replace(

0 commit comments

Comments
 (0)