123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- #!/usr/bin/env -S deno run --allow-env --allow-run --allow-read --allow-write
- import * as path from "https://deno.land/std@0.79.0/path/mod.ts";
- export const System = {
- execute: Deno.run,
- getenv: Deno.env.get,
- getallenv: Deno.env.toObject,
- getargs: () => Deno.args,
- getcwd: Deno.cwd,
- };
- /**
- * Run the backedup command line tool
- *
- * This allows to execute another program, with data files
- * synchronization performed to a remote SSH server.
- *
- * Before launching the program, data files are retrieved,
- * and after the program ended, data files are uploaded.
- */
- export async function run(): Promise<void> {
- const args = System.getargs();
- const cwd = System.getcwd();
- const directory = args[0] ? makeAbsolute(args[0], cwd) : cwd;
- const remote = await readRemoteConfig();
- const program = await readProgramConfig(directory);
- const config = { remote, program };
- await runConfig(config);
- }
- const SELF_NAME = "thunderk-backedup";
- type RemoteConfig = {
- host: string;
- directory: string;
- passphrase: string;
- speed?: number;
- };
- type ProgramConfig = {
- id: string;
- command: string[];
- directory: string;
- env: { [name: string]: string };
- saves: { [name: string]: string };
- };
- type RunConfig = {
- remote: RemoteConfig;
- program: ProgramConfig;
- };
- /**
- * Get the user's home directory
- */
- function getHomeDirectory(): string {
- return System.getenv("HOME") ||
- `/home/${System.getenv("USER") || "backedup"}`;
- }
- /**
- * Read the remote configuration, from the user directory
- */
- async function readRemoteConfig(): Promise<RemoteConfig> {
- const config_path = path.join(
- getHomeDirectory(),
- ".config",
- `/${SELF_NAME}.json`,
- );
- console.debug(`Read remote config: ${config_path}`);
- const content = await Deno.readTextFile(config_path);
- const data = JSON.parse(content);
- if (data.host && data.directory && data.passphrase) {
- return {
- host: data.host,
- directory: data.directory,
- passphrase: data.passphrase,
- speed: data.speed,
- };
- } else {
- throw new Error("Bad remote configuration");
- }
- }
- /**
- * Read the program configuration from a directory
- */
- async function readProgramConfig(directory: string): Promise<ProgramConfig> {
- const config_path = path.join(directory, `.${SELF_NAME}.json`);
- console.debug(`Read program config: ${config_path}`);
- const content = await Deno.readTextFile(config_path);
- const data = JSON.parse(content);
- if (data.id && data.command) {
- return {
- id: data.id,
- command: typeof data.command == "string"
- ? data.command.split(" ")
- : data.command,
- directory: data.pwd || directory,
- env: data.env || {},
- saves: data.saves || {},
- };
- } else {
- throw new Error("Bad program configuration");
- }
- }
- /**
- * Run a configuration
- */
- async function runConfig(config: RunConfig): Promise<void> {
- console.debug(`Start config: ${JSON.stringify(config)}`);
- await runProgram(config.program);
- await backupToRemote(config);
- }
- /**
- * Run a program
- */
- async function runProgram(config: ProgramConfig): Promise<void> {
- const env = { ...System.getallenv(), ...config.env };
- await localRun(config.command, config.directory, env);
- }
- /**
- * Backup local data to a remote using borg
- */
- async function backupToRemote(config: RunConfig): Promise<void> {
- if (Object.keys(config.program.saves).length == 0) {
- return;
- }
- const destpath = path.join(config.remote.directory, config.program.id);
- const borgpath = `ssh://${config.remote.host}${destpath}`;
- try {
- await borgRun(["check", borgpath], config.remote);
- } catch {
- await borgRun(
- ["init", "--encryption=repokey", "--make-parent-dirs", borgpath],
- config.remote,
- );
- }
- await borgRun(
- [
- "create",
- `${borgpath}::{now}`,
- ].concat(
- Object.values(config.program.saves).map((dir) =>
- makeAbsolute(dir, config.program.directory)
- ),
- ),
- config.remote,
- );
- }
- /**
- * Make a path absolute
- */
- export function makeAbsolute(dir: string, cwd: string): string {
- if (path.isAbsolute(dir)) {
- return dir;
- } else if (dir.startsWith("~/")) {
- const home_directory = getHomeDirectory();
- return path.join(home_directory, dir.slice(2));
- } else {
- return path.join(cwd, dir);
- }
- }
- /**
- * Run a borg command
- */
- async function borgRun(
- cmd: string[],
- remote: RemoteConfig,
- allow_fail = false,
- ): Promise<void> {
- await localRun(
- ["borg"].concat(cmd),
- "/tmp",
- { BORG_PASSPHRASE: remote.passphrase },
- allow_fail,
- );
- }
- /**
- * Run a local program
- */
- async function localRun(
- cmd: string[],
- cwd: string,
- env: { [name: string]: string },
- allow_fail = false,
- input?: string,
- ): Promise<void> {
- console.debug(`Run: ${cmd.join(" ")}`);
- const process = System.execute(
- { cmd, cwd, env, stdin: input ? "piped" : undefined },
- );
- if (input && process.stdin) {
- await process.stdin.write(new TextEncoder().encode(input));
- process.stdin.close();
- }
- const result = await process.status();
- if (!result.success && !allow_fail) {
- throw new Error(`Command failure: ${cmd.join(" ")}`);
- }
- }
- if (import.meta.main) {
- await run();
- }
|