chore: initial build

This commit is contained in:
2024-05-13 02:19:53 +07:00
parent 25ffaf93a2
commit bf1476d2fd
30 changed files with 230 additions and 58 deletions
+1
View File
@@ -1,6 +1,7 @@
dist/
node_modules/
storage/
public/
package-lock.json
bun.lockb
.env
+3 -5
View File
@@ -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",
+3 -1
View File
@@ -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");
+13 -9
View File
@@ -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();
+11 -3
View File
@@ -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> {
+6 -2
View File
@@ -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 "";
}
+27 -13
View File
@@ -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
View File
@@ -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;
+13 -2
View File
@@ -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;
+1 -1
View File
@@ -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.",
});
}
})
+1 -2
View File
@@ -4,6 +4,5 @@ import { backupScheduler } from "./backup-scheduler";
export const initScheduler = () => {
scheduler.scheduleJob("*/10 * * * * *", processBackup);
// scheduler.scheduleJob("* * * * * *", backupScheduler);
backupScheduler();
scheduler.scheduleJob("* * * * * *", backupScheduler);
};
+9 -5
View File
@@ -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 },
+6
View File
@@ -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.",
+6 -1
View File
@@ -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;
});
}
+4
View File
@@ -12,3 +12,7 @@ export type DatabaseListItem = {
name: string;
size: string;
};
export type DumpOptions = Partial<{
compress: boolean;
}>;
+2 -2
View File
@@ -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",
});