diff --git a/backup.ts b/backup.ts index 493a854..64d6d6c 100755 --- a/backup.ts +++ b/backup.ts @@ -2,7 +2,12 @@ import * as hex from "https://deno.land/std@0.219.0/encoding/hex.ts"; import * as fs from "https://deno.land/std@0.219.0/fs/mod.ts"; -import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts"; +import { + ArgumentValue, + Command, + Type, + ValidationError, +} from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts"; function command(name: string, ...args: string[]): Promise { return new Deno.Command(name, { args }).output(); @@ -12,8 +17,34 @@ function print(encoded: Uint8Array): void { console.log(new TextDecoder().decode(encoded)); } +class KeepType extends Type { + private readonly units = ["h", "d", "w", "m", "y"]; + + public parse({ label, name, value }: ArgumentValue): string { + const [end, ...rest] = value.split("").reverse(); + const start = rest.reverse().join(""); + + if (!this.units.includes(end)) { + throw new ValidationError( + `${label} "${name}" has an invalid unit. Allowed values are: ${ + this.units.join(", ") + }`, + ); + } + + const num = Number.parseInt(start, 10); + if (Number.isNaN(num)) { + throw new ValidationError(`${label} "${name}" is invalid.`); + } + + const unit = end.replace("h", "H"); + return `${num}${unit}`; + } +} + const cli = new Command() .name("backup") + .type("keep", new KeepType()) .option("-v --verbose", "Enable verbose logging") .option( "--no-init", @@ -23,13 +54,17 @@ const cli = new Command() required: true, }) .option("-R --repo ", "Path to borg repo", { required: true }) - .option("--rclone-remote ", "Rclone remote name") - .option("--rclone-root ", "Rclone remote root directory", { - default: ".", + .option("--no-prune", "Disable automatic archive pruning") + .option("-K --keep ", "Timeframe to keep backups for (eg: 7d)", { + required: true, }) .option("-S --sync", "Sync repo using rclone", { depends: ["rclone-remote", "rclone-root"], }) + .option("--rclone-remote ", "Rclone remote name") + .option("--rclone-root ", "Rclone remote root directory", { + default: ".", + }) .arguments("<...PATHS>") .action(async (args, ...paths) => { const borg = (...args: string[]) => command("borg", ...args); @@ -46,11 +81,14 @@ const cli = new Command() Deno.exit(1); } + // TODO: Dry-run const { verbose, init, name, repo, + prune, + keep, sync, } = args; @@ -104,7 +142,25 @@ const cli = new Command() if (verbose) print(resp.stderr); - // TODO: Automatic prune + if (prune) { + const resp = await borg( + "prune", + repo, + "--stats", + "--keep-last=1", + `--keep-within=${keep}`, + ); + + if (!resp.success) { + console.log("failed to prune backups"); + if (verbose) print(resp.stderr); + Deno.exit(1); + } + + if (verbose) print(resp.stderr); + + // TODO: Detect borg 1.2.x and compact + } if (sync) { const { rcloneRoot, rcloneRemote } = args;