First Commit. Duplicate bunext-sqlite preset
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import runBunSQLiteCommand from "./run-bun-sqlite-command";
|
||||
|
||||
/**
|
||||
* Writes a new database snapshot via `bun-sqlite backup`. bun-sqlite prunes
|
||||
* old snapshots down to `max_backups`.
|
||||
*/
|
||||
export default async function createDbBackup() {
|
||||
return await runBunSQLiteCommand({ args: ["backup"] });
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import path from "path";
|
||||
import BunSQLiteConfig from "@/bun-sqlite.config";
|
||||
|
||||
/**
|
||||
* Absolute path of the bun-sqlite backups directory. Mirrors bun-sqlite's
|
||||
* own resolution: `{db_dir}/{db_backup_dir || ".backups"}`.
|
||||
*/
|
||||
export default function grabDbBackupDir() {
|
||||
return path.resolve(
|
||||
process.cwd(),
|
||||
BunSQLiteConfig.db_dir || "",
|
||||
BunSQLiteConfig.db_backup_dir || ".backups",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import BunSQLiteConfig from "@/bun-sqlite.config";
|
||||
import type { DbBackup } from "@/src/types";
|
||||
import grabDbBackupDir from "./grab-db-backup-dir";
|
||||
|
||||
/**
|
||||
* Lists the `{db_name}-{timestamp}` snapshots written by `bun-sqlite
|
||||
* backup`. Newest first.
|
||||
*/
|
||||
export default function grabDbBackups(): DbBackup[] {
|
||||
const backup_dir = grabDbBackupDir();
|
||||
const backup_name_regex = new RegExp(
|
||||
`^${BunSQLiteConfig.db_name}-\\d+$`,
|
||||
);
|
||||
|
||||
if (!fs.existsSync(backup_dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(backup_dir)
|
||||
.filter((name) => backup_name_regex.test(name))
|
||||
.map((name) => {
|
||||
const stat = fs.statSync(path.join(backup_dir, name));
|
||||
const timestamp = Number(name.split("-").pop());
|
||||
|
||||
return {
|
||||
name,
|
||||
is_file: stat.isFile(),
|
||||
created_at: Number.isFinite(timestamp)
|
||||
? timestamp
|
||||
: stat.mtimeMs,
|
||||
size: stat.size,
|
||||
};
|
||||
})
|
||||
.filter((backup) => backup.is_file)
|
||||
.map(({ is_file, ...backup }) => backup)
|
||||
.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
|
||||
type Params = {
|
||||
req: Request;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the session user only when they are a super admin.
|
||||
*/
|
||||
export default async function grabSuperAdminUser({ req }: Params) {
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* No includes/excludes: only the `super_admin` bypass passes.
|
||||
*/
|
||||
if (!checkUserAccess({ user_types }).success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import path from "path";
|
||||
import grabDbBackupDir from "./grab-db-backup-dir";
|
||||
import grabDbBackups from "./grab-db-backups";
|
||||
|
||||
type Params = {
|
||||
backup_name?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a backup file name from the request to its absolute path. Only
|
||||
* names of existing backups are accepted, so arbitrary paths can't be read.
|
||||
*/
|
||||
export default function resolveDbBackupPath({ backup_name }: Params) {
|
||||
const backup = grabDbBackups().find((b) => b.name === backup_name);
|
||||
|
||||
if (!backup) {
|
||||
throw new Error(`Backup not found`);
|
||||
}
|
||||
|
||||
return path.join(grabDbBackupDir(), backup.name);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import resolveDbBackupPath from "./resolve-db-backup-path";
|
||||
import runBunSQLiteCommand from "./run-bun-sqlite-command";
|
||||
|
||||
type Params = {
|
||||
backup_name?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Restores a database snapshot from the backups directory. `bun-sqlite
|
||||
* restore` is an interactive picker, so this uses its non-interactive
|
||||
* equivalent, `bun-sqlite import <file>`.
|
||||
*/
|
||||
export default async function restoreDbBackup({ backup_name }: Params) {
|
||||
const backup_path = resolveDbBackupPath({ backup_name });
|
||||
|
||||
return await runBunSQLiteCommand({ args: ["import", backup_path] });
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
type Params = {
|
||||
args: string[];
|
||||
};
|
||||
|
||||
let is_running = false;
|
||||
|
||||
/**
|
||||
* Runs a `bun-sqlite` CLI command (same as `bunx bun-sqlite ...`). Only one
|
||||
* command runs at a time so a backup and a restore never overlap.
|
||||
*/
|
||||
export default async function runBunSQLiteCommand({ args }: Params) {
|
||||
if (is_running) {
|
||||
throw new Error(`Another backup or restore is already running`);
|
||||
}
|
||||
|
||||
is_running = true;
|
||||
|
||||
try {
|
||||
const proc = Bun.spawn([process.execPath, "x", "bun-sqlite", ...args], {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exit_code] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
if (exit_code !== 0) {
|
||||
throw new Error(
|
||||
stderr.trim() ||
|
||||
stdout.trim() ||
|
||||
`bun-sqlite ${args[0]} failed (exit ${exit_code})`,
|
||||
);
|
||||
}
|
||||
|
||||
return { stdout };
|
||||
} finally {
|
||||
is_running = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import BunSQLiteConfig from "@/bun-sqlite.config";
|
||||
import grabDbBackupDir from "./grab-db-backup-dir";
|
||||
|
||||
type Params = {
|
||||
file: File;
|
||||
};
|
||||
|
||||
const SQLITE_HEADER = "SQLite format 3\0";
|
||||
|
||||
/**
|
||||
* Saves an uploaded SQLite database file into the backups directory under
|
||||
* the same `{db_name}-{timestamp}` name `bun-sqlite backup` uses, so it is
|
||||
* listed and pruned like any other backup. Returns the saved file name.
|
||||
*/
|
||||
export default async function saveUploadedDbBackup({ file }: Params) {
|
||||
if (!file.size) {
|
||||
throw new Error(`The uploaded backup file is empty`);
|
||||
}
|
||||
|
||||
const header = await file.slice(0, SQLITE_HEADER.length).text();
|
||||
|
||||
if (header !== SQLITE_HEADER) {
|
||||
throw new Error(`Only SQLite database files can be restored`);
|
||||
}
|
||||
|
||||
const backup_dir = grabDbBackupDir();
|
||||
|
||||
if (!fs.existsSync(backup_dir)) {
|
||||
fs.mkdirSync(backup_dir, { recursive: true });
|
||||
}
|
||||
|
||||
const backup_name = `${BunSQLiteConfig.db_name}-${Date.now()}`;
|
||||
|
||||
await Bun.write(path.join(backup_dir, backup_name), file);
|
||||
|
||||
return backup_name;
|
||||
}
|
||||
Reference in New Issue
Block a user