| 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 |
} |