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.
201 lines
5.8 KiB
201 lines
5.8 KiB
import axios from 'axios'
|
|
import { spawnSync, SpawnSyncOptions } from 'child_process'
|
|
import { createHash, randomBytes } from 'crypto'
|
|
import { createWriteStream, renameSync, unlinkSync } from 'fs'
|
|
import { homedir, tmpdir } from 'os'
|
|
import { dirname, isAbsolute, join, resolve } from 'path'
|
|
import { Duration, Humanizer } from 'uhrwerk'
|
|
|
|
import { CONFIG_FILE, LocationFromPrefixes } from './config'
|
|
import { Location } from './types'
|
|
|
|
|
|
|
|
export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyncOptions = {}) => {
|
|
const { stdout, stderr, status } = spawnSync(command, args, {
|
|
...rest,
|
|
env: {
|
|
...process.env,
|
|
...env,
|
|
},
|
|
})
|
|
|
|
const out = stdout && stdout.toString().trim()
|
|
const err = stderr && stderr.toString().trim()
|
|
|
|
return { out, err, status }
|
|
}
|
|
|
|
export const execPlain = (command: string, opt: SpawnSyncOptions = {}) => {
|
|
const split = command.split(' ')
|
|
if (split.length < 1) throw new Error(`The command ${command} is not valid`.red)
|
|
|
|
return exec(split[0], split.slice(1), opt)
|
|
}
|
|
|
|
export const checkIfResticIsAvailable = () =>
|
|
checkIfCommandIsAvailable(
|
|
'restic',
|
|
'restic is not installed'.red +
|
|
'\nEither run ' + 'autorestic install'.green +
|
|
'\nOr go to https://restic.readthedocs.io/en/latest/020_installation.html#stable-releases',
|
|
)
|
|
|
|
export const checkIfCommandIsAvailable = (cmd: string, errorMsg?: string) => {
|
|
if (spawnSync(cmd).error)
|
|
throw new Error(errorMsg ? errorMsg : `"${cmd}" is not installed`.red)
|
|
}
|
|
|
|
export const makeObjectKeysLowercase = (object: Object): any =>
|
|
Object.fromEntries(
|
|
Object.entries(object).map(([key, value]) => [key.toLowerCase(), value]),
|
|
)
|
|
|
|
|
|
export function rand(length = 32): string {
|
|
return randomBytes(length / 2).toString('hex')
|
|
}
|
|
|
|
|
|
export const filterObject = <T>(
|
|
obj: { [key: string]: T },
|
|
filter: (item: [string, T]) => boolean,
|
|
): { [key: string]: T } =>
|
|
Object.fromEntries(Object.entries(obj).filter(filter))
|
|
|
|
export const filterObjectByKey = <T>(
|
|
obj: { [key: string]: T },
|
|
keys: string[],
|
|
) => filterObject(obj, ([key]) => keys.includes(key))
|
|
|
|
export const downloadFile = async (url: string, to: string) =>
|
|
new Promise<void>(async res => {
|
|
const { data: file } = await axios({
|
|
method: 'get',
|
|
url: url,
|
|
responseType: 'stream',
|
|
})
|
|
|
|
const tmp = join(tmpdir(), rand(64))
|
|
const stream = createWriteStream(tmp)
|
|
|
|
const writer = file.pipe(stream)
|
|
writer.on('close', () => {
|
|
stream.close()
|
|
try {
|
|
// Delete file if already exists. Needed if the binary wants to replace itself.
|
|
// Unix does not allow to overwrite a file that is being executed, but you can remove it and save other one at its place
|
|
unlinkSync(to)
|
|
} catch {
|
|
}
|
|
renameSync(tmp, to)
|
|
res()
|
|
})
|
|
})
|
|
|
|
// Check if is an absolute path, otherwise get the path relative to the config file
|
|
export const pathRelativeToConfigFile = (path: string): string => isAbsolute(path)
|
|
? path
|
|
: resolve(dirname(CONFIG_FILE), path)
|
|
|
|
export const resolveTildePath = (path: string): string | null =>
|
|
(path.length === 0 || path[0] !== '~')
|
|
? null
|
|
: join(homedir(), path.slice(1))
|
|
|
|
export const ConfigError = new Error('Config file not found')
|
|
|
|
export const getFlagsFromLocation = (location: Location, command?: string): string[] => {
|
|
if (!location.options) return []
|
|
|
|
const all = {
|
|
...location.options.global,
|
|
...(location.options[command || ''] || {}),
|
|
}
|
|
|
|
let flags: string[] = []
|
|
// Map the flags to an array for the exec function.
|
|
for (let [flag, values] of Object.entries(all))
|
|
for (const value of makeArrayIfIsNot(values)) {
|
|
const stringValue = String(value)
|
|
const resolvedTilde = resolveTildePath(stringValue)
|
|
flags = [...flags, `--${String(flag)}`, resolvedTilde === null ? stringValue : resolvedTilde]
|
|
}
|
|
|
|
return flags
|
|
}
|
|
|
|
export const makeArrayIfIsNot = <T>(maybeArray: T | T[]): T[] => Array.isArray(maybeArray) ? maybeArray : [maybeArray]
|
|
|
|
export const fill = (length: number, filler = ' '): string => new Array(length).fill(filler).join('')
|
|
|
|
export const capitalize = (string: string): string => string.charAt(0).toUpperCase() + string.slice(1)
|
|
|
|
export const treeToString = (obj: Object, highlight = [] as string[]): string => {
|
|
let cleaned = JSON.stringify(obj, null, 2)
|
|
.replace(/[{}"\[\],]/g, '')
|
|
.replace(/^ {2}/mg, '')
|
|
.replace(/\n\s*\n/g, '\n')
|
|
.trim()
|
|
|
|
for (const word of highlight)
|
|
cleaned = cleaned.replace(word, capitalize(word).green)
|
|
|
|
return cleaned
|
|
}
|
|
|
|
|
|
export class MeasureDuration {
|
|
private static Humanizer: Humanizer = [
|
|
[d => d.hours() > 0, d => `${d.hours()}h ${d.minutes()}min`],
|
|
[d => d.minutes() > 0, d => `${d.minutes()}min ${d.seconds()}s`],
|
|
[d => d.seconds() > 0, d => `${d.seconds()}s`],
|
|
[() => true, d => `${d.milliseconds()}ms`],
|
|
]
|
|
|
|
private start = Date.now()
|
|
|
|
|
|
finished(human?: false): number
|
|
finished(human?: true): string
|
|
finished(human?: boolean): number | string {
|
|
const delta = Date.now() - this.start
|
|
|
|
return human
|
|
? new Duration(delta, 'ms').humanize(MeasureDuration.Humanizer)
|
|
: delta
|
|
}
|
|
|
|
}
|
|
|
|
|
|
export const decodeLocationFromPrefix = (from: string): [LocationFromPrefixes, string] => {
|
|
const firstDelimiter = from.indexOf(':')
|
|
if (firstDelimiter === -1) return [LocationFromPrefixes.Filesystem, from]
|
|
|
|
const type = from.substr(0, firstDelimiter)
|
|
const value = from.substr(firstDelimiter + 1)
|
|
|
|
switch (type.toLowerCase()) {
|
|
case 'volume':
|
|
return [LocationFromPrefixes.DockerVolume, value]
|
|
case 'path':
|
|
return [LocationFromPrefixes.Filesystem, value]
|
|
default:
|
|
throw new Error(`Could not decode the location from: ${from}`.red)
|
|
}
|
|
}
|
|
|
|
export const hash = (plain: string): string => createHash('sha1').update(plain).digest().toString('hex')
|
|
|
|
export const getPathFromVolume = (volume: string) => pathRelativeToConfigFile(hash(volume))
|
|
|
|
export const checkIfDockerVolumeExistsOrFail = (volume: string) => {
|
|
const cmd = exec('docker', [
|
|
'volume', 'inspect', volume,
|
|
])
|
|
if (cmd.err.length > 0)
|
|
throw new Error('Volume not found')
|
|
}
|
|
|