diff --git a/src/utils.ts b/src/utils.ts index 04385f4..7b33d3d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,179 +9,155 @@ 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 } + 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) + 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), { shell: true, ...opt }) + return exec(split[0], split.slice(1), { shell: true, ...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', - ) + 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, { shell: true }).error) - throw new Error(errorMsg ? errorMsg : `"${cmd}" is not installed`.red) + const error = spawnSync(cmd, { shell: true }).stderr + if (error.length) 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]), - ) - + Object.fromEntries(Object.entries(object).map(([key, value]) => [key.toLowerCase(), value])) export function rand(length = 32): string { - return randomBytes(length / 2).toString('hex') + return randomBytes(length / 2).toString('hex') } +export const filterObject = (obj: { [key: string]: T }, filter: (item: [string, T]) => boolean): { [key: string]: T } => + Object.fromEntries(Object.entries(obj).filter(filter)) -export const filterObject = ( - obj: { [key: string]: T }, - filter: (item: [string, T]) => boolean, -): { [key: string]: T } => - Object.fromEntries(Object.entries(obj).filter(filter)) - -export const filterObjectByKey = ( - obj: { [key: string]: T }, - keys: string[], -) => filterObject(obj, ([key]) => keys.includes(key)) +export const filterObjectByKey = (obj: { [key: string]: T }, keys: string[]) => filterObject(obj, ([key]) => keys.includes(key)) export const downloadFile = async (url: string, to: string) => - new Promise(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() - }) - }) + new Promise(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 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 resolveTildePath = (path: string): string | null => (path.length === 0 || path[0] !== '~' ? null : join(homedir(), path.slice(1))) 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 + 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 = (maybeArray: T | T[]): T[] => Array.isArray(maybeArray) ? maybeArray : [maybeArray] +export const makeArrayIfIsNot = (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() + let cleaned = JSON.stringify(obj, null, 2) + .replace(/[{}"\[\],]/g, '') + .replace(/^ {2}/gm, '') + .replace(/\n\s*\n/g, '\n') + .trim() - for (const word of highlight) - cleaned = cleaned.replace(word, capitalize(word).green) + for (const word of highlight) cleaned = cleaned.replace(word, capitalize(word).green) - return cleaned + 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 - } - + 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) - } + 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') @@ -189,9 +165,6 @@ export const hash = (plain: string): string => createHash('sha1').update(plain). 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') + const cmd = exec('docker', ['volume', 'inspect', volume]) + if (cmd.err.length > 0) throw new Error('Volume not found') }