cli.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #!/usr/bin/env -S deno run --allow-env --allow-run --allow-read --allow-write
  2. import * as path from "https://deno.land/std@0.79.0/path/mod.ts";
  3. export const System = {
  4. execute: Deno.run,
  5. getenv: Deno.env.get,
  6. getallenv: Deno.env.toObject,
  7. getargs: () => Deno.args,
  8. getcwd: Deno.cwd,
  9. };
  10. /**
  11. * Run the backedup command line tool
  12. *
  13. * This allows to execute another program, with data files
  14. * synchronization performed to a remote SSH server.
  15. *
  16. * Before launching the program, data files are retrieved,
  17. * and after the program ended, data files are uploaded.
  18. */
  19. export async function run(): Promise<void> {
  20. const args = System.getargs();
  21. const cwd = System.getcwd();
  22. const directory = args[0] ? makeAbsolute(args[0], cwd) : cwd;
  23. const remote = await readRemoteConfig();
  24. const program = await readProgramConfig(directory);
  25. const config = { remote, program };
  26. await runConfig(config);
  27. }
  28. const SELF_NAME = "thunderk-backedup";
  29. type RemoteConfig = {
  30. host: string;
  31. directory: string;
  32. passphrase: string;
  33. speed?: number;
  34. };
  35. type ProgramConfig = {
  36. id: string;
  37. command: string[];
  38. directory: string;
  39. env: { [name: string]: string };
  40. saves: { [name: string]: string };
  41. };
  42. type RunConfig = {
  43. remote: RemoteConfig;
  44. program: ProgramConfig;
  45. };
  46. /**
  47. * Get the user's home directory
  48. */
  49. function getHomeDirectory(): string {
  50. return System.getenv("HOME") ||
  51. `/home/${System.getenv("USER") || "backedup"}`;
  52. }
  53. /**
  54. * Read the remote configuration, from the user directory
  55. */
  56. async function readRemoteConfig(): Promise<RemoteConfig> {
  57. const config_path = path.join(
  58. getHomeDirectory(),
  59. ".config",
  60. `/${SELF_NAME}.json`,
  61. );
  62. console.debug(`Read remote config: ${config_path}`);
  63. const content = await Deno.readTextFile(config_path);
  64. const data = JSON.parse(content);
  65. if (data.host && data.directory && data.passphrase) {
  66. return {
  67. host: data.host,
  68. directory: data.directory,
  69. passphrase: data.passphrase,
  70. speed: data.speed,
  71. };
  72. } else {
  73. throw new Error("Bad remote configuration");
  74. }
  75. }
  76. /**
  77. * Read the program configuration from a directory
  78. */
  79. async function readProgramConfig(directory: string): Promise<ProgramConfig> {
  80. const config_path = path.join(directory, `.${SELF_NAME}.json`);
  81. console.debug(`Read program config: ${config_path}`);
  82. const content = await Deno.readTextFile(config_path);
  83. const data = JSON.parse(content);
  84. if (data.id && data.command) {
  85. return {
  86. id: data.id,
  87. command: typeof data.command == "string"
  88. ? data.command.split(" ")
  89. : data.command,
  90. directory: data.pwd || directory,
  91. env: data.env || {},
  92. saves: data.saves || {},
  93. };
  94. } else {
  95. throw new Error("Bad program configuration");
  96. }
  97. }
  98. /**
  99. * Run a configuration
  100. */
  101. async function runConfig(config: RunConfig): Promise<void> {
  102. console.debug(`Start config: ${JSON.stringify(config)}`);
  103. await runProgram(config.program);
  104. await backupToRemote(config);
  105. }
  106. /**
  107. * Run a program
  108. */
  109. async function runProgram(config: ProgramConfig): Promise<void> {
  110. const env = { ...System.getallenv(), ...config.env };
  111. await localRun(config.command, config.directory, env);
  112. }
  113. /**
  114. * Backup local data to a remote using borg
  115. */
  116. async function backupToRemote(config: RunConfig): Promise<void> {
  117. if (Object.keys(config.program.saves).length == 0) {
  118. return;
  119. }
  120. const destpath = path.join(config.remote.directory, config.program.id);
  121. const borgpath = `ssh://${config.remote.host}${destpath}`;
  122. try {
  123. await borgRun(["check", borgpath], config.remote);
  124. } catch {
  125. await borgRun(
  126. ["init", "--encryption=repokey", "--make-parent-dirs", borgpath],
  127. config.remote,
  128. );
  129. }
  130. await borgRun(
  131. [
  132. "create",
  133. `${borgpath}::{now}`,
  134. ].concat(
  135. Object.values(config.program.saves).map((dir) =>
  136. makeAbsolute(dir, config.program.directory)
  137. ),
  138. ),
  139. config.remote,
  140. );
  141. }
  142. /**
  143. * Make a path absolute
  144. */
  145. export function makeAbsolute(dir: string, cwd: string): string {
  146. if (path.isAbsolute(dir)) {
  147. return dir;
  148. } else if (dir.startsWith("~/")) {
  149. const home_directory = getHomeDirectory();
  150. return path.join(home_directory, dir.slice(2));
  151. } else {
  152. return path.join(cwd, dir);
  153. }
  154. }
  155. /**
  156. * Run a borg command
  157. */
  158. async function borgRun(
  159. cmd: string[],
  160. remote: RemoteConfig,
  161. allow_fail = false,
  162. ): Promise<void> {
  163. await localRun(
  164. ["borg"].concat(cmd),
  165. "/tmp",
  166. { BORG_PASSPHRASE: remote.passphrase },
  167. allow_fail,
  168. );
  169. }
  170. /**
  171. * Run a local program
  172. */
  173. async function localRun(
  174. cmd: string[],
  175. cwd: string,
  176. env: { [name: string]: string },
  177. allow_fail = false,
  178. input?: string,
  179. ): Promise<void> {
  180. console.debug(`Run: ${cmd.join(" ")}`);
  181. const process = System.execute(
  182. { cmd, cwd, env, stdin: input ? "piped" : undefined },
  183. );
  184. if (input && process.stdin) {
  185. await process.stdin.write(new TextEncoder().encode(input));
  186. process.stdin.close();
  187. }
  188. const result = await process.status();
  189. if (!result.success && !allow_fail) {
  190. throw new Error(`Command failure: ${cmd.join(" ")}`);
  191. }
  192. }
  193. if (import.meta.main) {
  194. await run();
  195. }