TypeScript · 5763 bytes Raw Blame History
1 import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2 import type {
3 DirectoryListing,
4 UploadRequest,
5 UploadResponse,
6 DownloadRequest,
7 DownloadResponse,
8 FileItem
9 } from '../../shared/types.js';
10 import { z } from 'zod';
11
12 const listFilesSchema = z.object({
13 path: z.string().optional().default('/'),
14 });
15
16 const uploadSchema = z.object({
17 path: z.string().default('/'),
18 encrypted: z.boolean().optional(),
19 });
20
21 const fileIdSchema = z.object({
22 fileId: z.string().min(1),
23 });
24
25 declare module 'fastify' {
26 interface FastifyInstance {
27 zephyrfs: import('../integration/zephyrfs-client.js').ZephyrFSClient;
28 }
29 }
30
31 export async function filesRoutes(fastify: FastifyInstance) {
32 // List files in directory
33 fastify.get<{
34 Querystring: z.infer<typeof listFilesSchema>;
35 }>('/files', {
36 schema: {
37 querystring: listFilesSchema,
38 },
39 preHandler: fastify.authenticate,
40 }, async (request: FastifyRequest, reply: FastifyReply) => {
41 const { path } = request.query as z.infer<typeof listFilesSchema>;
42
43 try {
44 const listing = await fastify.zephyrfs.listFiles(path);
45 return listing;
46 } catch (error) {
47 fastify.log.error(error, 'Failed to list files');
48 throw fastify.httpErrors.internalServerError('Failed to list files');
49 }
50 });
51
52 // Upload file
53 fastify.post<{
54 Body: z.infer<typeof uploadSchema>;
55 }>('/files/upload', {
56 preHandler: fastify.authenticate,
57 }, async (request: FastifyRequest, reply: FastifyReply) => {
58 try {
59 const parts = request.parts();
60 let fileData: Buffer | undefined;
61 let filename: string | undefined;
62 let path = '/';
63 let encrypted = false;
64
65 for await (const part of parts) {
66 if (part.type === 'file') {
67 filename = part.filename;
68 const chunks: Buffer[] = [];
69 for await (const chunk of part.file) {
70 chunks.push(chunk);
71 }
72 fileData = Buffer.concat(chunks);
73 } else if (part.type === 'field') {
74 const fieldName = part.fieldname;
75 const value = part.value as string;
76
77 switch (fieldName) {
78 case 'path':
79 path = value;
80 break;
81 case 'encrypted':
82 encrypted = value === 'true';
83 break;
84 }
85 }
86 }
87
88 if (!fileData || !filename) {
89 throw fastify.httpErrors.badRequest('File and filename are required');
90 }
91
92 const result = await fastify.zephyrfs.uploadFile(path, filename, fileData, {
93 encrypted,
94 });
95
96 const response: UploadResponse = {
97 fileId: result.fileId,
98 uploadUrl: `/api/files/${result.fileId}`,
99 chunkSize: 1024 * 1024, // 1MB
100 totalChunks: Math.ceil(result.size / (1024 * 1024)),
101 };
102
103 return response;
104 } catch (error) {
105 fastify.log.error(error, 'Failed to upload file');
106 if (error.statusCode) {
107 throw error;
108 }
109 throw fastify.httpErrors.internalServerError('Failed to upload file');
110 }
111 });
112
113 // Download file
114 fastify.get<{
115 Params: z.infer<typeof fileIdSchema>;
116 Querystring: { range?: string };
117 }>('/files/:fileId/download', {
118 schema: {
119 params: fileIdSchema,
120 },
121 preHandler: fastify.authenticate,
122 }, async (request: FastifyRequest, reply: FastifyReply) => {
123 const { fileId } = request.params as z.infer<typeof fileIdSchema>;
124 const rangeHeader = request.headers.range;
125
126 try {
127 let options = {};
128
129 if (rangeHeader) {
130 const range = rangeHeader.replace('bytes=', '').split('-');
131 const start = parseInt(range[0]);
132 const end = range[1] ? parseInt(range[1]) : undefined;
133 options = { range: { start, end } };
134 }
135
136 const download = await fastify.zephyrfs.downloadFile(fileId, options);
137
138 // Set response headers
139 reply.header('Content-Disposition', `attachment; filename="${download.filename}"`);
140 reply.header('Content-Length', download.size.toString());
141
142 if (download.mimeType) {
143 reply.header('Content-Type', download.mimeType);
144 }
145
146 if (rangeHeader) {
147 reply.code(206); // Partial Content
148 reply.header('Accept-Ranges', 'bytes');
149 reply.header('Content-Range', `bytes ${options.range?.start || 0}-${options.range?.end || download.size - 1}/${download.size}`);
150 }
151
152 // Stream the file
153 return reply.send(download.stream);
154 } catch (error) {
155 fastify.log.error(error, 'Failed to download file');
156 throw fastify.httpErrors.notFound('File not found');
157 }
158 });
159
160 // Get file info
161 fastify.get<{
162 Params: z.infer<typeof fileIdSchema>;
163 }>('/files/:fileId/info', {
164 schema: {
165 params: fileIdSchema,
166 },
167 preHandler: fastify.authenticate,
168 }, async (request: FastifyRequest, reply: FastifyReply) => {
169 const { fileId } = request.params as z.infer<typeof fileIdSchema>;
170
171 try {
172 const fileInfo = await fastify.zephyrfs.getFileInfo(fileId);
173 return fileInfo;
174 } catch (error) {
175 fastify.log.error(error, 'Failed to get file info');
176 throw fastify.httpErrors.notFound('File not found');
177 }
178 });
179
180 // Delete file
181 fastify.delete<{
182 Params: z.infer<typeof fileIdSchema>;
183 }>('/files/:fileId', {
184 schema: {
185 params: fileIdSchema,
186 },
187 preHandler: fastify.authenticate,
188 }, async (request: FastifyRequest, reply: FastifyReply) => {
189 const { fileId } = request.params as z.infer<typeof fileIdSchema>;
190
191 try {
192 await fastify.zephyrfs.deleteFile(fileId);
193 return { success: true };
194 } catch (error) {
195 fastify.log.error(error, 'Failed to delete file');
196 throw fastify.httpErrors.notFound('File not found');
197 }
198 });
199 }