diff --git a/backups.ts b/backups.ts new file mode 100644 index 0000000..6fd4289 --- /dev/null +++ b/backups.ts @@ -0,0 +1,98 @@ +import { ok } from "./util.ts"; +import * as T from "./types.ts"; + +/** + * This utility simplifies the volume backup process. + * ```ts + * export const { createBackup, restoreBackup } = Backups.volumes("main").build(); + * ``` +*/ +export class Backups { + static BACKUP = "BACKUP" as const; + public backupSet = [] as { + srcPath: string; + srcVolume: string; + dstPath: string; + dstVolume: string; + }[]; + constructor() { + } + static volumes(...volumeNames: string[]) { + return new Backups().addSets(...volumeNames.map((srcVolume) => ({ + srcVolume, + srcPath: "./", + dstPath: `./${srcVolume}/`, + dstVolume: Backups.BACKUP, + }))); + } + static addSets( + ...options: { + srcPath: string; + srcVolume: string; + dstPath: string; + dstVolume: string; + }[] + ) { + return new Backups().addSets(...options); + } + addSets( + ...options: { + srcPath: string; + srcVolume: string; + dstPath: string; + dstVolume: string; + }[] + ) { + options.forEach((x) => this.backupSet.push(x)); + return this; + } + build() { + const createBackup: T.ExpectedExports.createBackup = async (effects) => { + for (const item of this.backupSet) { + if (notEmptyPath(item.dstPath)) { + await effects.createDir({ + volumeId: item.dstVolume, + path: item.dstPath, + }); + } + await effects.runRsync({ + ...item, + options: { + delete: true, + force: true, + ignoreExisting: false, + exclude: [], + }, + }).wait(); + } + return ok; + }; + const restoreBackup: T.ExpectedExports.restoreBackup = async (effects) => { + for (const item of this.backupSet) { + if (notEmptyPath(item.srcPath)) { + await effects.createDir({ + volumeId: item.srcVolume, + path: item.srcPath, + }); + } + await effects.runRsync({ + options: { + delete: true, + force: true, + ignoreExisting: false, + exclude: [], + }, + srcVolume: item.dstVolume, + dstVolume: item.srcVolume, + srcPath: item.dstPath, + dstPath: item.srcPath, + }).wait(); + } + return ok; + }; + return { createBackup, restoreBackup }; + } +} +function notEmptyPath(file: string) { + return ["", ".", "./"].indexOf(file) === -1; +} diff --git a/mod.ts b/mod.ts index 7cf9766..23cd38c 100644 --- a/mod.ts +++ b/mod.ts @@ -4,3 +4,4 @@ export * as compat from "./compat/mod.ts"; export * as migrations from "./migrations.ts"; export * as healthUtil from "./healthUtil.ts"; export * as util from "./util.ts"; +export { Backups } from "./backups.ts"; \ No newline at end of file diff --git a/types.ts b/types.ts index ff56726..7a4ccee 100644 --- a/types.ts +++ b/types.ts @@ -6,6 +6,10 @@ export namespace ExpectedExports { export type getConfig = (effects: Effects) => Promise>; /** These are how we make sure the our dependency configurations are valid and if not how to fix them. */ export type dependencies = Dependencies; + /** For backing up service data though the embassyOS UI */ + export type createBackup = (effects: Effects) => Promise>; + /** For restoring service data that was previously backed up using the embassyOS UI create backup flow. Backup restores are also triggered via the embassyOS UI, or doing a system restore flow during setup. */ + export type restoreBackup = (effects: Effects) => Promise>; /** Properties are used to get values from the docker, like a username + password, what ports we are hosting from */ export type properties = (effects: Effects) => Promise>; @@ -85,6 +89,20 @@ export type Effects = { /// Returns the body as a json json(): Promise; }>; + + runRsync(options: { + srcVolume: string, + dstVolume: string, + srcPath: string, + dstPath: string, + // rsync options: https://linux.die.net/man/1/rsync + options: { + delete: boolean, + force: boolean, + ignoreExisting: boolean, + exclude: string[] + } + }): {id: () => Promise, wait: () => Promise, progress: () => Promise} }; export type Metadata = { fileType: string;