180 lines
5.0 KiB
TypeScript
180 lines
5.0 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import { Type } from '@sinclair/typebox';
|
|
import { and, eq } from 'drizzle-orm';
|
|
import { nodes, servers } from '@source/database';
|
|
import { AppError } from '../../lib/errors.js';
|
|
import { requirePermission } from '../../lib/permissions.js';
|
|
import {
|
|
daemonDeleteFiles,
|
|
daemonListFiles,
|
|
daemonReadFile,
|
|
daemonWriteFile,
|
|
type DaemonNodeConnection,
|
|
} from '../../lib/daemon.js';
|
|
|
|
const FileParamSchema = {
|
|
params: Type.Object({
|
|
orgId: Type.String({ format: 'uuid' }),
|
|
serverId: Type.String({ format: 'uuid' }),
|
|
}),
|
|
};
|
|
|
|
function decodeBase64Payload(data: string): Buffer {
|
|
const normalized = data.trim();
|
|
if (!normalized) return Buffer.alloc(0);
|
|
|
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(normalized) || normalized.length % 4 !== 0) {
|
|
throw AppError.badRequest('Invalid base64 payload');
|
|
}
|
|
|
|
return Buffer.from(normalized, 'base64');
|
|
}
|
|
|
|
export default async function fileRoutes(app: FastifyInstance) {
|
|
app.addHook('onRequest', app.authenticate);
|
|
|
|
app.get(
|
|
'/',
|
|
{
|
|
schema: {
|
|
...FileParamSchema,
|
|
querystring: Type.Object({
|
|
path: Type.Optional(Type.String()),
|
|
}),
|
|
},
|
|
},
|
|
async (request) => {
|
|
const { orgId, serverId } = request.params as { orgId: string; serverId: string };
|
|
const { path } = request.query as { path?: string };
|
|
|
|
await requirePermission(request, orgId, 'files.read');
|
|
const serverContext = await getServerContext(app, orgId, serverId);
|
|
|
|
const files = await daemonListFiles(
|
|
serverContext.node,
|
|
serverContext.serverUuid,
|
|
path?.trim() || '/',
|
|
);
|
|
|
|
return { files };
|
|
},
|
|
);
|
|
|
|
app.get(
|
|
'/read',
|
|
{
|
|
schema: {
|
|
...FileParamSchema,
|
|
querystring: Type.Object({
|
|
path: Type.String({ minLength: 1 }),
|
|
encoding: Type.Optional(Type.Union([Type.Literal('utf8'), Type.Literal('base64')])),
|
|
}),
|
|
},
|
|
},
|
|
async (request) => {
|
|
const { orgId, serverId } = request.params as { orgId: string; serverId: string };
|
|
const { path, encoding } = request.query as {
|
|
path: string;
|
|
encoding?: 'utf8' | 'base64';
|
|
};
|
|
|
|
await requirePermission(request, orgId, 'files.read');
|
|
const serverContext = await getServerContext(app, orgId, serverId);
|
|
|
|
const content = await daemonReadFile(serverContext.node, serverContext.serverUuid, path);
|
|
const requestedEncoding = encoding === 'base64' ? 'base64' : 'utf8';
|
|
|
|
return {
|
|
data:
|
|
requestedEncoding === 'base64'
|
|
? content.data.toString('base64')
|
|
: content.data.toString('utf8'),
|
|
encoding: requestedEncoding,
|
|
mimeType: content.mimeType,
|
|
};
|
|
},
|
|
);
|
|
|
|
app.post(
|
|
'/write',
|
|
{
|
|
bodyLimit: 128 * 1024 * 1024,
|
|
schema: {
|
|
...FileParamSchema,
|
|
body: Type.Object({
|
|
path: Type.String({ minLength: 1 }),
|
|
data: Type.String(),
|
|
encoding: Type.Optional(Type.Union([Type.Literal('utf8'), Type.Literal('base64')])),
|
|
}),
|
|
},
|
|
},
|
|
async (request) => {
|
|
const { orgId, serverId } = request.params as { orgId: string; serverId: string };
|
|
const { path, data, encoding } = request.body as {
|
|
path: string;
|
|
data: string;
|
|
encoding?: 'utf8' | 'base64';
|
|
};
|
|
|
|
await requirePermission(request, orgId, 'files.write');
|
|
const serverContext = await getServerContext(app, orgId, serverId);
|
|
|
|
const payload = encoding === 'base64' ? decodeBase64Payload(data) : data;
|
|
|
|
await daemonWriteFile(serverContext.node, serverContext.serverUuid, path, payload);
|
|
return { success: true, path };
|
|
},
|
|
);
|
|
|
|
app.post(
|
|
'/delete',
|
|
{
|
|
schema: {
|
|
...FileParamSchema,
|
|
body: Type.Object({
|
|
paths: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
}),
|
|
},
|
|
},
|
|
async (request) => {
|
|
const { orgId, serverId } = request.params as { orgId: string; serverId: string };
|
|
const { paths } = request.body as { paths: string[] };
|
|
|
|
await requirePermission(request, orgId, 'files.delete');
|
|
const serverContext = await getServerContext(app, orgId, serverId);
|
|
|
|
await daemonDeleteFiles(serverContext.node, serverContext.serverUuid, paths);
|
|
return { success: true, paths };
|
|
},
|
|
);
|
|
}
|
|
|
|
async function getServerContext(app: FastifyInstance, orgId: string, serverId: string): Promise<{
|
|
serverUuid: string;
|
|
node: DaemonNodeConnection;
|
|
}> {
|
|
const [server] = await app.db
|
|
.select({
|
|
uuid: servers.uuid,
|
|
nodeFqdn: nodes.fqdn,
|
|
nodeGrpcPort: nodes.grpcPort,
|
|
nodeDaemonToken: nodes.daemonToken,
|
|
})
|
|
.from(servers)
|
|
.innerJoin(nodes, eq(servers.nodeId, nodes.id))
|
|
.where(and(eq(servers.id, serverId), eq(servers.organizationId, orgId)));
|
|
|
|
if (!server) {
|
|
throw AppError.notFound('Server not found');
|
|
}
|
|
|
|
return {
|
|
serverUuid: server.uuid,
|
|
node: {
|
|
fqdn: server.nodeFqdn,
|
|
grpcPort: server.nodeGrpcPort,
|
|
daemonToken: server.nodeDaemonToken,
|
|
},
|
|
};
|
|
}
|