mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 02:20:38 +09:00
Replace single retentionDays with a three-tier BackupRetentionPolicy: - Daily: keep all backups (presets: 3, 7, 14 days; default 7) - Weekly: keep one per calendar week (presets: 1, 2, 4 weeks; default 4) - Monthly: keep one per calendar month (presets: 1, 3, 6 months; default 1) Pruning sorts backups newest-first and applies each tier's cutoff, keeping only the newest entry per ISO week/month bucket. The Instance Settings General page now shows three preset selectors (no icon, matches existing page design). Remove Database icon import.
122 lines
3.8 KiB
TypeScript
122 lines
3.8 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { formatDatabaseBackupResult, runDatabaseBackup } from "./backup-lib.js";
|
|
|
|
type PartialConfig = {
|
|
database?: {
|
|
mode?: "embedded-postgres" | "postgres";
|
|
connectionString?: string;
|
|
embeddedPostgresPort?: number;
|
|
backup?: {
|
|
dir?: string;
|
|
retentionDays?: number;
|
|
};
|
|
};
|
|
};
|
|
|
|
function expandHomePrefix(value: string): string {
|
|
if (value === "~") return os.homedir();
|
|
if (value.startsWith("~/")) return path.resolve(os.homedir(), value.slice(2));
|
|
return value;
|
|
}
|
|
|
|
function resolvePaperclipHomeDir(): string {
|
|
const envHome = process.env.PAPERCLIP_HOME?.trim();
|
|
if (envHome) return path.resolve(expandHomePrefix(envHome));
|
|
return path.resolve(os.homedir(), ".paperclip");
|
|
}
|
|
|
|
function resolvePaperclipInstanceId(): string {
|
|
const raw = process.env.PAPERCLIP_INSTANCE_ID?.trim() || "default";
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(raw)) {
|
|
throw new Error(`Invalid PAPERCLIP_INSTANCE_ID '${raw}'.`);
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function resolveDefaultConfigPath(): string {
|
|
return path.resolve(resolvePaperclipHomeDir(), "instances", resolvePaperclipInstanceId(), "config.json");
|
|
}
|
|
|
|
function readConfig(configPath: string): PartialConfig | null {
|
|
if (!existsSync(configPath)) return null;
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
return typeof parsed === "object" && parsed ? (parsed as PartialConfig) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function asPositiveInt(value: unknown): number | null {
|
|
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
const rounded = Math.trunc(value);
|
|
return rounded > 0 ? rounded : null;
|
|
}
|
|
|
|
function resolveEmbeddedPort(config: PartialConfig | null): number {
|
|
return asPositiveInt(config?.database?.embeddedPostgresPort) ?? 54329;
|
|
}
|
|
|
|
function resolveConnectionString(config: PartialConfig | null): string {
|
|
const envUrl = process.env.DATABASE_URL?.trim();
|
|
if (envUrl) return envUrl;
|
|
|
|
if (config?.database?.mode === "postgres" && typeof config.database.connectionString === "string") {
|
|
const trimmed = config.database.connectionString.trim();
|
|
if (trimmed) return trimmed;
|
|
}
|
|
|
|
const port = resolveEmbeddedPort(config);
|
|
return `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
|
}
|
|
|
|
function resolveDefaultBackupDir(): string {
|
|
return path.resolve(resolvePaperclipHomeDir(), "instances", resolvePaperclipInstanceId(), "data", "backups");
|
|
}
|
|
|
|
function resolveBackupDir(config: PartialConfig | null): string {
|
|
const raw = config?.database?.backup?.dir;
|
|
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
return path.resolve(expandHomePrefix(raw.trim()));
|
|
}
|
|
return resolveDefaultBackupDir();
|
|
}
|
|
|
|
function resolveRetentionDays(config: PartialConfig | null): number {
|
|
return asPositiveInt(config?.database?.backup?.retentionDays) ?? 7;
|
|
}
|
|
|
|
async function main() {
|
|
const configPath = resolveDefaultConfigPath();
|
|
const config = readConfig(configPath);
|
|
const connectionString = resolveConnectionString(config);
|
|
const backupDir = resolveBackupDir(config);
|
|
const retentionDays = resolveRetentionDays(config);
|
|
|
|
console.log(`Config path: ${configPath}`);
|
|
console.log(`Backing up database to: ${backupDir}`);
|
|
console.log(`Retention window: ${retentionDays} day(s)`);
|
|
|
|
try {
|
|
const result = await runDatabaseBackup({
|
|
connectionString,
|
|
backupDir,
|
|
retention: { dailyDays: retentionDays, weeklyWeeks: 4, monthlyMonths: 1 },
|
|
filenamePrefix: "paperclip",
|
|
});
|
|
|
|
console.log(`Backup saved: ${formatDatabaseBackupResult(result)}`);
|
|
} catch (err) {
|
|
console.error("Backup failed.");
|
|
if (err instanceof Error) {
|
|
console.error(err.message);
|
|
} else {
|
|
console.error(String(err));
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
await main();
|