Tool to run software, backing up data at a remote site
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

215 lines
5.1 KiB

#!/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();
}