diff options
| author | ivar <i@oiee.no> | 2026-03-09 23:05:38 +0100 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2026-03-09 23:05:38 +0100 |
| commit | 69448e29a85cad3a94b3be3ad33efbc52764528f (patch) | |
| tree | c32b8c817322fdf26edbbb3fa75b9505a7020ae8 /cli/src/commands/backup.ts | |
| parent | b35302fa020ec82a9d67a6cb34379d42983d3cfc (diff) | |
| download | sparebank1-actualbudget-69448e29a85cad3a94b3be3ad33efbc52764528f.tar.xz sparebank1-actualbudget-69448e29a85cad3a94b3be3ad33efbc52764528f.zip | |
Diffstat (limited to 'cli/src/commands/backup.ts')
| -rw-r--r-- | cli/src/commands/backup.ts | 75 |
1 files changed, 75 insertions, 0 deletions
diff --git a/cli/src/commands/backup.ts b/cli/src/commands/backup.ts new file mode 100644 index 0000000..4a7e4f0 --- /dev/null +++ b/cli/src/commands/backup.ts @@ -0,0 +1,75 @@ +import { mkdirSync, readdirSync, writeFileSync } from "node:fs" +import { join } from "node:path" +import * as p from "@clack/prompts" +import * as actualApi from "@actual-app/api" +import { CONFIG_DIR, loadConfig } from "../config" +import { initActual } from "../actual" + +const BACKUP_DIR = join(CONFIG_DIR, "backups") + +export async function backup() { + const config = loadConfig() + + const spinner = p.spinner() + spinner.start("Connecting to Actual...") + await initActual(config.actual) + spinner.stop("Connected.") + + spinner.start("Exporting budget...") + const result = await (actualApi as any).internal.send("export-budget") + if (result.error) throw new Error(`Export failed: ${result.error}`) + + mkdirSync(BACKUP_DIR, { recursive: true }) + const filename = `backup-${timestamp()}.zip` + const filepath = join(BACKUP_DIR, filename) + writeFileSync(filepath, result.data) + + spinner.stop(`Backup saved to ${filepath}`) +} + +export async function restore(args: string[]) { + const config = loadConfig() + + const backups = listBackups() + if (!backups.length) { + p.log.warn(`No backups found in ${BACKUP_DIR}`) + return + } + + const filepath = await p.select({ + message: "Select backup to restore", + options: backups.map(f => ({ value: join(BACKUP_DIR, f), label: f })) + }) + if (p.isCancel(filepath)) { p.cancel("Cancelled."); process.exit(0) } + + const confirmed = await p.confirm({ + message: `Restore ${filepath}? This will overwrite your current budget.` + }) + if (p.isCancel(confirmed) || !confirmed) { p.cancel("Cancelled."); process.exit(0) } + + const spinner = p.spinner() + spinner.start("Connecting to Actual...") + await initActual(config.actual) + + spinner.start("Restoring backup...") + const result = await (actualApi as any).internal.send("import-budget", { filepath, type: "actual" }) + if (result?.error) throw new Error(`Restore failed: ${result.error}`) + + await actualApi.sync() + spinner.stop("Restored and synced.") +} + +function listBackups(): string[] { + try { + return readdirSync(BACKUP_DIR) + .filter(f => f.endsWith(".zip")) + .sort() + .reverse() + } catch { + return [] + } +} + +function timestamp(): string { + return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19) +} |
