Updates
This commit is contained in:
parent
3faf4be368
commit
225e3eba4c
72
README.md
72
README.md
@ -20,6 +20,8 @@ A schema-driven MariaDB manager for [Bun](https://bun.sh), featuring automatic s
|
|||||||
- [`typedef`](#typedef--generate-typescript-types-only)
|
- [`typedef`](#typedef--generate-typescript-types-only)
|
||||||
- [`backup`](#backup--sql-dump-backup)
|
- [`backup`](#backup--sql-dump-backup)
|
||||||
- [`restore`](#restore--restore-from-an-sql-dump)
|
- [`restore`](#restore--restore-from-an-sql-dump)
|
||||||
|
- [`export`](#export--portable-sql--schema-archive)
|
||||||
|
- [`import`](#import--sql-or-full-export-archive)
|
||||||
- [`admin`](#admin--interactive-admin-tools)
|
- [`admin`](#admin--interactive-admin-tools)
|
||||||
- [CRUD API](#crud-api)
|
- [CRUD API](#crud-api)
|
||||||
- [Select](#select)
|
- [Select](#select)
|
||||||
@ -191,6 +193,7 @@ The config file must be named `bun-mariadb.config.ts` and placed at the project
|
|||||||
| `db_dir` | `string` | Yes | Directory for schema, types, and local artifacts (relative to project root) |
|
| `db_dir` | `string` | Yes | Directory for schema, types, and local artifacts (relative to project root) |
|
||||||
| `db_backup_dir` | `string` | No | Backup directory name, relative to `db_dir` (default: `.backups`) |
|
| `db_backup_dir` | `string` | No | Backup directory name, relative to `db_dir` (default: `.backups`) |
|
||||||
| `max_backups` | `number` | No | Max backup files to keep (default: `10`) |
|
| `max_backups` | `number` | No | Max backup files to keep (default: `10`) |
|
||||||
|
| `max_exports` | `number` | No | Max export archives to keep (default: `10`) |
|
||||||
| `typedef_file_path` | `string` | No | Output path for generated TypeScript types (relative to project root) |
|
| `typedef_file_path` | `string` | No | Output path for generated TypeScript types (relative to project root) |
|
||||||
| `db_config` | `object` | No | Extra options passed to Bun's `SQL` MariaDB adapter |
|
| `db_config` | `object` | No | Extra options passed to Bun's `SQL` MariaDB adapter |
|
||||||
| `charset` | `string` | No | Database charset (default: `utf8mb4`) |
|
| `charset` | `string` | No | Database charset (default: `utf8mb4`) |
|
||||||
@ -359,6 +362,75 @@ Interactive picker of available `.sql` backups (newest first). Imports the selec
|
|||||||
|
|
||||||
Requires `mariadb` or `mysql` on `PATH`.
|
Requires `mariadb` or `mysql` on `PATH`.
|
||||||
|
|
||||||
|
### `export` — Portable SQL + schema archive
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bunx bun-mariadb export [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| ------------------- | --------------------------------------------------------------------------- |
|
||||||
|
| `-o`, `--output` | Output archive path (`.tar.gz` or `.zip`). Defaults to `{db_dir}/.exports` |
|
||||||
|
| `-f`, `--format` | When `--output` is omitted: `tar.gz` (default) or `zip` |
|
||||||
|
|
||||||
|
Creates a portable archive containing:
|
||||||
|
|
||||||
|
1. `dump.sql` — full SQL dump (same options as `backup`)
|
||||||
|
2. `schema.ts` — the project’s TypeScript schema file from `db_dir`
|
||||||
|
|
||||||
|
Archives are written to `{db_dir}/.exports` (sibling of the backups directory). Old exports are pruned using `max_exports`.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Write {db_name}-{timestamp}.tar.gz into {db_dir}/.exports
|
||||||
|
bunx bun-mariadb export
|
||||||
|
|
||||||
|
# Explicit path / format
|
||||||
|
bunx bun-mariadb export -o ./my-app-export.tar.gz
|
||||||
|
bunx bun-mariadb export -f zip
|
||||||
|
bunx bun-mariadb export -o ./my-app-export.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `mariadb-dump` or `mysqldump` on `PATH`. Zip output also requires `zip`.
|
||||||
|
|
||||||
|
### `import` — SQL or full export archive
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bunx bun-mariadb import [file] [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| ---------------- | --------------------------------------------------------------------------- |
|
||||||
|
| `[file]` | Path to a `.sql` dump or export archive (`.tar.gz` / `.tar` / `.zip`) |
|
||||||
|
| `--sql-only` | Archive only: restore SQL, do not overwrite `schema.ts` |
|
||||||
|
| `--schema-only` | Archive only: write `schema.ts`, do not restore SQL |
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
- **`.sql` file** — restores SQL into the configured database (same as restore, but non-interactive)
|
||||||
|
- **Export archive** — restores `dump.sql` and writes `schema.ts` into `db_dir`
|
||||||
|
- **No file argument** — interactive picker of archives in `{db_dir}/.exports`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Plain SQL dump
|
||||||
|
bunx bun-mariadb import ./db/.backups/my_app-1710000000000.sql
|
||||||
|
|
||||||
|
# Full export (SQL + schema.ts)
|
||||||
|
bunx bun-mariadb import ./db/exports/my_app-1710000000000.tar.gz
|
||||||
|
|
||||||
|
# Archive: SQL only / schema only
|
||||||
|
bunx bun-mariadb import ./export.tar.gz --sql-only
|
||||||
|
bunx bun-mariadb import ./export.tar.gz --schema-only
|
||||||
|
|
||||||
|
# Interactive picker (from {db_dir}/.exports)
|
||||||
|
bunx bun-mariadb import
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `mariadb` or `mysql` on `PATH`. Zip archives also require `unzip`.
|
||||||
|
|
||||||
### `admin` — Interactive admin tools
|
### `admin` — Interactive admin tools
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -5,30 +5,7 @@ import grabDBDir from "../utils/grab-db-dir";
|
|||||||
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
|
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import trimBackups from "../utils/trim-backups";
|
import trimBackups from "../utils/trim-backups";
|
||||||
import mariadbCliEnv, {
|
import { dumpDatabase } from "../utils/mariadb-dump-restore";
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("backup")
|
return new Command("backup")
|
||||||
@ -46,40 +23,9 @@ export default function () {
|
|||||||
const backup_file_name = grabDBBackupFileName({ config });
|
const backup_file_name = grabDBBackupFileName({ config });
|
||||||
const backup_path = path.join(backup_dir, backup_file_name);
|
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 {
|
try {
|
||||||
const proc = Bun.spawn(args, {
|
const sql = await dumpDatabase(config);
|
||||||
stdout: "pipe",
|
fs.writeFileSync(backup_path, sql, "utf-8");
|
||||||
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");
|
|
||||||
trimBackups({ config });
|
trimBackups({ config });
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@ -87,11 +33,7 @@ export default function () {
|
|||||||
);
|
);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(
|
console.error(chalk.red(`Backup ERROR => ${error.message}`));
|
||||||
chalk.red(
|
|
||||||
`Backup ERROR => ${error.message}. Ensure \`mariadb-dump\` or \`mysqldump\` is installed.`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
95
src/commands/export.ts
Normal file
95
src/commands/export.ts
Normal 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
src/commands/import.ts
Normal file
143
src/commands/import.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -5,6 +5,8 @@ import schema from "./schema";
|
|||||||
import typedef from "./typedef";
|
import typedef from "./typedef";
|
||||||
import backup from "./backup";
|
import backup from "./backup";
|
||||||
import restore from "./restore";
|
import restore from "./restore";
|
||||||
|
import exportCmd from "./export";
|
||||||
|
import importCmd from "./import";
|
||||||
import admin from "./admin";
|
import admin from "./admin";
|
||||||
import init from "../functions/init";
|
import init from "../functions/init";
|
||||||
|
|
||||||
@ -25,6 +27,8 @@ program.addCommand(schema());
|
|||||||
program.addCommand(typedef());
|
program.addCommand(typedef());
|
||||||
program.addCommand(backup());
|
program.addCommand(backup());
|
||||||
program.addCommand(restore());
|
program.addCommand(restore());
|
||||||
|
program.addCommand(exportCmd());
|
||||||
|
program.addCommand(importCmd());
|
||||||
program.addCommand(admin());
|
program.addCommand(admin());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,30 +6,7 @@ import grabSortedBackups from "../utils/grab-sorted-backups";
|
|||||||
import { select } from "@inquirer/prompts";
|
import { select } from "@inquirer/prompts";
|
||||||
import grabBackupData from "../utils/grab-backup-data";
|
import grabBackupData from "../utils/grab-backup-data";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import mariadbCliEnv, {
|
import { restoreDatabase } from "../utils/mariadb-dump-restore";
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("restore")
|
return new Command("restore")
|
||||||
@ -71,35 +48,8 @@ export default function () {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientBin = resolveClientBinary();
|
|
||||||
const sql = fs.readFileSync(backup_path, "utf-8");
|
const sql = fs.readFileSync(backup_path, "utf-8");
|
||||||
|
await restoreDatabase(config, sql);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.bold(chalk.green(`DB Restore Success!`))} ← ${selected_backup}`,
|
`${chalk.bold(chalk.green(`DB Restore Success!`))} ← ${selected_backup}`,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
export const AppData = {
|
export const AppData = {
|
||||||
ConfigFileName: "bun-mariadb.config.ts",
|
ConfigFileName: "bun-mariadb.config.ts",
|
||||||
MaxBackups: 10,
|
MaxBackups: 10,
|
||||||
|
MaxExports: 10,
|
||||||
DefaultBackupDirName: ".backups",
|
DefaultBackupDirName: ".backups",
|
||||||
|
DefaultExportDirName: ".exports",
|
||||||
DbSchemaManagerTableName: "__db_schema_manager__",
|
DbSchemaManagerTableName: "__db_schema_manager__",
|
||||||
DbSchemaFileName: "schema.ts",
|
DbSchemaFileName: "schema.ts",
|
||||||
MaxInitRetries: 50,
|
MaxInitRetries: 50,
|
||||||
|
|||||||
@ -95,6 +95,14 @@ export default function init(): void {
|
|||||||
fs.mkdirSync(BackupDir, { recursive: true });
|
fs.mkdirSync(BackupDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ExportDir = path.resolve(
|
||||||
|
db_dir,
|
||||||
|
AppData["DefaultExportDirName"],
|
||||||
|
);
|
||||||
|
if (!fs.existsSync(ExportDir)) {
|
||||||
|
fs.mkdirSync(ExportDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
global.CONFIG = Config;
|
global.CONFIG = Config;
|
||||||
global.DB_SCHEMA = DbSchema;
|
global.DB_SCHEMA = DbSchema;
|
||||||
|
|
||||||
|
|||||||
@ -1518,6 +1518,10 @@ export type BunMariaDBConfig = {
|
|||||||
*/
|
*/
|
||||||
db_backup_dir?: string;
|
db_backup_dir?: string;
|
||||||
max_backups?: number;
|
max_backups?: number;
|
||||||
|
/**
|
||||||
|
* Max export archives to keep in `{db_dir}/.exports` (default: 10)
|
||||||
|
*/
|
||||||
|
max_exports?: number;
|
||||||
/**
|
/**
|
||||||
* Root directory for schema, types, and local artifacts (relative to project root)
|
* Root directory for schema, types, and local artifacts (relative to project root)
|
||||||
*/
|
*/
|
||||||
|
|||||||
252
src/utils/export-archive.ts
Normal file
252
src/utils/export-archive.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
|
|
||||||
|
export const ExportArchiveMembers = {
|
||||||
|
SqlFileName: "dump.sql",
|
||||||
|
SchemaFileName: AppData.DbSchemaFileName,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ExportArchiveContents = {
|
||||||
|
sql: string;
|
||||||
|
schemaTs: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"] as const;
|
||||||
|
|
||||||
|
export function isArchivePath(filePath: string): boolean {
|
||||||
|
const lower = filePath.toLowerCase();
|
||||||
|
return ARCHIVE_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSqlPath(filePath: string): boolean {
|
||||||
|
return filePath.toLowerCase().endsWith(".sql");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema.ts.
|
||||||
|
*/
|
||||||
|
export async function writeExportArchive({
|
||||||
|
contents,
|
||||||
|
outPath,
|
||||||
|
}: {
|
||||||
|
contents: ExportArchiveContents;
|
||||||
|
outPath: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const lower = outPath.toLowerCase();
|
||||||
|
if (lower.endsWith(".zip")) {
|
||||||
|
await writeZipArchive({ contents, outPath });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = {
|
||||||
|
[ExportArchiveMembers.SqlFileName]: contents.sql,
|
||||||
|
[ExportArchiveMembers.SchemaFileName]: contents.schemaTs,
|
||||||
|
};
|
||||||
|
|
||||||
|
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
||||||
|
if (gzip) {
|
||||||
|
await Bun.Archive.write(outPath, members, { compress: "gzip" });
|
||||||
|
} else {
|
||||||
|
await Bun.Archive.write(outPath, members);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a portable export archive (tar / tar.gz / zip).
|
||||||
|
*/
|
||||||
|
export async function readExportArchive(
|
||||||
|
archivePath: string,
|
||||||
|
): Promise<ExportArchiveContents> {
|
||||||
|
const lower = archivePath.toLowerCase();
|
||||||
|
|
||||||
|
if (lower.endsWith(".zip")) {
|
||||||
|
return readZipArchive(archivePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await Bun.file(archivePath).bytes();
|
||||||
|
const archive = new Bun.Archive(bytes);
|
||||||
|
const files = await archive.files();
|
||||||
|
|
||||||
|
const sql =
|
||||||
|
(await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ??
|
||||||
|
(await readFirstMatching(files, (name) => name.endsWith(".sql")));
|
||||||
|
|
||||||
|
const schemaTs =
|
||||||
|
(await readArchiveMember(
|
||||||
|
files,
|
||||||
|
ExportArchiveMembers.SchemaFileName,
|
||||||
|
)) ??
|
||||||
|
(await readFirstMatching(files, (name) => name.endsWith("schema.ts")));
|
||||||
|
|
||||||
|
if (!sql) {
|
||||||
|
throw new Error(
|
||||||
|
`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schemaTs) {
|
||||||
|
throw new Error(
|
||||||
|
`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sql, schemaTs };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readArchiveMember(
|
||||||
|
files: Map<string, File>,
|
||||||
|
name: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
// Exact match, or basename match for nested paths
|
||||||
|
for (const [entry, file] of files) {
|
||||||
|
if (entry === name || path.basename(entry) === name) {
|
||||||
|
return await file.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFirstMatching(
|
||||||
|
files: Map<string, File>,
|
||||||
|
predicate: (name: string) => boolean,
|
||||||
|
): Promise<string | null> {
|
||||||
|
for (const [entry, file] of files) {
|
||||||
|
if (predicate(entry) || predicate(path.basename(entry))) {
|
||||||
|
return await file.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeZipArchive({
|
||||||
|
contents,
|
||||||
|
outPath,
|
||||||
|
}: {
|
||||||
|
contents: ExportArchiveContents;
|
||||||
|
outPath: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { BUN_MARIADB_TEMP_DIR } = grabDirNames();
|
||||||
|
const tempDir = path.join(
|
||||||
|
BUN_MARIADB_TEMP_DIR,
|
||||||
|
`export-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName);
|
||||||
|
const schemaPath = path.join(
|
||||||
|
tempDir,
|
||||||
|
ExportArchiveMembers.SchemaFileName,
|
||||||
|
);
|
||||||
|
fs.writeFileSync(sqlPath, contents.sql, "utf-8");
|
||||||
|
fs.writeFileSync(schemaPath, contents.schemaTs, "utf-8");
|
||||||
|
|
||||||
|
const absOut = path.resolve(outPath);
|
||||||
|
const proc = Bun.spawn(
|
||||||
|
[
|
||||||
|
"zip",
|
||||||
|
"-q",
|
||||||
|
"-j",
|
||||||
|
absOut,
|
||||||
|
ExportArchiveMembers.SqlFileName,
|
||||||
|
ExportArchiveMembers.SchemaFileName,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: tempDir,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [stderr, exitCode] = await Promise.all([
|
||||||
|
new Response(proc.stderr).text(),
|
||||||
|
proc.exited,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`zip failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`zip\` is installed, or use .tar.gz.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readZipArchive(
|
||||||
|
archivePath: string,
|
||||||
|
): Promise<ExportArchiveContents> {
|
||||||
|
const { BUN_MARIADB_TEMP_DIR } = grabDirNames();
|
||||||
|
const tempDir = path.join(
|
||||||
|
BUN_MARIADB_TEMP_DIR,
|
||||||
|
`import-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const absArchive = path.resolve(archivePath);
|
||||||
|
const proc = Bun.spawn(["unzip", "-q", "-o", absArchive, "-d", tempDir], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [stderr, exitCode] = await Promise.all([
|
||||||
|
new Response(proc.stderr).text(),
|
||||||
|
proc.exited,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`unzip failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`unzip\` is installed.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = findFileContents(tempDir, (name) =>
|
||||||
|
name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql"),
|
||||||
|
);
|
||||||
|
const schemaTs = findFileContents(
|
||||||
|
tempDir,
|
||||||
|
(name) =>
|
||||||
|
name === ExportArchiveMembers.SchemaFileName ||
|
||||||
|
name.endsWith("schema.ts"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sql) {
|
||||||
|
throw new Error(
|
||||||
|
`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!schemaTs) {
|
||||||
|
throw new Error(
|
||||||
|
`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sql, schemaTs };
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFileContents(
|
||||||
|
dir: string,
|
||||||
|
predicate: (baseName: string) => boolean,
|
||||||
|
): string | null {
|
||||||
|
const stack = [dir];
|
||||||
|
while (stack.length) {
|
||||||
|
const current = stack.pop()!;
|
||||||
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||||
|
const full = path.join(current, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
stack.push(full);
|
||||||
|
} else if (predicate(entry.name)) {
|
||||||
|
return fs.readFileSync(full, "utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -3,10 +3,33 @@ type Params = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse timestamped backup names: `{db_name}-{timestamp}[.sql]`
|
* Strip known backup/export extensions from a file name.
|
||||||
|
*/
|
||||||
|
function stripBackupExtension(name: string): string {
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
if (lower.endsWith(".tar.gz")) {
|
||||||
|
return name.slice(0, -7);
|
||||||
|
}
|
||||||
|
if (lower.endsWith(".tgz")) {
|
||||||
|
return name.slice(0, -4);
|
||||||
|
}
|
||||||
|
if (lower.endsWith(".tar")) {
|
||||||
|
return name.slice(0, -4);
|
||||||
|
}
|
||||||
|
if (lower.endsWith(".zip")) {
|
||||||
|
return name.slice(0, -4);
|
||||||
|
}
|
||||||
|
if (lower.endsWith(".sql")) {
|
||||||
|
return name.slice(0, -4);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse timestamped backup/export names: `{db_name}-{timestamp}[.sql|.tar.gz|...]`
|
||||||
*/
|
*/
|
||||||
export default function grabBackupData({ backup_name }: Params) {
|
export default function grabBackupData({ backup_name }: Params) {
|
||||||
const normalized = backup_name.replace(/\.sql$/, "");
|
const normalized = stripBackupExtension(backup_name);
|
||||||
const backup_parts = normalized.split("-");
|
const backup_parts = normalized.split("-");
|
||||||
const backup_date_timestamp = Number(backup_parts.pop());
|
const backup_date_timestamp = Number(backup_parts.pop());
|
||||||
const origin_backup_name = backup_parts.join("-");
|
const origin_backup_name = backup_parts.join("-");
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export default function grabDBDir({ config }: Params) {
|
|||||||
config.db_backup_dir || AppData["DefaultBackupDirName"];
|
config.db_backup_dir || AppData["DefaultBackupDirName"];
|
||||||
|
|
||||||
const backup_dir = path.resolve(db_dir, backup_dir_name);
|
const backup_dir = path.resolve(db_dir, backup_dir_name);
|
||||||
|
const export_dir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
|
||||||
|
|
||||||
return { db_dir, backup_dir };
|
return { db_dir, backup_dir, export_dir };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,18 @@ type Params = {
|
|||||||
config: BunMariaDBConfig;
|
config: BunMariaDBConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function stripBackupExtension(name: string): string {
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
if (lower.endsWith(".tar.gz")) return name.slice(0, -7);
|
||||||
|
if (lower.endsWith(".tgz")) return name.slice(0, -4);
|
||||||
|
if (lower.endsWith(".tar")) return name.slice(0, -4);
|
||||||
|
if (lower.endsWith(".zip")) return name.slice(0, -4);
|
||||||
|
if (lower.endsWith(".sql")) return name.slice(0, -4);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
function backupTimestamp(name: string): number {
|
function backupTimestamp(name: string): number {
|
||||||
const base = name.replace(/\.sql$/, "");
|
const base = stripBackupExtension(name);
|
||||||
const ts = Number(base.split("-").pop());
|
const ts = Number(base.split("-").pop());
|
||||||
return Number.isFinite(ts) ? ts : 0;
|
return Number.isFinite(ts) ? ts : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/utils/grab-sorted-exports.ts
Normal file
34
src/utils/grab-sorted-exports.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import type { BunMariaDBConfig } from "../types";
|
||||||
|
import grabDBDir from "./grab-db-dir";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
config: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
function stripExportExtension(name: string): string {
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
if (lower.endsWith(".tar.gz")) return name.slice(0, -7);
|
||||||
|
if (lower.endsWith(".tgz")) return name.slice(0, -4);
|
||||||
|
if (lower.endsWith(".tar")) return name.slice(0, -4);
|
||||||
|
if (lower.endsWith(".zip")) return name.slice(0, -4);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportTimestamp(name: string): number {
|
||||||
|
const base = stripExportExtension(name);
|
||||||
|
const ts = Number(base.split("-").pop());
|
||||||
|
return Number.isFinite(ts) ? ts : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function grabSortedExports({ config }: Params) {
|
||||||
|
const { export_dir } = grabDBDir({ config });
|
||||||
|
|
||||||
|
if (!fs.existsSync(export_dir)) {
|
||||||
|
return [] as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const exports = fs.readdirSync(export_dir);
|
||||||
|
|
||||||
|
return exports.sort((a, b) => exportTimestamp(b) - exportTimestamp(a));
|
||||||
|
}
|
||||||
109
src/utils/mariadb-dump-restore.ts
Normal file
109
src/utils/mariadb-dump-restore.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../types";
|
||||||
|
import mariadbCliEnv, { mariadbCliConnectionArgs } from "./mariadb-cli-env";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefer mariadb-dump, fall back to mysqldump.
|
||||||
|
*/
|
||||||
|
export 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefer mariadb client, fall back to mysql.
|
||||||
|
*/
|
||||||
|
export 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dump the configured database to an SQL string.
|
||||||
|
*/
|
||||||
|
export async function dumpDatabase(config: BunMariaDBConfig): Promise<string> {
|
||||||
|
const dumpBin = resolveDumpBinary();
|
||||||
|
const args = [
|
||||||
|
dumpBin,
|
||||||
|
...mariadbCliConnectionArgs(),
|
||||||
|
"--single-transaction",
|
||||||
|
"--routines",
|
||||||
|
"--triggers",
|
||||||
|
"--events",
|
||||||
|
config.db_name,
|
||||||
|
];
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw new Error(
|
||||||
|
`Dump failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`mariadb-dump\` or \`mysqldump\` is installed.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore an SQL dump into the configured database.
|
||||||
|
*/
|
||||||
|
export async function restoreDatabase(
|
||||||
|
config: BunMariaDBConfig,
|
||||||
|
sql: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const clientBin = resolveClientBinary();
|
||||||
|
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) {
|
||||||
|
throw new Error(
|
||||||
|
`Restore failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`mariadb\` or \`mysql\` is installed.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/utils/trim-exports.ts
Normal file
24
src/utils/trim-exports.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import type { BunMariaDBConfig } from "../types";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
import grabDBDir from "./grab-db-dir";
|
||||||
|
import grabSortedExports from "./grab-sorted-exports";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
config: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function trimExports({ config }: Params) {
|
||||||
|
const { export_dir } = grabDBDir({ config });
|
||||||
|
const exports = grabSortedExports({ config });
|
||||||
|
const max_exports = config.max_exports || AppData["MaxExports"];
|
||||||
|
|
||||||
|
for (let i = 0; i < exports.length; i++) {
|
||||||
|
const export_name = exports[i];
|
||||||
|
if (!export_name) continue;
|
||||||
|
if (i > max_exports - 1) {
|
||||||
|
fs.unlinkSync(path.join(export_dir, export_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user