1- import { Readable } from 'node:stream'
2- import { createLogger } from '@sim/logger'
3- import { ZipArchive } from 'archiver'
41import { v2BulkDownloadFilesContract } from '@/lib/api/contracts/v2/files'
52import { defineV2BinaryRoute , v2ApiKeyAuth , v2RateLimits } from '@/lib/api/server/routes'
6- import { nodeReadableToWebStream } from '@/lib/core/utils/node-stream'
7- import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
8- import { downloadFileStream } from '@/lib/uploads/core/storage-service'
9- import { buildZipEntryPaths } from '@/lib/uploads/zip-entry-path'
10- import { v2FileErrorPolicies } from '@/lib/workspace-files/api'
3+ import { presentWorkspaceFileArchive , v2FileErrorPolicies } from '@/lib/workspace-files/api'
114import { downloadWorkspaceFileItems } from '@/lib/workspace-files/application/download-workspace-file-items'
125import { fileOperations } from '@/lib/workspace-files/application/operations'
136
14- const logger = createLogger ( 'V2FilesBulkDownloadAPI' )
15-
167export const dynamic = 'force-dynamic'
178export const revalidate = 0
189
1910/** Opens each object only as the archiver reaches it, so peak memory stays flat. */
20- function lazyWorkspaceFileStream ( file : WorkspaceFileRecord ) : Readable {
21- return Readable . from (
22- ( async function * ( ) {
23- yield * await downloadFileStream ( {
24- key : file . key ,
25- context : file . storageContext ?? 'workspace' ,
26- } )
27- } ) ( ) ,
28- { objectMode : false }
29- )
30- }
3111
3212/**
3313 * GET /api/v2/files/bulk-download — stream a selection of files as one zip.
@@ -54,36 +34,5 @@ export const GET = defineV2BinaryRoute({
5434 folderPaths : query . folderPaths ,
5535 } ) ,
5636 useCase : downloadWorkspaceFileItems ,
57- present : ( { filesToZip, folderPaths, renderedDocuments, excludedFileIds } ) => {
58- const report = excludedFileIds . length > 0 ? { partial : true , excludedFileIds } : undefined
59- const entryPaths = buildZipEntryPaths ( [
60- ...filesToZip . map ( ( file ) => ( {
61- name : file . name ,
62- folderPath : file . folderId ? folderPaths . get ( file . folderId ) : null ,
63- contentType : file . type ,
64- } ) ) ,
65- ...( report ? [ { name : 'sim-download-report.json' , contentType : 'application/json' } ] : [ ] ) ,
66- ] )
67- const archive = new ZipArchive ( { store : true } )
68- archive . on ( 'warning' , ( error : Error ) => {
69- logger . warn ( 'Archive warning while streaming workspace files' , { error } )
70- } )
71- filesToZip . forEach ( ( file , index ) => {
72- archive . append ( renderedDocuments . get ( file . id ) ?? lazyWorkspaceFileStream ( file ) , {
73- name : entryPaths [ index ] ,
74- } )
75- } )
76- if ( report ) {
77- archive . append ( JSON . stringify ( report , null , 2 ) , { name : entryPaths [ filesToZip . length ] } )
78- }
79- archive . finalize ( ) . catch ( ( error ) => {
80- logger . error ( 'Failed to finalize workspace file archive' , { error } )
81- } )
82-
83- return {
84- body : nodeReadableToWebStream ( archive ) ,
85- contentType : 'application/zip' ,
86- contentDisposition : 'attachment; filename="workspace-files.zip"' ,
87- }
88- } ,
37+ present : presentWorkspaceFileArchive ,
8938} )
0 commit comments