TypeScript · 5814 bytes Raw Blame History
1 import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2 import { z } from 'zod';
3 import archiver from 'archiver';
4 import { PassThrough } from 'node:stream';
5
6 const bulkDownloadSchema = z.object({
7 fileIds: z.array(z.string()).min(1).max(100), // Limit to 100 files
8 format: z.enum(['zip', 'tar']).default('zip'),
9 archiveName: z.string().optional(),
10 });
11
12 const bulkDeleteSchema = z.object({
13 fileIds: z.array(z.string()).min(1).max(100),
14 });
15
16 const bulkMoveSchema = z.object({
17 fileIds: z.array(z.string()).min(1).max(100),
18 targetPath: z.string(),
19 });
20
21 export async function bulkRoutes(fastify: FastifyInstance) {
22 // Bulk download files as archive
23 fastify.post<{
24 Body: z.infer<typeof bulkDownloadSchema>;
25 }>('/files/bulk/download', {
26 schema: {
27 body: bulkDownloadSchema,
28 },
29 preHandler: fastify.authenticate,
30 }, async (request: FastifyRequest, reply: FastifyReply) => {
31 const { fileIds, format, archiveName } = request.body as z.infer<typeof bulkDownloadSchema>;
32
33 try {
34 // Create archive
35 const archive = archiver(format, {
36 zlib: { level: 6 }, // Compression level for zip
37 });
38
39 const passThrough = new PassThrough();
40 archive.pipe(passThrough);
41
42 // Set response headers
43 const timestamp = new Date().toISOString().split('T')[0];
44 const defaultName = archiveName || `zephyrfs-files-${timestamp}.${format}`;
45
46 reply.header('Content-Type', `application/${format}`);
47 reply.header('Content-Disposition', `attachment; filename="${defaultName}"`);
48
49 // Handle archive errors
50 archive.on('error', (err) => {
51 fastify.log.error(err, 'Archive creation failed');
52 if (!reply.sent) {
53 reply.code(500).send({ error: 'Archive creation failed' });
54 }
55 });
56
57 // Process each file
58 const filePromises = fileIds.map(async (fileId) => {
59 try {
60 // Get file info first
61 const fileInfo = await fastify.zephyrfs.getFileInfo(fileId);
62
63 // Download file stream
64 const download = await fastify.zephyrfs.downloadFile(fileId);
65
66 // Add to archive
67 archive.append(download.stream as any, {
68 name: fileInfo.name,
69 date: fileInfo.lastModified,
70 });
71
72 } catch (error) {
73 fastify.log.warn({ fileId, error }, 'Failed to add file to archive');
74 // Continue with other files instead of failing entire operation
75 }
76 });
77
78 // Wait for all files to be processed
79 await Promise.allSettled(filePromises);
80
81 // Finalize archive
82 await archive.finalize();
83
84 // Stream the archive
85 return reply.send(passThrough);
86
87 } catch (error) {
88 fastify.log.error(error, 'Bulk download failed');
89 throw fastify.httpErrors.internalServerError('Bulk download failed');
90 }
91 });
92
93 // Bulk delete files
94 fastify.delete<{
95 Body: z.infer<typeof bulkDeleteSchema>;
96 }>('/files/bulk/delete', {
97 schema: {
98 body: bulkDeleteSchema,
99 },
100 preHandler: fastify.authenticate,
101 }, async (request: FastifyRequest, reply: FastifyReply) => {
102 const { fileIds } = request.body as z.infer<typeof bulkDeleteSchema>;
103
104 try {
105 const results = await Promise.allSettled(
106 fileIds.map(fileId => fastify.zephyrfs.deleteFile(fileId))
107 );
108
109 // Count successes and failures
110 const successful = results.filter(r => r.status === 'fulfilled').length;
111 const failed = results.filter(r => r.status === 'rejected').length;
112
113 // Get details of failures
114 const failures = results
115 .map((result, index) => ({ result, fileId: fileIds[index] }))
116 .filter(({ result }) => result.status === 'rejected')
117 .map(({ result, fileId }) => ({
118 fileId,
119 error: (result as PromiseRejectedResult).reason?.message || 'Unknown error'
120 }));
121
122 return {
123 success: true,
124 summary: {
125 total: fileIds.length,
126 successful,
127 failed,
128 },
129 failures: failed > 0 ? failures : undefined,
130 };
131
132 } catch (error) {
133 fastify.log.error(error, 'Bulk delete failed');
134 throw fastify.httpErrors.internalServerError('Bulk delete failed');
135 }
136 });
137
138 // Bulk move files
139 fastify.post<{
140 Body: z.infer<typeof bulkMoveSchema>;
141 }>('/files/bulk/move', {
142 schema: {
143 body: bulkMoveSchema,
144 },
145 preHandler: fastify.authenticate,
146 }, async (request: FastifyRequest, reply: FastifyReply) => {
147 const { fileIds, targetPath } = request.body as z.infer<typeof bulkMoveSchema>;
148
149 try {
150 // For now, return not implemented since file moving requires
151 // proper implementation in the ZephyrFS core
152 throw fastify.httpErrors.notImplemented('Bulk move is not yet implemented');
153
154 } catch (error) {
155 fastify.log.error(error, 'Bulk move failed');
156 if (error.statusCode) {
157 throw error;
158 }
159 throw fastify.httpErrors.internalServerError('Bulk move failed');
160 }
161 });
162
163 // Get bulk operation status
164 fastify.get<{
165 Params: { operationId: string };
166 }>('/files/bulk/status/:operationId', {
167 preHandler: fastify.authenticate,
168 }, async (request: FastifyRequest, reply: FastifyReply) => {
169 const { operationId } = request.params as { operationId: string };
170
171 try {
172 // For now, return not implemented since this would require
173 // background job tracking
174 throw fastify.httpErrors.notImplemented('Bulk operation status tracking is not yet implemented');
175
176 } catch (error) {
177 fastify.log.error(error, 'Failed to get bulk operation status');
178 if (error.statusCode) {
179 throw error;
180 }
181 throw fastify.httpErrors.internalServerError('Failed to get bulk operation status');
182 }
183 });
184 }