feat: overhaul server automation, files editor, and CS2 setup workflows
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { servers, backups } from '@source/database';
|
||||
import { servers, backups, nodes } from '@source/database';
|
||||
import { AppError } from '../../lib/errors.js';
|
||||
import { requirePermission } from '../../lib/permissions.js';
|
||||
import { createAuditLog } from '../../lib/audit.js';
|
||||
import {
|
||||
daemonCreateBackup,
|
||||
daemonDeleteBackup,
|
||||
daemonRestoreBackup,
|
||||
type DaemonNodeConnection,
|
||||
} from '../../lib/daemon.js';
|
||||
|
||||
const ParamSchema = {
|
||||
params: Type.Object({
|
||||
@@ -54,10 +60,7 @@ export default async function backupRoutes(app: FastifyInstance) {
|
||||
|
||||
const body = request.body as { name: string; isLocked?: boolean };
|
||||
|
||||
const server = await app.db.query.servers.findFirst({
|
||||
where: and(eq(servers.id, serverId), eq(servers.organizationId, orgId)),
|
||||
});
|
||||
if (!server) throw AppError.notFound('Server not found');
|
||||
const serverContext = await getServerBackupContext(app, orgId, serverId);
|
||||
|
||||
// Create backup record (pending — daemon will update when complete)
|
||||
const [backup] = await app.db
|
||||
@@ -69,12 +72,38 @@ export default async function backupRoutes(app: FastifyInstance) {
|
||||
})
|
||||
.returning();
|
||||
|
||||
// TODO: Send gRPC CreateBackup to daemon
|
||||
// Daemon will:
|
||||
// 1. tar+gz the server directory
|
||||
// 2. Upload to @source/cdn
|
||||
// 3. Callback to API with cdnPath, sizeBytes, checksum
|
||||
// 4. API updates backup record with completedAt
|
||||
if (!backup) {
|
||||
throw new AppError(500, 'Failed to create backup record', 'BACKUP_CREATE_FAILED');
|
||||
}
|
||||
|
||||
let completedBackup = backup;
|
||||
try {
|
||||
const daemonResult = await daemonCreateBackup(
|
||||
serverContext.node,
|
||||
serverContext.serverUuid,
|
||||
backup.id,
|
||||
);
|
||||
|
||||
if (!daemonResult.success) {
|
||||
throw new Error('Daemon returned unsuccessful backup response');
|
||||
}
|
||||
|
||||
const [updated] = await app.db
|
||||
.update(backups)
|
||||
.set({
|
||||
sizeBytes: daemonResult.sizeBytes,
|
||||
checksum: daemonResult.checksum || null,
|
||||
completedAt: new Date(),
|
||||
})
|
||||
.where(eq(backups.id, backup.id))
|
||||
.returning();
|
||||
|
||||
completedBackup = updated ?? completedBackup;
|
||||
} catch (error) {
|
||||
request.log.error({ error, serverId, backupId: backup.id }, 'Failed to create backup on daemon');
|
||||
await app.db.delete(backups).where(eq(backups.id, backup.id));
|
||||
throw new AppError(502, 'Failed to create backup on daemon', 'DAEMON_BACKUP_CREATE_FAILED');
|
||||
}
|
||||
|
||||
await createAuditLog(app.db, request, {
|
||||
organizationId: orgId,
|
||||
@@ -83,7 +112,7 @@ export default async function backupRoutes(app: FastifyInstance) {
|
||||
metadata: { name: body.name },
|
||||
});
|
||||
|
||||
return reply.code(201).send(backup);
|
||||
return reply.code(201).send(completedBackup);
|
||||
});
|
||||
|
||||
// POST /backups/:backupId/restore — restore a backup
|
||||
@@ -95,10 +124,7 @@ export default async function backupRoutes(app: FastifyInstance) {
|
||||
};
|
||||
await requirePermission(request, orgId, 'backup.restore');
|
||||
|
||||
const server = await app.db.query.servers.findFirst({
|
||||
where: and(eq(servers.id, serverId), eq(servers.organizationId, orgId)),
|
||||
});
|
||||
if (!server) throw AppError.notFound('Server not found');
|
||||
const serverContext = await getServerBackupContext(app, orgId, serverId);
|
||||
|
||||
const backup = await app.db.query.backups.findFirst({
|
||||
where: and(eq(backups.id, backupId), eq(backups.serverId, serverId)),
|
||||
@@ -106,12 +132,20 @@ export default async function backupRoutes(app: FastifyInstance) {
|
||||
if (!backup) throw AppError.notFound('Backup not found');
|
||||
if (!backup.completedAt) throw AppError.badRequest('Backup is not yet completed');
|
||||
|
||||
// TODO: Send gRPC RestoreBackup to daemon
|
||||
// Daemon will:
|
||||
// 1. Stop the server
|
||||
// 2. Download backup from @source/cdn
|
||||
// 3. Extract tar.gz over server directory
|
||||
// 4. Start the server
|
||||
try {
|
||||
await daemonRestoreBackup(
|
||||
serverContext.node,
|
||||
serverContext.serverUuid,
|
||||
backup.id,
|
||||
backup.cdnPath,
|
||||
);
|
||||
} catch (error) {
|
||||
request.log.error(
|
||||
{ error, serverId, backupId },
|
||||
'Failed to restore backup on daemon',
|
||||
);
|
||||
throw new AppError(502, 'Failed to restore backup on daemon', 'DAEMON_BACKUP_RESTORE_FAILED');
|
||||
}
|
||||
|
||||
await createAuditLog(app.db, request, {
|
||||
organizationId: orgId,
|
||||
@@ -161,7 +195,17 @@ export default async function backupRoutes(app: FastifyInstance) {
|
||||
if (!backup) throw AppError.notFound('Backup not found');
|
||||
if (backup.isLocked) throw AppError.badRequest('Cannot delete a locked backup');
|
||||
|
||||
// TODO: Send gRPC DeleteBackup to daemon to remove from CDN
|
||||
const serverContext = await getServerBackupContext(app, orgId, serverId);
|
||||
|
||||
try {
|
||||
await daemonDeleteBackup(serverContext.node, serverContext.serverUuid, backup.id);
|
||||
} catch (error) {
|
||||
request.log.error(
|
||||
{ error, serverId, backupId },
|
||||
'Failed to delete backup on daemon',
|
||||
);
|
||||
throw new AppError(502, 'Failed to delete backup on daemon', 'DAEMON_BACKUP_DELETE_FAILED');
|
||||
}
|
||||
|
||||
await app.db.delete(backups).where(eq(backups.id, backupId));
|
||||
|
||||
@@ -175,3 +219,33 @@ export default async function backupRoutes(app: FastifyInstance) {
|
||||
return reply.code(204).send();
|
||||
});
|
||||
}
|
||||
|
||||
async function getServerBackupContext(
|
||||
app: FastifyInstance,
|
||||
orgId: string,
|
||||
serverId: string,
|
||||
): Promise<{ serverUuid: string; node: DaemonNodeConnection }> {
|
||||
const [server] = await app.db
|
||||
.select({
|
||||
serverUuid: 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.serverUuid,
|
||||
node: {
|
||||
fqdn: server.nodeFqdn,
|
||||
grpcPort: server.nodeGrpcPort,
|
||||
daemonToken: server.nodeDaemonToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user