db-backup-tool/backend/src/services/backup.service.ts

145 lines
3.6 KiB
TypeScript

import db from "../db";
import { backupModel, databaseModel, serverModel } from "../db/models";
import type {
CreateBackupSchema,
GetAllBackupQuery,
RestoreBackupSchema,
} from "../schemas/backup.schema";
import { and, count, desc, eq, inArray } from "drizzle-orm";
import DatabaseService from "./database.service";
import { HTTPException } from "hono/http-exception";
import ServerService from "./server.service";
export default class BackupService {
private databaseService = new DatabaseService();
/**
* Get all backups
*/
async getAll(query: GetAllBackupQuery = {}) {
const { serverId, databaseId } = query;
const page = query.page || 1;
const limit = query.limit || 10;
const where = and(
serverId ? eq(backupModel.serverId, serverId) : undefined,
databaseId ? eq(backupModel.databaseId, databaseId) : undefined
);
const [totalRows] = await db
.select({ count: count() })
.from(backupModel)
.where(where)
.limit(1);
const backups = await db.query.backup.findMany({
where,
with: {
database: { columns: { name: true } },
},
orderBy: desc(serverModel.createdAt),
limit,
offset: (page - 1) * limit,
});
return { count: totalRows.count, rows: backups };
}
async getOrFail(id: string) {
const backup = await db.query.backup.findFirst({
where: eq(backupModel.id, id),
});
if (!backup) {
throw new HTTPException(404, { message: "Backup not found." });
}
return backup;
}
/**
* Queue new backup
*/
async create(data: CreateBackupSchema) {
if (data.databaseId) {
const database = await this.databaseService.getOrFail(data.databaseId);
await this.checkPendingBackup(database.id);
const [result] = await db
.insert(backupModel)
.values({
type: "backup",
serverId: database.serverId,
databaseId: database.id,
})
.returning();
return result;
} else if (data.serverId) {
const databases = await db.query.database.findMany({
where: and(
eq(databaseModel.serverId, data.serverId),
eq(databaseModel.isActive, true)
),
});
if (!databases.length) {
throw new HTTPException(400, {
message: "No active databases found for this server.",
});
}
const values = databases.map((d) => ({
type: "backup",
serverId: d.serverId,
databaseId: d.id,
}));
const result = await db
.insert(backupModel)
.values(values as never)
.returning();
return result;
}
return null;
}
async restore(data: RestoreBackupSchema) {
const backup = await this.getOrFail(data.backupId);
await this.checkPendingBackup(backup.databaseId);
if (!backup.key) {
throw new HTTPException(400, {
message: "Cannot restore backup without file key.",
});
}
const [result] = await db
.insert(backupModel)
.values({
type: "restore",
serverId: backup.serverId,
databaseId: backup.databaseId,
key: backup.key,
hash: backup.hash,
size: backup.size,
})
.returning();
return result;
}
async checkPendingBackup(databaseId: string) {
const hasOngoingBackup = await db.query.backup.findFirst({
where: and(
eq(backupModel.databaseId, databaseId),
inArray(backupModel.status, ["pending", "running"])
),
});
if (hasOngoingBackup) {
throw new HTTPException(400, {
message: "There is already an ongoing backup for this database",
});
}
}
}