import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { z } from 'zod'; import { env } from '../config/env.js'; import { copyResourceManagerItem, createResourceManagerDirectory, createResourceManagerFile, deleteResourceManagerItem, ensureResourceManagerRoot, getResourceManagerTree, listResourceManagerDirectory, moveResourceManagerItem, openResourceManagerPreviewStream, readResourceManagerFile, saveResourceManagerFile, uploadResourceManagerFile, } from '../services/resource-manager-service.js'; const queryPathSchema = z.object({ path: z.string().trim().optional().default(''), }); const createDirectoryBodySchema = z.object({ parentPath: z.string().trim().optional().default(''), name: z.string().trim().min(1).max(255), }); const createFileBodySchema = z.object({ parentPath: z.string().trim().optional().default(''), name: z.string().trim().min(1).max(255), content: z.string().optional().default(''), }); const saveFileBodySchema = z.object({ path: z.string().trim().min(1), content: z.string(), }); const uploadFileBodySchema = z.object({ parentPath: z.string().trim().optional().default(''), fileName: z.string().trim().min(1).max(255), contentBase64: z.string().trim().min(1), }); const copyMoveBodySchema = z.object({ path: z.string().trim().min(1), targetDirectoryPath: z.string().trim().optional().default(''), nextName: z.string().trim().max(255).optional().nullable(), }); function getRequestAccessToken(request: FastifyRequest) { const tokenHeader = request.headers['x-access-token']; return Array.isArray(tokenHeader) ? tokenHeader[0]?.trim() ?? '' : String(tokenHeader ?? '').trim(); } function ensureAuthorized(request: FastifyRequest, reply: FastifyReply, tokenOverride?: string | null) { if ((tokenOverride ?? getRequestAccessToken(request)) === env.SERVER_COMMAND_ACCESS_TOKEN) { return true; } reply.status(403); void reply.send({ message: '권한 토큰이 필요합니다.', }); return false; } function resolveRepoRootPath() { return env.SERVER_COMMAND_MAIN_PROJECT_ROOT || env.PLAN_MAIN_PROJECT_REPO_PATH || env.PLAN_GIT_REPO_PATH; } export async function registerResourceManagerRoutes(app: FastifyInstance) { app.get('/api/resource-manager/tree', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const repoRootPath = resolveRepoRootPath(); await ensureResourceManagerRoot(repoRootPath); return { ok: true, item: await getResourceManagerTree(repoRootPath), }; }); app.get('/api/resource-manager/directory', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const query = queryPathSchema.parse(request.query ?? {}); return { ok: true, item: await listResourceManagerDirectory(resolveRepoRootPath(), query.path), }; }); app.get('/api/resource-manager/file', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const query = queryPathSchema.extend({ path: z.string().trim().min(1), }).parse(request.query ?? {}); return { ok: true, item: await readResourceManagerFile(resolveRepoRootPath(), query.path), }; }); app.get('/api/resource-manager/preview/*', async (request, reply) => { const query = z.object({ token: z.string().trim().optional(), }).parse(request.query ?? {}); if (!ensureAuthorized(request, reply, query.token ?? null)) { return; } const wildcard = String((request.params as { '*': string | undefined })['*'] ?? '').trim(); const preview = await openResourceManagerPreviewStream(resolveRepoRootPath(), decodeURIComponent(wildcard)); reply.header('Cache-Control', 'no-store'); reply.type(preview.contentType); return reply.send(preview.stream); }); app.post('/api/resource-manager/directories', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const payload = createDirectoryBodySchema.parse(request.body ?? {}); await createResourceManagerDirectory(resolveRepoRootPath(), payload.parentPath, payload.name); return { ok: true, }; }); app.post('/api/resource-manager/files', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const payload = createFileBodySchema.parse(request.body ?? {}); await createResourceManagerFile(resolveRepoRootPath(), payload.parentPath, payload.name, payload.content); return { ok: true, }; }); app.put('/api/resource-manager/files/content', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const payload = saveFileBodySchema.parse(request.body ?? {}); await saveResourceManagerFile(resolveRepoRootPath(), payload.path, payload.content); return { ok: true, }; }); app.post('/api/resource-manager/files/upload', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const payload = uploadFileBodySchema.parse(request.body ?? {}); return { ok: true, item: await uploadResourceManagerFile( resolveRepoRootPath(), payload.parentPath, payload.fileName, payload.contentBase64, ), }; }); app.post('/api/resource-manager/items/copy', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const payload = copyMoveBodySchema.parse(request.body ?? {}); return { ok: true, item: await copyResourceManagerItem( resolveRepoRootPath(), payload.path, payload.targetDirectoryPath, payload.nextName, ), }; }); app.post('/api/resource-manager/items/move', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const payload = copyMoveBodySchema.parse(request.body ?? {}); return { ok: true, item: await moveResourceManagerItem( resolveRepoRootPath(), payload.path, payload.targetDirectoryPath, payload.nextName, ), }; }); app.delete('/api/resource-manager/items', async (request, reply) => { if (!ensureAuthorized(request, reply)) { return; } const query = queryPathSchema.extend({ path: z.string().trim().min(1), }).parse(request.query ?? {}); await deleteResourceManagerItem(resolveRepoRootPath(), query.path); return { ok: true, }; }); }