Files
start-os/sdk/lib/backup/Backups.ts
Lucy a535fc17c3 Feature/fe new registry (#2647)
* bugfixes

* update fe types

* implement new registry types in marketplace and ui

* fix marketplace types to have default params

* add alt implementation toggle

* merge cleanup

* more cleanup and notes

* fix build

* cleanup sync with next/minor

* add exver JS parser

* parse ValidExVer to string

* update types to interface

* add VersionRange and comparative functions

* Parse ExtendedVersion from string

* add conjunction, disjunction, and inversion logic

* consider flavor in satisfiedBy fn

* consider prerelease for ordering

* add compare fn for sorting

* rename fns for consistency

* refactoring

* update compare fn to return null if flavors don't match

* begin simplifying dependencies

* under construction

* wip

* add dependency metadata to CurrentDependencyInfo

* ditch inheritance for recursive VersionRange constructor. Recursive 'satisfiedBy' fn wip

* preprocess manifest

* misc fixes

* use sdk version as osVersion in manifest

* chore: Change the type to just validate and not generate all solutions.

* add publishedAt

* fix pegjs exports

* integrate exver into sdk

* misc fixes

* complete satisfiedBy fn

* refactor - use greaterThanOrEqual and lessThanOrEqual fns

* fix tests

* update dependency details

* update types

* remove interim types

* rename alt implementation to flavor

* cleanup os update

* format exver.ts

* add s9pk parsing endpoints

* fix build

* update to exver

* exver and bug fixes

* update static endpoints + cleanup

* cleanup

* update static proxy verification

* make mocks more robust; fix dep icon fallback; cleanup

* refactor alert versions and update fixtures

* registry bugfixes

* misc fixes

* cleanup unused

* convert patchdb ui seed to camelCase

* update otherVersions type

* change otherVersions: null to 'none'

* refactor and complete feature

* improve static endpoints

* fix install params

* mask systemd-networkd-wait-online

* fix static file fetching

* include non-matching versions in otherVersions

* convert release notes to modal and clean up displayExver

* alert for no other versions

* Fix ack-instructions casing

* fix indeterminate loader on service install

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Shadowy Super Coder <musashidisciple@proton.me>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
Co-authored-by: J H <dragondef@gmail.com>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
2024-07-23 00:48:12 +00:00

209 lines
5.7 KiB
TypeScript

import * as T from "../types"
import * as child_process from "child_process"
import { promises as fsPromises } from "fs"
export type BACKUP = "BACKUP"
export const DEFAULT_OPTIONS: T.BackupOptions = {
delete: true,
force: true,
ignoreExisting: false,
exclude: [],
}
export type BackupSet<Volumes extends string> = {
srcPath: string
srcVolume: Volumes | BACKUP
dstPath: string
dstVolume: Volumes | BACKUP
options?: Partial<T.BackupOptions>
}
/**
* This utility simplifies the volume backup process.
* ```ts
* export const { createBackup, restoreBackup } = Backups.volumes("main").build();
* ```
*
* Changing the options of the rsync, (ie exludes) use either
* ```ts
* Backups.volumes("main").set_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
* // or
* Backups.with_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
* ```
*
* Using the more fine control, using the addSets for more control
* ```ts
* Backups.addSets({
* srcVolume: 'main', srcPath:'smallData/', dstPath: 'main/smallData/', dstVolume: : Backups.BACKUP
* }, {
* srcVolume: 'main', srcPath:'bigData/', dstPath: 'main/bigData/', dstVolume: : Backups.BACKUP, options: {exclude:['bigData/excludeThis']}}
* ).build()q
* ```
*/
export class Backups<M extends T.Manifest> {
static BACKUP: BACKUP = "BACKUP"
private constructor(
private options = DEFAULT_OPTIONS,
private backupSet = [] as BackupSet<M["volumes"][number]>[],
) {}
static volumes<M extends T.Manifest = never>(
...volumeNames: Array<M["volumes"][0]>
): Backups<M> {
return new Backups<M>().addSets(
...volumeNames.map((srcVolume) => ({
srcVolume,
srcPath: "./",
dstPath: `./${srcVolume}/`,
dstVolume: Backups.BACKUP,
})),
)
}
static addSets<M extends T.Manifest = never>(
...options: BackupSet<M["volumes"][0]>[]
) {
return new Backups().addSets(...options)
}
static with_options<M extends T.Manifest = never>(
options?: Partial<T.BackupOptions>,
) {
return new Backups({ ...DEFAULT_OPTIONS, ...options })
}
static withOptions = Backups.with_options
setOptions(options?: Partial<T.BackupOptions>) {
this.options = {
...this.options,
...options,
}
return this
}
volumes(...volumeNames: Array<M["volumes"][0]>) {
return this.addSets(
...volumeNames.map((srcVolume) => ({
srcVolume,
srcPath: "./",
dstPath: `./${srcVolume}/`,
dstVolume: Backups.BACKUP,
})),
)
}
addSets(...options: BackupSet<M["volumes"][0]>[]) {
options.forEach((x) =>
this.backupSet.push({ ...x, options: { ...this.options, ...x.options } }),
)
return this
}
build(pathMaker: T.PathMaker) {
const createBackup: T.ExpectedExports.createBackup = async ({
effects,
}) => {
for (const item of this.backupSet) {
const rsyncResults = await runRsync(
{
dstPath: item.dstPath,
dstVolume: item.dstVolume,
options: { ...this.options, ...item.options },
srcPath: item.srcPath,
srcVolume: item.srcVolume,
},
pathMaker,
)
await rsyncResults.wait()
}
return
}
const restoreBackup: T.ExpectedExports.restoreBackup = async ({
effects,
}) => {
for (const item of this.backupSet) {
const rsyncResults = await runRsync(
{
dstPath: item.dstPath,
dstVolume: item.dstVolume,
options: { ...this.options, ...item.options },
srcPath: item.srcPath,
srcVolume: item.srcVolume,
},
pathMaker,
)
await rsyncResults.wait()
}
return
}
return { createBackup, restoreBackup }
}
}
function notEmptyPath(file: string) {
return ["", ".", "./"].indexOf(file) === -1
}
async function runRsync(
rsyncOptions: {
srcVolume: string
dstVolume: string
srcPath: string
dstPath: string
options: T.BackupOptions
},
pathMaker: T.PathMaker,
): Promise<{
id: () => Promise<string>
wait: () => Promise<null>
progress: () => Promise<number>
}> {
const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions
const command = "rsync"
const args: string[] = []
if (options.delete) {
args.push("--delete")
}
if (options.force) {
args.push("--force")
}
if (options.ignoreExisting) {
args.push("--ignore-existing")
}
for (const exclude of options.exclude) {
args.push(`--exclude=${exclude}`)
}
args.push("-actAXH")
args.push("--info=progress2")
args.push("--no-inc-recursive")
args.push(pathMaker({ volume: srcVolume, path: srcPath }))
args.push(pathMaker({ volume: dstVolume, path: dstPath }))
const spawned = child_process.spawn(command, args, { detached: true })
let percentage = 0.0
spawned.stdout.on("data", (data: unknown) => {
const lines = String(data).replace("\r", "\n").split("\n")
for (const line of lines) {
const parsed = /$([0-9.]+)%/.exec(line)?.[1]
if (!parsed) continue
percentage = Number.parseFloat(parsed)
}
})
spawned.stderr.on("data", (data: unknown) => {
console.error(String(data))
})
const id = async () => {
const pid = spawned.pid
if (pid === undefined) {
throw new Error("rsync process has no pid")
}
return String(pid)
}
const waitPromise = new Promise<null>((resolve, reject) => {
spawned.on("exit", (code: any) => {
if (code === 0) {
resolve(null)
} else {
reject(new Error(`rsync exited with code ${code}`))
}
})
})
const wait = () => waitPromise
const progress = () => Promise.resolve(percentage)
return { id, wait, progress }
}