This commit is contained in:
2026-07-14 09:34:10 +01:00
parent 3faf4be368
commit 225e3eba4c
16 changed files with 791 additions and 118 deletions
+4 -62
View File
@@ -5,30 +5,7 @@ import grabDBDir from "../utils/grab-db-dir";
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
import chalk from "chalk";
import trimBackups from "../utils/trim-backups";
import mariadbCliEnv, {
mariadbCliConnectionArgs,
} from "../utils/mariadb-cli-env";
/**
* Prefer mariadb-dump, fall back to mysqldump.
*/
function resolveDumpBinary(): string {
const candidates = ["mariadb-dump", "mysqldump"];
for (const bin of candidates) {
try {
const result = Bun.spawnSync(["which", bin], {
stdout: "pipe",
stderr: "pipe",
});
if (result.exitCode === 0) {
return new TextDecoder().decode(result.stdout).trim() || bin;
}
} catch {
// try next
}
}
return "mariadb-dump";
}
import { dumpDatabase } from "../utils/mariadb-dump-restore";
export default function () {
return new Command("backup")
@@ -46,40 +23,9 @@ export default function () {
const backup_file_name = grabDBBackupFileName({ config });
const backup_path = path.join(backup_dir, backup_file_name);
const dumpBin = resolveDumpBinary();
const args = [
dumpBin,
...mariadbCliConnectionArgs(),
"--single-transaction",
"--routines",
"--triggers",
"--events",
config.db_name,
];
try {
const proc = Bun.spawn(args, {
stdout: "pipe",
stderr: "pipe",
env: mariadbCliEnv(),
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
if (exitCode !== 0) {
console.error(
chalk.red(
`Backup failed (exit ${exitCode}): ${stderr || "unknown error"}`,
),
);
process.exit(1);
}
fs.writeFileSync(backup_path, stdout, "utf-8");
const sql = await dumpDatabase(config);
fs.writeFileSync(backup_path, sql, "utf-8");
trimBackups({ config });
console.log(
@@ -87,11 +33,7 @@ export default function () {
);
process.exit(0);
} catch (error: any) {
console.error(
chalk.red(
`Backup ERROR => ${error.message}. Ensure \`mariadb-dump\` or \`mysqldump\` is installed.`,
),
);
console.error(chalk.red(`Backup ERROR => ${error.message}`));
process.exit(1);
}
});
+95
View File
@@ -0,0 +1,95 @@
import { Command } from "commander";
import path from "path";
import fs from "fs";
import chalk from "chalk";
import grabDBDir from "../utils/grab-db-dir";
import { AppData } from "../data/app-data";
import { dumpDatabase } from "../utils/mariadb-dump-restore";
import { writeExportArchive } from "../utils/export-archive";
import trimExports from "../utils/trim-exports";
function defaultExportFileName(
dbName: string,
format: "tar.gz" | "zip",
): string {
return `${dbName}-${Date.now()}.${format}`;
}
export default function () {
return new Command("export")
.description(
"Export database SQL dump + schema.ts into a portable archive",
)
.option(
"-o, --output <path>",
"Output archive path (.tar.gz or .zip). Defaults to db exports dir",
)
.option(
"-f, --format <format>",
"Archive format when --output is omitted: tar.gz | zip",
"tar.gz",
)
.action(async (opts) => {
console.log(`Exporting database ...`);
const config = global.CONFIG;
const { db_dir, export_dir } = grabDBDir({ config });
if (!fs.existsSync(export_dir)) {
fs.mkdirSync(export_dir, { recursive: true });
}
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
if (!fs.existsSync(schemaPath)) {
console.error(
chalk.red(`Schema file not found: ${schemaPath}`),
);
process.exit(1);
}
const formatRaw = String(opts.format || "tar.gz").toLowerCase();
const format: "tar.gz" | "zip" =
formatRaw === "zip" ? "zip" : "tar.gz";
let outPath: string;
if (opts.output) {
outPath = path.resolve(opts.output);
const parent = path.dirname(outPath);
if (!fs.existsSync(parent)) {
fs.mkdirSync(parent, { recursive: true });
}
} else {
outPath = path.join(
export_dir,
defaultExportFileName(config.db_name, format),
);
}
try {
const sql = await dumpDatabase(config);
const schemaTs = fs.readFileSync(schemaPath, "utf-8");
await writeExportArchive({
contents: { sql, schemaTs },
outPath,
});
if (path.dirname(outPath) === export_dir) {
trimExports({ config });
}
console.log(
`${chalk.bold(chalk.green(`DB Export Success!`))}${outPath}`,
);
console.log(
chalk.dim(
`Contains: dump.sql + ${AppData.DbSchemaFileName}`,
),
);
process.exit(0);
} catch (error: any) {
console.error(chalk.red(`Export ERROR => ${error.message}`));
process.exit(1);
}
});
}
+143
View File
@@ -0,0 +1,143 @@
import { Command } from "commander";
import path from "path";
import fs from "fs";
import chalk from "chalk";
import { select } from "@inquirer/prompts";
import grabDBDir from "../utils/grab-db-dir";
import grabSortedExports from "../utils/grab-sorted-exports";
import grabBackupData from "../utils/grab-backup-data";
import { AppData } from "../data/app-data";
import { restoreDatabase } from "../utils/mariadb-dump-restore";
import {
isArchivePath,
isSqlPath,
readExportArchive,
} from "../utils/export-archive";
function formatChoice(name: string, index: number): string {
const { backup_date } = grabBackupData({ backup_name: name });
const time = Number.isNaN(backup_date.getTime())
? "unknown date"
: `${backup_date.toDateString()} ${backup_date.getHours()}:${backup_date.getMinutes()}:${backup_date.getSeconds().toString().padStart(2, "0")}`;
return `#${index + 1}: ${name} (${time})`;
}
export default function () {
return new Command("import")
.description(
"Import an SQL dump, or a full export archive (SQL + schema.ts)",
)
.argument(
"[file]",
"Path to .sql file or export archive (.tar.gz / .tar / .zip)",
)
.option(
"--sql-only",
"When importing an archive, restore SQL only (skip writing schema.ts)",
)
.option(
"--schema-only",
"When importing an archive, write schema.ts only (skip SQL restore)",
)
.action(async (fileArg: string | undefined, opts) => {
console.log(`Importing database ...`);
const config = global.CONFIG;
const { db_dir, export_dir } = grabDBDir({ config });
try {
let filePath = fileArg ? path.resolve(fileArg) : "";
if (!filePath) {
const candidates = grabSortedExports({ config }).filter(
(b) => isArchivePath(b),
);
if (!candidates[0]) {
console.error(
`No export archives found in \`${export_dir}\`. Use \`export\`, or pass a file path.`,
);
process.exit(1);
}
const selected = await select({
message: "Select an export to import:",
choices: candidates.map((b, i) => ({
name: formatChoice(b, i),
value: b,
})),
});
filePath = path.join(export_dir, selected);
}
if (!fs.existsSync(filePath)) {
console.error(chalk.red(`File not found: ${filePath}`));
process.exit(1);
}
if (isSqlPath(filePath)) {
if (opts.schemaOnly) {
console.error(
chalk.red(
`--schema-only requires an export archive, not a plain .sql file.`,
),
);
process.exit(1);
}
const sql = fs.readFileSync(filePath, "utf-8");
await restoreDatabase(config, sql);
console.log(
`${chalk.bold(chalk.green(`DB Import Success!`))}${filePath}`,
);
process.exit(0);
}
if (!isArchivePath(filePath)) {
console.error(
chalk.red(
`Unsupported file type: ${filePath}. Use .sql, .tar.gz, .tar, or .zip.`,
),
);
process.exit(1);
}
if (opts.sqlOnly && opts.schemaOnly) {
console.error(
chalk.red(
`Cannot combine --sql-only and --schema-only.`,
),
);
process.exit(1);
}
const { sql, schemaTs } = await readExportArchive(filePath);
if (!opts.schemaOnly) {
await restoreDatabase(config, sql);
console.log(chalk.green(`SQL restored from archive`));
}
if (!opts.sqlOnly) {
const schemaPath = path.join(
db_dir,
AppData.DbSchemaFileName,
);
fs.writeFileSync(schemaPath, schemaTs, "utf-8");
console.log(
chalk.green(`Schema written → ${schemaPath}`),
);
}
console.log(
`${chalk.bold(chalk.green(`DB Import Success!`))}${filePath}`,
);
process.exit(0);
} catch (error: any) {
console.error(chalk.red(`Import ERROR => ${error.message}`));
process.exit(1);
}
});
}
+4
View File
@@ -5,6 +5,8 @@ import schema from "./schema";
import typedef from "./typedef";
import backup from "./backup";
import restore from "./restore";
import exportCmd from "./export";
import importCmd from "./import";
import admin from "./admin";
import init from "../functions/init";
@@ -25,6 +27,8 @@ program.addCommand(schema());
program.addCommand(typedef());
program.addCommand(backup());
program.addCommand(restore());
program.addCommand(exportCmd());
program.addCommand(importCmd());
program.addCommand(admin());
/**
+2 -52
View File
@@ -6,30 +6,7 @@ import grabSortedBackups from "../utils/grab-sorted-backups";
import { select } from "@inquirer/prompts";
import grabBackupData from "../utils/grab-backup-data";
import path from "path";
import mariadbCliEnv, {
mariadbCliConnectionArgs,
} from "../utils/mariadb-cli-env";
/**
* Prefer mariadb client, fall back to mysql.
*/
function resolveClientBinary(): string {
const candidates = ["mariadb", "mysql"];
for (const bin of candidates) {
try {
const result = Bun.spawnSync(["which", bin], {
stdout: "pipe",
stderr: "pipe",
});
if (result.exitCode === 0) {
return new TextDecoder().decode(result.stdout).trim() || bin;
}
} catch {
// try next
}
}
return "mariadb";
}
import { restoreDatabase } from "../utils/mariadb-dump-restore";
export default function () {
return new Command("restore")
@@ -71,35 +48,8 @@ export default function () {
process.exit(1);
}
const clientBin = resolveClientBinary();
const sql = fs.readFileSync(backup_path, "utf-8");
const args = [
clientBin,
...mariadbCliConnectionArgs(),
config.db_name,
];
const proc = Bun.spawn(args, {
stdin: new Blob([sql]),
stdout: "pipe",
stderr: "pipe",
env: mariadbCliEnv(),
});
const [stderr, exitCode] = await Promise.all([
new Response(proc.stderr).text(),
proc.exited,
]);
if (exitCode !== 0) {
console.error(
chalk.red(
`Restore failed (exit ${exitCode}): ${stderr || "unknown error"}`,
),
);
process.exit(1);
}
await restoreDatabase(config, sql);
console.log(
`${chalk.bold(chalk.green(`DB Restore Success!`))}${selected_backup}`,