diff --git a/src/autorestic.ts b/src/autorestic.ts index 096e490..10276a1 100644 --- a/src/autorestic.ts +++ b/src/autorestic.ts @@ -32,7 +32,7 @@ if (flags.version) { process.exit(0) } -export const config: Config = init() +export const config = init() function main() { if (commands.length < 1) return help() diff --git a/src/backend.ts b/src/backend.ts index 7a4da2d..832d822 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -2,56 +2,57 @@ import { Writer } from 'clitastic' import { config, VERBOSE } from './autorestic' import { Backend, Backends } from './types' -import { exec } from './utils' +import { exec, ConfigError } from './utils' const ALREADY_EXISTS = /(?=.*exists)(?=.*already)(?=.*config).*/ - export const getPathFromBackend = (backend: Backend): string => { - switch (backend.type) { - case 'local': - return backend.path - case 'b2': - case 'azure': - case 'gs': - case 's3': - return `${backend.type}:${backend.path}` - case 'sftp': - case 'rest': - throw new Error(`Unsupported backend type: "${backend.type}"`) - default: - throw new Error(`Unknown backend type.`) - } + switch (backend.type) { + case 'local': + return backend.path + case 'b2': + case 'azure': + case 'gs': + case 's3': + return `${backend.type}:${backend.path}` + case 'sftp': + case 'rest': + throw new Error(`Unsupported backend type: "${backend.type}"`) + default: + throw new Error(`Unknown backend type.`) + } } - export const getEnvFromBackend = (backend: Backend) => { - const { type, path, key, ...rest } = backend - return { - RESTIC_PASSWORD: key, - RESTIC_REPOSITORY: getPathFromBackend(backend), - ...rest, - } + const { type, path, key, ...rest } = backend + return { + RESTIC_PASSWORD: key, + RESTIC_REPOSITORY: getPathFromBackend(backend), + ...rest, + } } - export const checkAndConfigureBackend = (name: string, backend: Backend) => { - const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳') - const env = getEnvFromBackend(backend) + const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳') + const env = getEnvFromBackend(backend) - const { out, err } = exec('restic', ['init'], { env }) + const { out, err } = exec('restic', ['init'], { env }) - if (err.length > 0 && !ALREADY_EXISTS.test(err)) - throw new Error(`Could not load the backend "${name}": ${err}`) + if (err.length > 0 && !ALREADY_EXISTS.test(err)) + throw new Error(`Could not load the backend "${name}": ${err}`) - if (VERBOSE && out.length > 0) console.log(out) + if (VERBOSE && out.length > 0) console.log(out) - writer.done(name.blue + ' : ' + 'Done ✓'.green) + writer.done(name.blue + ' : ' + 'Done ✓'.green) } +export const checkAndConfigureBackends = (backends?: Backends) => { + if (!backends) { + if (!config) throw ConfigError + backends = config.backends + } -export const checkAndConfigureBackends = (backends: Backends = config.backends) => { - console.log('\nConfiguring Backends'.grey.underline) - for (const [name, backend] of Object.entries(backends)) - checkAndConfigureBackend(name, backend) -} \ No newline at end of file + console.log('\nConfiguring Backends'.grey.underline) + for (const [name, backend] of Object.entries(backends)) + checkAndConfigureBackend(name, backend) +} diff --git a/src/backup.ts b/src/backup.ts index 5f2a874..7d0c7a2 100644 --- a/src/backup.ts +++ b/src/backup.ts @@ -3,11 +3,12 @@ import { Writer } from 'clitastic' import { config, VERBOSE } from './autorestic' import { getEnvFromBackend } from './backend' import { Locations, Location } from './types' -import { exec } from './utils' +import { exec, ConfigError } from './utils' import { CONFIG_FILE } from './config' import { resolve, dirname } from 'path' export const backupSingle = (name: string, from: string, to: string) => { + if (!config) throw ConfigError const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳') const backend = config.backends[to] const pathRelativeToConfigFile = resolve(dirname(CONFIG_FILE), from) @@ -34,7 +35,12 @@ export const backupLocation = (name: string, backup: Location) => { } else backupSingle(display, backup.from, backup.to) } -export const backupAll = (backups: Locations = config.locations) => { +export const backupAll = (backups?: Locations) => { + if (!backups) { + if (!config) throw ConfigError + backups = config.locations + } + console.log('\nBacking Up'.underline.grey) for (const [name, backup] of Object.entries(backups)) backupLocation(name, backup) diff --git a/src/config.ts b/src/config.ts index 69f9a5b..83d4713 100644 --- a/src/config.ts +++ b/src/config.ts @@ -69,8 +69,13 @@ const findConfigFile = (): string => { export let CONFIG_FILE: string = '' -export const init = (): Config => { - CONFIG_FILE = findConfigFile() +export const init = (): Config | undefined => { + try { + CONFIG_FILE = findConfigFile() + } catch (e) { + return + } + const raw: Config = makeObjectKeysLowercase( yaml.safeLoad(readFileSync(CONFIG_FILE).toString()) ) diff --git a/src/handlers.ts b/src/handlers.ts index def0e44..3336aef 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -15,6 +15,7 @@ import { exec, filterObjectByKey, singleToArray, + ConfigError, } from './utils' export type Handlers = { @@ -22,6 +23,7 @@ export type Handlers = { } const parseBackend = (flags: Flags): Backends => { + if (!config) throw ConfigError if (!flags.all && !flags.backend) throw new Error( 'No backends specified.'.red + @@ -39,6 +41,7 @@ const parseBackend = (flags: Flags): Backends => { } const parseLocations = (flags: Flags): Locations => { + if (!config) throw ConfigError if (!flags.all && !flags.location) throw new Error( 'No locations specified.'.red + @@ -64,6 +67,7 @@ const handlers: Handlers = { checkAndConfigureBackends(backends) }, backup(args, flags) { + if (!config) throw ConfigError checkIfResticIsAvailable() const locations: Locations = parseLocations(flags) @@ -79,6 +83,7 @@ const handlers: Handlers = { console.log('\nFinished!'.underline + ' 🎉') }, restore(args, flags) { + if (!config) throw ConfigError checkIfResticIsAvailable() const locations = parseLocations(flags) for (const [name, location] of Object.entries(locations)) { diff --git a/src/types.ts b/src/types.ts index 607b995..cbcf86d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,70 +1,77 @@ type BackendLocal = { - type: 'local', - key: string, - path: string + type: 'local' + key: string + path: string } type BackendSFTP = { - type: 'sftp', - key: string, - path: string, - password?: string, + type: 'sftp' + key: string + path: string + password?: string } type BackendREST = { - type: 'rest', - key: string, - path: string, - user?: string, - password?: string + type: 'rest' + key: string + path: string + user?: string + password?: string } type BackendS3 = { - type: 's3', - key: string, - path: string, - aws_access_key_id: string, - aws_secret_access_key: string, + type: 's3' + key: string + path: string + aws_access_key_id: string + aws_secret_access_key: string } type BackendB2 = { - type: 'b2', - key: string, - path: string, - b2_account_id: string, - b2_account_key: string + type: 'b2' + key: string + path: string + b2_account_id: string + b2_account_key: string } type BackendAzure = { - type: 'azure', - key: string, - path: string, - azure_account_name: string, - azure_account_key: string + type: 'azure' + key: string + path: string + azure_account_name: string + azure_account_key: string } type BackendGS = { - type: 'gs', - key: string, - path: string, - google_project_id: string, - google_application_credentials: string + type: 'gs' + key: string + path: string + google_project_id: string + google_application_credentials: string } -export type Backend = BackendAzure | BackendB2 | BackendGS | BackendLocal | BackendREST | BackendS3 | BackendSFTP +export type Backend = + | BackendAzure + | BackendB2 + | BackendGS + | BackendLocal + | BackendREST + | BackendS3 + | BackendSFTP export type Backends = { [name: string]: Backend } export type Location = { - from: string, - to: string | string[] + from: string + to: string | string[] } export type Locations = { [name: string]: Location } export type Config = { - locations: Locations - backends: Backends + locations: Locations + backends: Backends } -export type Flags = { [arg: string]: any } \ No newline at end of file +export type Flags = { [arg: string]: any } diff --git a/src/utils.ts b/src/utils.ts index 0e8f4b1..77c3b9c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,61 +3,75 @@ import { spawnSync, SpawnSyncOptions } from 'child_process' import { randomBytes } from 'crypto' import { createWriteStream } from 'fs' -export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyncOptions = {}) => { +export const exec = ( + command: string, + args: string[], + { env, ...rest }: SpawnSyncOptions = {} +) => { + const cmd = spawnSync(command, args, { + ...rest, + env: { + ...process.env, + ...env, + }, + }) - const cmd = spawnSync(command, args, { - ...rest, - env: { - ...process.env, - ...env, - }, - }) + const out = cmd.stdout && cmd.stdout.toString().trim() + const err = cmd.stderr && cmd.stderr.toString().trim() - const out = cmd.stdout && cmd.stdout.toString().trim() - const err = cmd.stderr && cmd.stderr.toString().trim() - - return { out, err } + return { out, err } } -export const checkIfResticIsAvailable = () => checkIfCommandIsAvailable( - 'restic', - 'Restic is not installed'.red + ' https://restic.readthedocs.io/en/latest/020_installation.html#stable-releases', -) +export const checkIfResticIsAvailable = () => + checkIfCommandIsAvailable( + 'restic', + 'Restic is not installed'.red + + ' https://restic.readthedocs.io/en/latest/020_installation.html#stable-releases' + ) export const checkIfCommandIsAvailable = (cmd: string, errorMsg?: string) => { - if (require('child_process').spawnSync(cmd).error) - throw new Error(errorMsg ? errorMsg : `"${errorMsg}" is not installed`.red) + if (require('child_process').spawnSync(cmd).error) + throw new Error(errorMsg ? errorMsg : `"${errorMsg}" 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 singleToArray = (singleOrArray: T | T[]): T[] => Array.isArray(singleOrArray) ? singleOrArray : [singleOrArray] +export const singleToArray = (singleOrArray: T | T[]): T[] => + Array.isArray(singleOrArray) ? singleOrArray : [singleOrArray] -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', - }) +export const downloadFile = async (url: string, to: string) => + new Promise(async res => { + const { data: file } = await axios({ + method: 'get', + url: url, + responseType: 'stream', + }) - const stream = createWriteStream(to) + const stream = createWriteStream(to) - const writer = file.pipe(stream) - writer.on('close', () => { - stream.close() - res() - }) -}) \ No newline at end of file + const writer = file.pipe(stream) + writer.on('close', () => { + stream.close() + res() + }) + }) + +export const ConfigError = new Error('Config file not found')