mirror of
https://github.com/khairul169/db-backup-tool.git
synced 2026-09-16 17:33:26 +07:00
chore: init project
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
dist/
|
||||
node_modules/
|
||||
storage/
|
||||
package-lock.json
|
||||
bun.lockb
|
||||
@@ -0,0 +1,24 @@
|
||||
FROM alpine:3.19.0
|
||||
|
||||
ENV GLIBC_VERSION 2.34-r0
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install bun
|
||||
ADD https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip bun-linux-x64.zip
|
||||
RUN apk add --no-cache --update unzip curl && \
|
||||
curl -Lo /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \
|
||||
curl -Lo glibc.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk" && \
|
||||
curl -Lo glibc-bin.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-bin-${GLIBC_VERSION}.apk" && \
|
||||
apk add --force-overwrite glibc-bin.apk glibc.apk && \
|
||||
/usr/glibc-compat/sbin/ldconfig /lib /usr/glibc-compat/lib && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
apk del curl && \
|
||||
rm -rf /var/cache/apk/* glibc.apk glibc-bin.apk
|
||||
|
||||
RUN unzip bun-linux-x64.zip && chmod +x ./bun-linux-x64/bun && mv ./bun-linux-x64/bun /usr/bin && rm -f bun-linux-x64.zip
|
||||
|
||||
# Add db clients
|
||||
RUN apk --no-cache add postgresql16-client
|
||||
|
||||
ENTRYPOINT ["bun", "run", "dev"]
|
||||
@@ -0,0 +1,28 @@
|
||||
FROM alpine:3.19
|
||||
WORKDIR /app
|
||||
|
||||
ENV GLIBC_VERSION 2.35-r1
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache --update unzip curl
|
||||
# curl -Lo /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \
|
||||
# curl -Lo glibc.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-${GLIBC_VERSION}.apk" && \
|
||||
# curl -Lo glibc-bin.apk "https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VERSION}/glibc-bin-${GLIBC_VERSION}.apk" && \
|
||||
# apk add --force-overwrite glibc-bin.apk glibc.apk && \
|
||||
# /usr/glibc-compat/sbin/ldconfig /lib /usr/glibc-compat/lib && \
|
||||
# echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
# apk del curl && \
|
||||
# rm -rf /var/cache/apk/* glibc.apk glibc-bin.apk
|
||||
|
||||
ADD https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip bun-linux-x64.zip
|
||||
# RUN unzip bun-linux-x64.zip && chmod +x ./bun-linux-x64/bun && mv ./bun-linux-x64/bun /usr/local/bin && rm -rf bun-linux-x64.zip
|
||||
RUN unzip bun-linux-x64.zip && ls bun-linux-x64 && ./bun-linux-x64/bun --version
|
||||
|
||||
RUN chmod +x /usr/local/bin/bun
|
||||
RUN /usr/local/bin/bun --version
|
||||
|
||||
# CMD ["bun", "--version"]
|
||||
|
||||
# RUN apk --no-cache add postgresql16-client
|
||||
|
||||
# ENTRYPOINT ["bun", "run", "dev"]
|
||||
@@ -0,0 +1,15 @@
|
||||
# backend
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.1.7. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
@@ -0,0 +1,12 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
backend:
|
||||
container_name: db-backup-be
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
volumes:
|
||||
- ./:/app:rw
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
@@ -0,0 +1,35 @@
|
||||
import DatabaseUtil from "@/lib/database";
|
||||
import { DOCKER_HOST, STORAGE_DIR } from "@/utility/consts";
|
||||
import { mkdir } from "@/utility/utils";
|
||||
import path from "path";
|
||||
|
||||
const main = async () => {
|
||||
try {
|
||||
const db = new DatabaseUtil({
|
||||
type: "postgres",
|
||||
host: DOCKER_HOST,
|
||||
user: "postgres",
|
||||
pass: "postgres",
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
const databases = await db.getDatabases();
|
||||
console.log(databases);
|
||||
|
||||
const dbName = "test";
|
||||
|
||||
// Create backup
|
||||
const outDir = path.join(STORAGE_DIR, db.config.host, dbName);
|
||||
mkdir(outDir);
|
||||
const outFile = path.join(outDir, `/${Date.now()}.tar`);
|
||||
console.log(await db.dump(dbName, outFile));
|
||||
console.log(outFile);
|
||||
|
||||
// Restore backup
|
||||
console.log(await db.restore(outFile));
|
||||
} catch (err) {
|
||||
console.log((err as any).message);
|
||||
}
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --watch index.ts",
|
||||
"dev:compose": "docker compose -f docker-compose.dev.yml up --build",
|
||||
"build": "bun build index.ts --outdir dist --target bun",
|
||||
"start": "bun dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { DatabaseListItem } from "../types/database.types";
|
||||
|
||||
class BaseDbms {
|
||||
async getDatabases(): Promise<DatabaseListItem[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async dump(_dbName: string, _path: string): Promise<string> {
|
||||
return "";
|
||||
}
|
||||
|
||||
async restore(_path: string): Promise<string> {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseDbms;
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { DatabaseListItem, PostgresConfig } from "../types/database.types";
|
||||
import { exec } from "../utility/process";
|
||||
import BaseDbms from "./base";
|
||||
|
||||
class PostgresDbms extends BaseDbms {
|
||||
constructor(private config: PostgresConfig) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getDatabases() {
|
||||
return this.sql<DatabaseListItem>(
|
||||
"SELECT datname AS name, pg_size_pretty(pg_database_size(datname)) AS size \
|
||||
FROM pg_database WHERE datistemplate=false ORDER BY name ASC"
|
||||
);
|
||||
}
|
||||
|
||||
async dump(dbName: string, path: string) {
|
||||
return exec(["pg_dump", this.dbUrl + `/${dbName}`, "-Ftar", "-f", path]);
|
||||
}
|
||||
|
||||
async restore(path: string) {
|
||||
return exec([
|
||||
"pg_restore",
|
||||
"-d",
|
||||
this.dbUrl,
|
||||
"-cC",
|
||||
"--if-exists",
|
||||
"--exit-on-error",
|
||||
"-Ftar",
|
||||
path,
|
||||
]);
|
||||
}
|
||||
|
||||
private async sql<T = any>(query: string) {
|
||||
const sql = `SELECT row_to_json(row) FROM (${query}) row`;
|
||||
return exec(["psql", this.dbUrl, "-t", "-c", sql])
|
||||
.then((res) => res.split("\n").map((i) => i.trim()))
|
||||
.then((res) => res.filter((i) => i.length > 0))
|
||||
.then((i) => i.map((data) => JSON.parse(data) as T));
|
||||
}
|
||||
|
||||
private get dbUrl() {
|
||||
const { user, pass, host } = this.config;
|
||||
const port = this.config.port || 5432;
|
||||
return `postgresql://${user}:${pass}@${host}:${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default PostgresDbms;
|
||||
@@ -0,0 +1,31 @@
|
||||
import BaseDbms from "../dbms/base";
|
||||
import PostgresDbms from "../dbms/postgres";
|
||||
import type { DatabaseConfig, DatabaseListItem } from "../types/database.types";
|
||||
|
||||
class DatabaseUtil {
|
||||
private db = new BaseDbms();
|
||||
|
||||
constructor(public config: DatabaseConfig) {
|
||||
switch (config.type) {
|
||||
case "postgres":
|
||||
this.db = new PostgresDbms(config);
|
||||
break;
|
||||
default:
|
||||
throw Error("Database type not supported: " + config.type);
|
||||
}
|
||||
}
|
||||
|
||||
async getDatabases(): Promise<DatabaseListItem[]> {
|
||||
return this.db.getDatabases();
|
||||
}
|
||||
|
||||
async dump(dbName: string, path: string): Promise<string> {
|
||||
return this.db.dump(dbName, path);
|
||||
}
|
||||
|
||||
async restore(path: string): Promise<string> {
|
||||
return this.db.restore(path);
|
||||
}
|
||||
}
|
||||
|
||||
export default DatabaseUtil;
|
||||
@@ -0,0 +1,14 @@
|
||||
export type DatabaseConfig = PostgresConfig;
|
||||
|
||||
export type PostgresConfig = {
|
||||
type: "postgres";
|
||||
host: string;
|
||||
user: string;
|
||||
pass: string;
|
||||
port?: number;
|
||||
};
|
||||
|
||||
export type DatabaseListItem = {
|
||||
name: string;
|
||||
size: string;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import path from "path";
|
||||
|
||||
export const DOCKER_HOST = "host.docker.internal";
|
||||
export const STORAGE_DIR = path.resolve(__dirname, "../../storage");
|
||||
@@ -0,0 +1,23 @@
|
||||
type ExecOptions = {
|
||||
env?: any;
|
||||
// TODO: add ssh wrapper
|
||||
ssh?: any;
|
||||
};
|
||||
|
||||
export const exec = async (
|
||||
cmds: string[],
|
||||
options: Partial<ExecOptions> = {}
|
||||
) => {
|
||||
const proc = Bun.spawn(cmds, {
|
||||
env: options.env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const err = await new Response(proc.stderr).text();
|
||||
const res = await new Response(proc.stdout).text();
|
||||
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import fs from "fs";
|
||||
|
||||
export const mkdir = (dir: string) => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user