103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import path from "node:path";
|
|
import * as p from "@clack/prompts";
|
|
import pc from "picocolors";
|
|
import { formatDatabaseBackupResult, runDatabaseBackup } from "@paperclipai/db";
|
|
import {
|
|
expandHomePrefix,
|
|
resolveDefaultBackupDir,
|
|
resolvePaperclipInstanceId,
|
|
} from "../config/home.js";
|
|
import { readConfig, resolveConfigPath } from "../config/store.js";
|
|
import { printPaperclipCliBanner } from "../utils/banner.js";
|
|
|
|
type DbBackupOptions = {
|
|
config?: string;
|
|
dir?: string;
|
|
retentionDays?: number;
|
|
filenamePrefix?: string;
|
|
json?: boolean;
|
|
};
|
|
|
|
function resolveConnectionString(configPath?: string): { value: string; source: string } {
|
|
const envUrl = process.env.DATABASE_URL?.trim();
|
|
if (envUrl) return { value: envUrl, source: "DATABASE_URL" };
|
|
|
|
const config = readConfig(configPath);
|
|
if (config?.database.mode === "postgres" && config.database.connectionString?.trim()) {
|
|
return { value: config.database.connectionString.trim(), source: "config.database.connectionString" };
|
|
}
|
|
|
|
const port = config?.database.embeddedPostgresPort ?? 54329;
|
|
return {
|
|
value: `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`,
|
|
source: `embedded-postgres@${port}`,
|
|
};
|
|
}
|
|
|
|
function normalizeRetentionDays(value: number | undefined, fallback: number): number {
|
|
const candidate = value ?? fallback;
|
|
if (!Number.isInteger(candidate) || candidate < 1) {
|
|
throw new Error(`Invalid retention days '${String(candidate)}'. Use a positive integer.`);
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
function resolveBackupDir(raw: string): string {
|
|
return path.resolve(expandHomePrefix(raw.trim()));
|
|
}
|
|
|
|
export async function dbBackupCommand(opts: DbBackupOptions): Promise<void> {
|
|
printPaperclipCliBanner();
|
|
p.intro(pc.bgCyan(pc.black(" paperclip db:backup ")));
|
|
|
|
const configPath = resolveConfigPath(opts.config);
|
|
const config = readConfig(opts.config);
|
|
const connection = resolveConnectionString(opts.config);
|
|
const defaultDir = resolveDefaultBackupDir(resolvePaperclipInstanceId());
|
|
const configuredDir = opts.dir?.trim() || config?.database.backup.dir || defaultDir;
|
|
const backupDir = resolveBackupDir(configuredDir);
|
|
const retentionDays = normalizeRetentionDays(
|
|
opts.retentionDays,
|
|
config?.database.backup.retentionDays ?? 30,
|
|
);
|
|
const filenamePrefix = opts.filenamePrefix?.trim() || "paperclip";
|
|
|
|
p.log.message(pc.dim(`Config: ${configPath}`));
|
|
p.log.message(pc.dim(`Connection source: ${connection.source}`));
|
|
p.log.message(pc.dim(`Backup dir: ${backupDir}`));
|
|
p.log.message(pc.dim(`Retention: ${retentionDays} day(s)`));
|
|
|
|
const spinner = p.spinner();
|
|
spinner.start("Creating database backup...");
|
|
try {
|
|
const result = await runDatabaseBackup({
|
|
connectionString: connection.value,
|
|
backupDir,
|
|
retentionDays,
|
|
filenamePrefix,
|
|
});
|
|
spinner.stop(`Backup saved: ${formatDatabaseBackupResult(result)}`);
|
|
|
|
if (opts.json) {
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
backupFile: result.backupFile,
|
|
sizeBytes: result.sizeBytes,
|
|
prunedCount: result.prunedCount,
|
|
backupDir,
|
|
retentionDays,
|
|
connectionSource: connection.source,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|
|
p.outro(pc.green("Backup completed."));
|
|
} catch (err) {
|
|
spinner.stop(pc.red("Backup failed."));
|
|
throw err;
|
|
}
|
|
}
|