mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
* 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>
209 lines
5.7 KiB
TypeScript
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 }
|
|
}
|