240 lines
6.4 KiB
TypeScript
240 lines
6.4 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|