mirror of
https://github.com/khairul169/db-backup-tool.git
synced 2026-09-16 17:33:26 +07:00
chore: initial build
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
dist/
|
||||
node_modules/
|
||||
storage/
|
||||
public/
|
||||
package-lock.json
|
||||
bun.lockb
|
||||
.env
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/main.ts",
|
||||
"dev:compose": "cp ../bun.lockb . && docker compose -f docker-compose.dev.yml up --build",
|
||||
"build": "bun build src/main.ts --outdir dist --target bun",
|
||||
"build": "NODE_ENV=production bun build src/main.ts --outdir dist --target bun",
|
||||
"start": "bun dist/main.js",
|
||||
"generate": "drizzle-kit generate",
|
||||
"migrate": "bun src/db/migrate.ts",
|
||||
@@ -14,10 +14,8 @@
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/node-schedule": "^2.1.7",
|
||||
"drizzle-kit": "^0.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
"drizzle-kit": "^0.21.0",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/zod-validator": "^0.2.1",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import path from "path";
|
||||
|
||||
export const __PROD = process.env.NODE_ENV === "production";
|
||||
export const __DEV = !__PROD;
|
||||
export const DOCKER_HOST = "host.docker.internal";
|
||||
export const STORAGE_DIR = path.resolve(__dirname, "../storage");
|
||||
export const STORAGE_DIR = path.resolve(process.cwd(), "storage");
|
||||
export const BACKUP_DIR = STORAGE_DIR + "/backups";
|
||||
export const DATABASE_PATH = path.join(STORAGE_DIR, "database.db");
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import fs from "fs";
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import { migrate as migrator } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import { DATABASE_PATH } from "../consts";
|
||||
import db, { sqlite } from ".";
|
||||
import { seed } from "./seed";
|
||||
|
||||
const initializeData = fs.existsSync(DATABASE_PATH);
|
||||
const initializeData = !fs.existsSync(DATABASE_PATH);
|
||||
|
||||
await migrate(db, {
|
||||
migrationsFolder: __dirname + "/migrations",
|
||||
});
|
||||
const migrate = async () => {
|
||||
migrator(db, {
|
||||
migrationsFolder: __dirname + "/migrations",
|
||||
});
|
||||
|
||||
if (initializeData) {
|
||||
await seed();
|
||||
}
|
||||
if (initializeData) {
|
||||
await seed();
|
||||
}
|
||||
|
||||
await sqlite.close();
|
||||
sqlite.close();
|
||||
};
|
||||
|
||||
migrate();
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import BaseDbms from "./dbms/base";
|
||||
import PostgresDbms from "./dbms/postgres";
|
||||
import type { DatabaseConfig, DatabaseListItem } from "../types/database.types";
|
||||
import type {
|
||||
DatabaseConfig,
|
||||
DatabaseListItem,
|
||||
DumpOptions,
|
||||
} from "../types/database.types";
|
||||
|
||||
class DatabaseUtil {
|
||||
private db = new BaseDbms();
|
||||
@@ -19,8 +23,12 @@ class DatabaseUtil {
|
||||
return this.db.getDatabases();
|
||||
}
|
||||
|
||||
async dump(dbName: string, path: string): Promise<string> {
|
||||
return this.db.dump(dbName, path);
|
||||
async dump(
|
||||
dbName: string,
|
||||
path: string,
|
||||
options?: DumpOptions
|
||||
): Promise<string> {
|
||||
return this.db.dump(dbName, path, options);
|
||||
}
|
||||
|
||||
async restore(path: string): Promise<string> {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { DatabaseListItem } from "../../types/database.types";
|
||||
import type { DatabaseListItem, DumpOptions } from "../../types/database.types";
|
||||
|
||||
class BaseDbms {
|
||||
async getDatabases(): Promise<DatabaseListItem[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async dump(_dbName: string, _path: string): Promise<string> {
|
||||
async dump(
|
||||
_dbName: string,
|
||||
_path: string,
|
||||
_options?: DumpOptions
|
||||
): Promise<string> {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type {
|
||||
DatabaseListItem,
|
||||
DumpOptions,
|
||||
PostgresConfig,
|
||||
} from "../../types/database.types";
|
||||
import path from "path";
|
||||
import { exec } from "../../utility/process";
|
||||
import { urlencode } from "../../utility/utils";
|
||||
import BaseDbms from "./base";
|
||||
@@ -18,21 +20,33 @@ class PostgresDbms extends BaseDbms {
|
||||
);
|
||||
}
|
||||
|
||||
async dump(dbName: string, path: string) {
|
||||
return exec(["pg_dump", this.dbUrl + `/${dbName}`, "-Z9", "-f", path]);
|
||||
async dump(dbName: string, path: string, options: DumpOptions = {}) {
|
||||
const { compress } = options;
|
||||
const ext = compress ? ".gz" : ".sql";
|
||||
const filename = path + ext;
|
||||
|
||||
await exec([
|
||||
"pg_dump",
|
||||
this.dbUrl + `/${dbName}`,
|
||||
"-Cc",
|
||||
compress ? "-Z9" : null,
|
||||
"-f",
|
||||
filename,
|
||||
]);
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
async restore(path: string) {
|
||||
return exec([
|
||||
"pg_restore",
|
||||
"-d",
|
||||
this.dbUrl,
|
||||
"-cC",
|
||||
"--if-exists",
|
||||
"--exit-on-error",
|
||||
// "-Ftar",
|
||||
path,
|
||||
]);
|
||||
async restore(backupFile: string) {
|
||||
const ext = path.extname(backupFile);
|
||||
const isCompressed = ext === ".gz";
|
||||
let cmd = `psql ${this.dbUrl} < ${backupFile}`;
|
||||
|
||||
if (isCompressed) {
|
||||
cmd = `zcat ${backupFile} | psql ${this.dbUrl}`;
|
||||
}
|
||||
|
||||
return exec(["sh", "-c", cmd]);
|
||||
}
|
||||
|
||||
private async sql<T = any>(query: string) {
|
||||
|
||||
+17
-3
@@ -1,8 +1,22 @@
|
||||
import { Hono } from "hono";
|
||||
import routers from "./routers";
|
||||
import { initScheduler } from "./schedulers";
|
||||
import { __PROD } from "./consts";
|
||||
import { serveStatic } from "hono/bun";
|
||||
|
||||
console.log("Starting app..");
|
||||
|
||||
const app = new Hono();
|
||||
initScheduler();
|
||||
|
||||
export default routers;
|
||||
// Add API routes
|
||||
app.route(__PROD ? "/api" : "/", routers);
|
||||
|
||||
// Serve frontend
|
||||
if (__PROD) {
|
||||
app.use(serveStatic({ root: "./public" }));
|
||||
app.use("*", serveStatic({ path: "./public/index.html" }));
|
||||
|
||||
const PORT = Number(process.env.PORT) || 3000;
|
||||
console.log(`App listening on http://localhost:${PORT}`);
|
||||
}
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { processBackup } from "../schedulers/process-backup";
|
||||
import {
|
||||
createBackupSchema,
|
||||
getAllBackupQuery,
|
||||
@@ -18,12 +19,22 @@ const router = new Hono()
|
||||
|
||||
.post("/", zValidator("json", createBackupSchema), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
return c.json(await backupService.create(body));
|
||||
const result = await backupService.create(body);
|
||||
|
||||
// start backup scheduler
|
||||
processBackup();
|
||||
|
||||
return c.json(result);
|
||||
})
|
||||
|
||||
.post("/restore", zValidator("json", restoreBackupSchema), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
return c.json(await backupService.restore(body));
|
||||
const result = await backupService.restore(body);
|
||||
|
||||
// start restore scheduler
|
||||
processBackup();
|
||||
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -46,7 +46,7 @@ const router = new Hono()
|
||||
return c.json({ success: true, databases });
|
||||
} catch (err) {
|
||||
throw new HTTPException(400, {
|
||||
message: "Cannot connect to the database.",
|
||||
message: (err as any).message || "Cannot connect to the database.",
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,6 +4,5 @@ import { backupScheduler } from "./backup-scheduler";
|
||||
|
||||
export const initScheduler = () => {
|
||||
scheduler.scheduleJob("*/10 * * * * *", processBackup);
|
||||
// scheduler.scheduleJob("* * * * * *", backupScheduler);
|
||||
backupScheduler();
|
||||
scheduler.scheduleJob("* * * * * *", backupScheduler);
|
||||
};
|
||||
|
||||
@@ -25,11 +25,15 @@ const runBackup = async (task: PendingTasks[number]) => {
|
||||
|
||||
if (task.type === "backup") {
|
||||
const key = path.join(server.connection.host, dbName, `${Date.now()}`);
|
||||
const outFile = path.join(BACKUP_DIR, key);
|
||||
let outFile = path.join(BACKUP_DIR, key);
|
||||
mkdir(path.dirname(outFile));
|
||||
|
||||
// Run database dump command
|
||||
const output = await dbUtil.dump(dbName, outFile);
|
||||
const filename = await dbUtil.dump(dbName, outFile, {
|
||||
compress: task.server.backup?.compress,
|
||||
});
|
||||
const ext = path.extname(filename);
|
||||
outFile = outFile + ext;
|
||||
|
||||
// Get file stats and file checksum
|
||||
const fileStats = fs.statSync(outFile);
|
||||
@@ -40,8 +44,8 @@ const runBackup = async (task: PendingTasks[number]) => {
|
||||
.update(backupModel)
|
||||
.set({
|
||||
status: "success",
|
||||
output,
|
||||
key,
|
||||
output: "",
|
||||
key: key + ext,
|
||||
hash: sha256Hash,
|
||||
size: fileStats.size,
|
||||
})
|
||||
@@ -90,7 +94,7 @@ const getPendingTasks = async () => {
|
||||
orderBy: (i) => asc(i.createdAt),
|
||||
with: {
|
||||
server: {
|
||||
columns: { connection: true, ssh: true },
|
||||
columns: { connection: true, ssh: true, backup: true },
|
||||
},
|
||||
database: {
|
||||
columns: { name: true },
|
||||
|
||||
@@ -106,6 +106,12 @@ export default class BackupService {
|
||||
const backup = await this.getOrFail(data.backupId);
|
||||
await this.checkPendingBackup(backup.databaseId);
|
||||
|
||||
if (backup.status !== "success") {
|
||||
throw new HTTPException(400, {
|
||||
message: "Cannot restore backup that is not success.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!backup.key) {
|
||||
throw new HTTPException(400, {
|
||||
message: "Cannot restore backup without file key.",
|
||||
|
||||
@@ -82,7 +82,12 @@ export default class ServerService {
|
||||
}))
|
||||
);
|
||||
|
||||
return data;
|
||||
const server = this.parse(result);
|
||||
if (server.connection?.pass) {
|
||||
delete server.connection.pass;
|
||||
}
|
||||
|
||||
return server;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,3 +12,7 @@ export type DatabaseListItem = {
|
||||
name: string;
|
||||
size: string;
|
||||
};
|
||||
|
||||
export type DumpOptions = Partial<{
|
||||
compress: boolean;
|
||||
}>;
|
||||
|
||||
@@ -5,10 +5,10 @@ type ExecOptions = {
|
||||
};
|
||||
|
||||
export const exec = async (
|
||||
cmds: string[],
|
||||
cmds: (string | null | undefined)[],
|
||||
options: Partial<ExecOptions> = {}
|
||||
) => {
|
||||
const proc = Bun.spawn(cmds, {
|
||||
const proc = Bun.spawn(cmds.filter((i) => i != null) as string[], {
|
||||
env: options.env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user