aboutsummaryrefslogtreecommitdiffstats
path: root/cli/src/commands/backup.ts
blob: 4a7e4f091ae1a0dfe504205cd66e1c0d585f7772 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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)
}