TypeScript · 7318 bytes Raw Blame History
1 import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2 import { z } from 'zod';
3
4 const createFolderSchema = z.object({
5 name: z.string().min(1).max(255),
6 path: z.string().default('/'),
7 encrypted: z.boolean().optional().default(false),
8 });
9
10 const renameFolderSchema = z.object({
11 newName: z.string().min(1).max(255),
12 });
13
14 const moveFolderSchema = z.object({
15 sourcePath: z.string(),
16 targetPath: z.string(),
17 });
18
19 export async function foldersRoutes(fastify: FastifyInstance) {
20 // Create folder
21 fastify.post<{
22 Body: z.infer<typeof createFolderSchema>;
23 }>('/files/folder', {
24 schema: {
25 body: createFolderSchema,
26 },
27 preHandler: fastify.authenticate,
28 }, async (request: FastifyRequest, reply: FastifyReply) => {
29 const { name, path, encrypted } = request.body as z.infer<typeof createFolderSchema>;
30
31 try {
32 // Validate folder name
33 const invalidChars = /[<>:"/\\|?*\x00-\x1f]/;
34 if (invalidChars.test(name)) {
35 throw fastify.httpErrors.badRequest('Folder name contains invalid characters');
36 }
37
38 // Check for reserved names
39 const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
40 if (reservedNames.includes(name.toUpperCase())) {
41 throw fastify.httpErrors.badRequest('This folder name is reserved');
42 }
43
44 // Trim and validate
45 const folderName = name.trim();
46 if (!folderName || folderName.startsWith('.') || folderName.endsWith('.')) {
47 throw fastify.httpErrors.badRequest('Invalid folder name');
48 }
49
50 // Check if folder already exists
51 try {
52 const listing = await fastify.zephyrfs.listFiles(path);
53 const existingFolder = listing.files.find(f => f.name === folderName && f.type === 'directory');
54 if (existingFolder) {
55 throw fastify.httpErrors.conflict('Folder already exists');
56 }
57 } catch (error) {
58 // If listing fails, the parent directory doesn't exist
59 if (error.statusCode === 404) {
60 throw fastify.httpErrors.notFound('Parent directory does not exist');
61 }
62 throw error;
63 }
64
65 // Create folder by creating a marker file (since most storage systems don't support empty directories)
66 const folderPath = path === '/' ? `/${folderName}` : `${path}/${folderName}`;
67 const markerFileName = '.zephyrfs_folder_marker';
68 const markerContent = JSON.stringify({
69 type: 'directory',
70 name: folderName,
71 created: new Date().toISOString(),
72 encrypted: encrypted,
73 });
74
75 const markerBuffer = Buffer.from(markerContent, 'utf-8');
76 await fastify.zephyrfs.uploadFile(folderPath, markerFileName, markerBuffer, {
77 encrypted: false, // Marker files are always unencrypted for metadata
78 });
79
80 return {
81 success: true,
82 path: folderPath,
83 name: folderName,
84 encrypted: encrypted,
85 };
86 } catch (error) {
87 fastify.log.error(error, 'Failed to create folder');
88 if (error.statusCode) {
89 throw error;
90 }
91 throw fastify.httpErrors.internalServerError('Failed to create folder');
92 }
93 });
94
95 // Rename folder
96 fastify.patch<{
97 Params: { path: string };
98 Body: z.infer<typeof renameFolderSchema>;
99 }>('/files/folder/:path(*)', {
100 schema: {
101 body: renameFolderSchema,
102 },
103 preHandler: fastify.authenticate,
104 }, async (request: FastifyRequest, reply: FastifyReply) => {
105 const folderPath = '/' + (request.params as any).path;
106 const { newName } = request.body as z.infer<typeof renameFolderSchema>;
107
108 try {
109 // Validate new name
110 const invalidChars = /[<>:"/\\|?*\x00-\x1f]/;
111 if (invalidChars.test(newName)) {
112 throw fastify.httpErrors.badRequest('Folder name contains invalid characters');
113 }
114
115 const trimmedName = newName.trim();
116 if (!trimmedName || trimmedName.startsWith('.') || trimmedName.endsWith('.')) {
117 throw fastify.httpErrors.badRequest('Invalid folder name');
118 }
119
120 // Get parent path
121 const pathParts = folderPath.split('/').filter(Boolean);
122 const parentPath = pathParts.length > 1 ? '/' + pathParts.slice(0, -1).join('/') : '/';
123
124 // Check if target name already exists
125 try {
126 const parentListing = await fastify.zephyrfs.listFiles(parentPath);
127 const existingItem = parentListing.files.find(f => f.name === trimmedName);
128 if (existingItem) {
129 throw fastify.httpErrors.conflict('An item with this name already exists');
130 }
131 } catch (error) {
132 if (error.statusCode === 404) {
133 throw fastify.httpErrors.notFound('Parent directory does not exist');
134 }
135 throw error;
136 }
137
138 // For now, return not implemented since folder renaming requires moving all contained files
139 // This would need to be implemented in the ZephyrFS core with proper atomic operations
140 throw fastify.httpErrors.notImplemented('Folder renaming is not yet implemented');
141
142 } catch (error) {
143 fastify.log.error(error, 'Failed to rename folder');
144 if (error.statusCode) {
145 throw error;
146 }
147 throw fastify.httpErrors.internalServerError('Failed to rename folder');
148 }
149 });
150
151 // Move folder
152 fastify.post<{
153 Body: z.infer<typeof moveFolderSchema>;
154 }>('/files/folder/move', {
155 schema: {
156 body: moveFolderSchema,
157 },
158 preHandler: fastify.authenticate,
159 }, async (request: FastifyRequest, reply: FastifyReply) => {
160 const { sourcePath, targetPath } = request.body as z.infer<typeof moveFolderSchema>;
161
162 try {
163 // For now, return not implemented since folder moving requires atomic operations
164 // This would need to be implemented in the ZephyrFS core
165 throw fastify.httpErrors.notImplemented('Folder moving is not yet implemented');
166
167 } catch (error) {
168 fastify.log.error(error, 'Failed to move folder');
169 if (error.statusCode) {
170 throw error;
171 }
172 throw fastify.httpErrors.internalServerError('Failed to move folder');
173 }
174 });
175
176 // Delete empty folder
177 fastify.delete<{
178 Params: { path: string };
179 }>('/files/folder/:path(*)', {
180 preHandler: fastify.authenticate,
181 }, async (request: FastifyRequest, reply: FastifyReply) => {
182 const folderPath = '/' + (request.params as any).path;
183
184 try {
185 // Check if folder exists and is empty
186 const listing = await fastify.zephyrfs.listFiles(folderPath);
187
188 // Filter out the marker file
189 const realFiles = listing.files.filter(f => f.name !== '.zephyrfs_folder_marker');
190
191 if (realFiles.length > 0) {
192 throw fastify.httpErrors.badRequest('Folder is not empty');
193 }
194
195 // Delete the marker file
196 const markerFile = listing.files.find(f => f.name === '.zephyrfs_folder_marker');
197 if (markerFile) {
198 await fastify.zephyrfs.deleteFile(markerFile.id);
199 }
200
201 return { success: true };
202
203 } catch (error) {
204 fastify.log.error(error, 'Failed to delete folder');
205 if (error.statusCode) {
206 throw error;
207 }
208 throw fastify.httpErrors.internalServerError('Failed to delete folder');
209 }
210 });
211 }