parent
e9a7e03af7
commit
60d7e0b561
@ -1,63 +0,0 @@
|
||||
import 'colors'
|
||||
import minimist from 'minimist'
|
||||
|
||||
import { init } from './config'
|
||||
import handlers, { error, help } from './handlers'
|
||||
import { Config } from './types'
|
||||
import { readLock, writeLock, unlock } from './lock'
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.log(err.message)
|
||||
unlock()
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
export const { _: commands, ...flags } = minimist(process.argv.slice(2), {
|
||||
alias: {
|
||||
c: 'config',
|
||||
v: 'version',
|
||||
h: 'help',
|
||||
a: 'all',
|
||||
l: 'location',
|
||||
b: 'backend',
|
||||
d: 'dry-run',
|
||||
},
|
||||
boolean: ['a', 'd'],
|
||||
string: ['l', 'b'],
|
||||
})
|
||||
|
||||
export const VERSION = '0.20'
|
||||
export const INSTALL_DIR = '/usr/local/bin'
|
||||
export const VERBOSE = flags.verbose
|
||||
|
||||
export let config: Config
|
||||
|
||||
async function main() {
|
||||
config = init()
|
||||
|
||||
// Don't let 2 instances run on the same config
|
||||
const lock = readLock()
|
||||
if (lock.running) {
|
||||
console.log('An instance of autorestic is already running for this config file'.red)
|
||||
return
|
||||
}
|
||||
writeLock({
|
||||
...lock,
|
||||
running: true,
|
||||
})
|
||||
|
||||
// For dev
|
||||
// return await handlers['cron']([], { ...flags, all: true })
|
||||
|
||||
if (commands.length < 1 || commands[0] === 'help') return help()
|
||||
|
||||
const command: string = commands[0]
|
||||
const args: string[] = commands.slice(1)
|
||||
|
||||
const fn = handlers[command] || error
|
||||
await fn(args, flags)
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e: Error) => console.error(e.message))
|
||||
.finally(unlock)
|
@ -1,74 +1,66 @@
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { config, VERBOSE } from './autorestic'
|
||||
import { config, VERBOSE } from './'
|
||||
import { Backend, Backends, Locations } from './types'
|
||||
import { exec, pathRelativeToConfigFile, filterObjectByKey } from './utils'
|
||||
|
||||
|
||||
|
||||
const ALREADY_EXISTS = /(?=.*already)(?=.*config).*/
|
||||
|
||||
export const getPathFromBackend = (backend: Backend): string => {
|
||||
switch (backend.type) {
|
||||
case 'local':
|
||||
return pathRelativeToConfigFile(backend.path)
|
||||
case 'b2':
|
||||
case 'azure':
|
||||
case 'gs':
|
||||
case 's3':
|
||||
case 'sftp':
|
||||
case 'rest':
|
||||
return `${backend.type}:${backend.path}`
|
||||
default:
|
||||
throw new Error(`Unknown backend type.`)
|
||||
}
|
||||
switch (backend.type) {
|
||||
case 'local':
|
||||
return pathRelativeToConfigFile(backend.path)
|
||||
case 'b2':
|
||||
case 'azure':
|
||||
case 'gs':
|
||||
case 's3':
|
||||
case 'sftp':
|
||||
case 'rest':
|
||||
return `${backend.type}:${backend.path}`
|
||||
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 getBackendsFromLocations = (locations: Locations): string[] => {
|
||||
const backends = new Set<string>()
|
||||
for (const to of Object.values(locations).map(location => location.to))
|
||||
Array.isArray(to) ? to.forEach(t => backends.add(t)) : backends.add(to)
|
||||
return Array.from(backends)
|
||||
const backends = new Set<string>()
|
||||
for (const to of Object.values(locations).map((location) => location.to)) Array.isArray(to) ? to.forEach((t) => backends.add(t)) : backends.add(to)
|
||||
return Array.from(backends)
|
||||
}
|
||||
|
||||
export const checkAndConfigureBackend = (name: string, backend: Backend) => {
|
||||
const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳')
|
||||
try {
|
||||
const env = getEnvFromBackend(backend)
|
||||
const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳')
|
||||
try {
|
||||
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)
|
||||
} catch (e) {
|
||||
writer.done(name.blue + ' : ' + 'Error ⚠️ ' + e.message.red)
|
||||
}
|
||||
writer.done(name.blue + ' : ' + 'Done ✓'.green)
|
||||
} catch (e) {
|
||||
writer.done(name.blue + ' : ' + 'Error ⚠️ ' + e.message.red)
|
||||
}
|
||||
}
|
||||
|
||||
export const checkAndConfigureBackends = (backends?: Backends) => {
|
||||
if (!backends)
|
||||
backends = config.backends
|
||||
if (!backends) backends = config.backends
|
||||
|
||||
console.log('\nConfiguring Backends'.grey.underline)
|
||||
for (const [name, backend] of Object.entries(backends))
|
||||
checkAndConfigureBackend(name, backend)
|
||||
console.log('\nConfiguring Backends'.grey.underline)
|
||||
for (const [name, backend] of Object.entries(backends)) checkAndConfigureBackend(name, backend)
|
||||
}
|
||||
|
||||
export const checkAndConfigureBackendsForLocations = (locations: Locations) => {
|
||||
checkAndConfigureBackends(
|
||||
filterObjectByKey(config.backends, getBackendsFromLocations(locations)),
|
||||
)
|
||||
checkAndConfigureBackends(filterObjectByKey(config.backends, getBackendsFromLocations(locations)))
|
||||
}
|
||||
|
@ -1,111 +1,103 @@
|
||||
import { Writer } from 'clitastic'
|
||||
import { mkdirSync } from 'fs'
|
||||
|
||||
import { config, VERBOSE } from './autorestic'
|
||||
import { config, VERBOSE } from './'
|
||||
import { getEnvFromBackend } from './backend'
|
||||
import { LocationFromPrefixes } from './config'
|
||||
import { Locations, Location, Backend } from './types'
|
||||
import {
|
||||
exec,
|
||||
pathRelativeToConfigFile,
|
||||
getFlagsFromLocation,
|
||||
makeArrayIfIsNot,
|
||||
execPlain,
|
||||
MeasureDuration,
|
||||
fill,
|
||||
decodeLocationFromPrefix,
|
||||
checkIfDockerVolumeExistsOrFail,
|
||||
getPathFromVolume,
|
||||
exec,
|
||||
pathRelativeToConfigFile,
|
||||
getFlagsFromLocation,
|
||||
makeArrayIfIsNot,
|
||||
execPlain,
|
||||
MeasureDuration,
|
||||
fill,
|
||||
decodeLocationFromPrefix,
|
||||
checkIfDockerVolumeExistsOrFail,
|
||||
getPathFromVolume,
|
||||
} from './utils'
|
||||
|
||||
|
||||
|
||||
export const backupFromFilesystem = (from: string, location: Location, backend: Backend, tags?: string[]) => {
|
||||
const path = pathRelativeToConfigFile(from)
|
||||
const path = pathRelativeToConfigFile(from)
|
||||
|
||||
const { out, err, status } = exec(
|
||||
'restic',
|
||||
['backup', '.', ...getFlagsFromLocation(location, 'backup')],
|
||||
{ env: getEnvFromBackend(backend), cwd: path },
|
||||
)
|
||||
const { out, err, status } = exec('restic', ['backup', '.', ...getFlagsFromLocation(location, 'backup')], {
|
||||
env: getEnvFromBackend(backend),
|
||||
cwd: path,
|
||||
})
|
||||
|
||||
if (VERBOSE) console.log(out, err)
|
||||
if (status != 0 || err.length > 0)
|
||||
throw new Error(err)
|
||||
if (VERBOSE) console.log(out, err)
|
||||
if (status != 0 || err.length > 0) throw new Error(err)
|
||||
}
|
||||
|
||||
export const backupFromVolume = (volume: string, location: Location, backend: Backend) => {
|
||||
const tmp = getPathFromVolume(volume)
|
||||
try {
|
||||
mkdirSync(tmp)
|
||||
checkIfDockerVolumeExistsOrFail(volume)
|
||||
|
||||
// For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost.
|
||||
// execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /data /backup`)
|
||||
execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar cf /backup/archive.tar -C /data .`)
|
||||
|
||||
backupFromFilesystem(tmp, location, backend)
|
||||
} catch (e) {
|
||||
throw e
|
||||
} finally {
|
||||
execPlain(`rm -rf ${tmp}`)
|
||||
}
|
||||
const tmp = getPathFromVolume(volume)
|
||||
try {
|
||||
mkdirSync(tmp)
|
||||
checkIfDockerVolumeExistsOrFail(volume)
|
||||
|
||||
// For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost.
|
||||
// execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /data /backup`)
|
||||
execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar cf /backup/archive.tar -C /data .`)
|
||||
|
||||
backupFromFilesystem(tmp, location, backend)
|
||||
} catch (e) {
|
||||
throw e
|
||||
} finally {
|
||||
execPlain(`rm -rf ${tmp}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const backupSingle = (name: string, to: string, location: Location) => {
|
||||
const delta = new MeasureDuration()
|
||||
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
|
||||
|
||||
try {
|
||||
const backend = config.backends[to]
|
||||
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||
|
||||
switch (type) {
|
||||
|
||||
case LocationFromPrefixes.Filesystem:
|
||||
backupFromFilesystem(value, location, backend)
|
||||
break
|
||||
|
||||
case LocationFromPrefixes.DockerVolume:
|
||||
backupFromVolume(value, location, backend)
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
writer.done(`${name}${to.blue} : ${'Done ✓'.green} (${delta.finished(true)})`)
|
||||
} catch (e) {
|
||||
writer.done(`${name}${to.blue} : ${'Failed!'.red} (${delta.finished(true)}) ${e.message}`)
|
||||
}
|
||||
const delta = new MeasureDuration()
|
||||
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
|
||||
|
||||
try {
|
||||
const backend = config.backends[to]
|
||||
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||
|
||||
switch (type) {
|
||||
case LocationFromPrefixes.Filesystem:
|
||||
backupFromFilesystem(value, location, backend)
|
||||
break
|
||||
|
||||
case LocationFromPrefixes.DockerVolume:
|
||||
backupFromVolume(value, location, backend)
|
||||
break
|
||||
}
|
||||
|
||||
writer.done(`${name}${to.blue} : ${'Done ✓'.green} (${delta.finished(true)})`)
|
||||
} catch (e) {
|
||||
writer.done(`${name}${to.blue} : ${'Failed!'.red} (${delta.finished(true)}) ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const backupLocation = (name: string, location: Location) => {
|
||||
const display = name.yellow + ' ▶ '
|
||||
const filler = fill(name.length + 3)
|
||||
let first = true
|
||||
|
||||
if (location.hooks && location.hooks.before)
|
||||
for (const command of makeArrayIfIsNot(location.hooks.before)) {
|
||||
const cmd = execPlain(command, {})
|
||||
console.log(cmd.out, cmd.err)
|
||||
}
|
||||
|
||||
for (const t of makeArrayIfIsNot(location.to)) {
|
||||
backupSingle(first ? display : filler, t, location)
|
||||
if (first) first = false
|
||||
}
|
||||
|
||||
if (location.hooks && location.hooks.after)
|
||||
for (const command of makeArrayIfIsNot(location.hooks.after)) {
|
||||
const cmd = execPlain(command)
|
||||
console.log(cmd.out, cmd.err)
|
||||
}
|
||||
const display = name.yellow + ' ▶ '
|
||||
const filler = fill(name.length + 3)
|
||||
let first = true
|
||||
|
||||
if (location.hooks && location.hooks.before)
|
||||
for (const command of makeArrayIfIsNot(location.hooks.before)) {
|
||||
const cmd = execPlain(command, {})
|
||||
console.log(cmd.out, cmd.err)
|
||||
}
|
||||
|
||||
for (const t of makeArrayIfIsNot(location.to)) {
|
||||
backupSingle(first ? display : filler, t, location)
|
||||
if (first) first = false
|
||||
}
|
||||
|
||||
if (location.hooks && location.hooks.after)
|
||||
for (const command of makeArrayIfIsNot(location.hooks.after)) {
|
||||
const cmd = execPlain(command)
|
||||
console.log(cmd.out, cmd.err)
|
||||
}
|
||||
}
|
||||
|
||||
export const backupAll = (locations?: Locations) => {
|
||||
if (!locations)
|
||||
locations = config.locations
|
||||
if (!locations) locations = config.locations
|
||||
|
||||
console.log('\nBacking Up'.underline.grey)
|
||||
for (const [name, location] of Object.entries(locations))
|
||||
backupLocation(name, location)
|
||||
console.log('\nBacking Up'.underline.grey)
|
||||
for (const [name, location] of Object.entries(locations)) backupLocation(name, location)
|
||||
}
|
||||
|
@ -1,75 +1,61 @@
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { config, VERBOSE } from './autorestic'
|
||||
import { config, VERBOSE } from './'
|
||||
import { getEnvFromBackend } from './backend'
|
||||
import { LocationFromPrefixes } from './config'
|
||||
import { Locations, Location, Flags } from './types'
|
||||
import {
|
||||
exec,
|
||||
pathRelativeToConfigFile,
|
||||
getFlagsFromLocation,
|
||||
makeArrayIfIsNot,
|
||||
fill, decodeLocationFromPrefix, getPathFromVolume,
|
||||
} from './utils'
|
||||
|
||||
|
||||
import { exec, pathRelativeToConfigFile, getFlagsFromLocation, makeArrayIfIsNot, fill, decodeLocationFromPrefix, getPathFromVolume } from './utils'
|
||||
|
||||
export const forgetSingle = (name: string, to: string, location: Location, dryRun: boolean) => {
|
||||
const base = name + to.blue + ' : '
|
||||
const writer = new Writer(base + 'Removing old snapshots… ⏳')
|
||||
|
||||
const backend = config.backends[to]
|
||||
const flags = getFlagsFromLocation(location, 'forget')
|
||||
|
||||
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||
let path: string
|
||||
switch (type) {
|
||||
case LocationFromPrefixes.Filesystem:
|
||||
path = pathRelativeToConfigFile(value)
|
||||
break
|
||||
case LocationFromPrefixes.DockerVolume:
|
||||
path = getPathFromVolume(value)
|
||||
break
|
||||
}
|
||||
|
||||
if (flags.length == 0) {
|
||||
writer.done(base + 'Skipping, no policy declared')
|
||||
return
|
||||
}
|
||||
if (dryRun) flags.push('--dry-run')
|
||||
|
||||
writer.replaceLn(base + 'Forgetting old snapshots… ⏳')
|
||||
const cmd = exec(
|
||||
'restic',
|
||||
['forget', '--path', path, '--prune', ...flags],
|
||||
{ env: getEnvFromBackend(backend) },
|
||||
)
|
||||
|
||||
if (VERBOSE) console.log(cmd.out, cmd.err)
|
||||
writer.done(base + 'Done ✓'.green)
|
||||
const base = name + to.blue + ' : '
|
||||
const writer = new Writer(base + 'Removing old snapshots… ⏳')
|
||||
|
||||
const backend = config.backends[to]
|
||||
const flags = getFlagsFromLocation(location, 'forget')
|
||||
|
||||
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||
let path: string
|
||||
switch (type) {
|
||||
case LocationFromPrefixes.Filesystem:
|
||||
path = pathRelativeToConfigFile(value)
|
||||
break
|
||||
case LocationFromPrefixes.DockerVolume:
|
||||
path = getPathFromVolume(value)
|
||||
break
|
||||
}
|
||||
|
||||
if (flags.length == 0) {
|
||||
writer.done(base + 'Skipping, no policy declared')
|
||||
return
|
||||
}
|
||||
if (dryRun) flags.push('--dry-run')
|
||||
|
||||
writer.replaceLn(base + 'Forgetting old snapshots… ⏳')
|
||||
const cmd = exec('restic', ['forget', '--path', path, '--prune', ...flags], { env: getEnvFromBackend(backend) })
|
||||
|
||||
if (VERBOSE) console.log(cmd.out, cmd.err)
|
||||
writer.done(base + 'Done ✓'.green)
|
||||
}
|
||||
|
||||
export const forgetLocation = (name: string, backup: Location, dryRun: boolean) => {
|
||||
const display = name.yellow + ' ▶ '
|
||||
const filler = fill(name.length + 3)
|
||||
let first = true
|
||||
|
||||
for (const t of makeArrayIfIsNot(backup.to)) {
|
||||
const nameOrBlankSpaces: string = first ? display : filler
|
||||
forgetSingle(nameOrBlankSpaces, t, backup, dryRun)
|
||||
if (first) first = false
|
||||
}
|
||||
const display = name.yellow + ' ▶ '
|
||||
const filler = fill(name.length + 3)
|
||||
let first = true
|
||||
|
||||
for (const t of makeArrayIfIsNot(backup.to)) {
|
||||
const nameOrBlankSpaces: string = first ? display : filler
|
||||
forgetSingle(nameOrBlankSpaces, t, backup, dryRun)
|
||||
if (first) first = false
|
||||
}
|
||||
}
|
||||
|
||||
export const forgetAll = (backups?: Locations, flags?: Flags) => {
|
||||
if (!backups) {
|
||||
backups = config.locations
|
||||
}
|
||||
export const forgetAll = (backups?: Locations, dryRun = false) => {
|
||||
if (!backups) {
|
||||
backups = config.locations
|
||||
}
|
||||
|
||||
console.log('\nRemoving old snapshots according to policy'.underline.grey)
|
||||
const dryRun = flags ? flags['dry-run'] : false
|
||||
if (dryRun) console.log('Running in dry-run mode, not touching data\n'.yellow)
|
||||
console.log('\nRemoving old snapshots according to policy'.underline.grey)
|
||||
if (dryRun) console.log('Running in dry-run mode, not touching data\n'.yellow)
|
||||
|
||||
for (const [name, backup] of Object.entries(backups))
|
||||
forgetLocation(name, backup, dryRun)
|
||||
for (const [name, backup] of Object.entries(backups)) forgetLocation(name, backup, dryRun)
|
||||
}
|
||||
|
@ -1,244 +0,0 @@
|
||||
import { chmodSync, renameSync, unlinkSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join, resolve } from 'path'
|
||||
|
||||
import axios from 'axios'
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { config, INSTALL_DIR, VERSION } from './autorestic'
|
||||
import { checkAndConfigureBackends, getEnvFromBackend, checkAndConfigureBackendsForLocations } from './backend'
|
||||
import { backupAll } from './backup'
|
||||
import { runCron } from './cron'
|
||||
import { forgetAll } from './forget'
|
||||
import showAll from './info'
|
||||
import { restoreSingle } from './restore'
|
||||
import { Backends, Flags, Locations } from './types'
|
||||
import {
|
||||
checkIfCommandIsAvailable,
|
||||
checkIfResticIsAvailable,
|
||||
downloadFile,
|
||||
exec,
|
||||
filterObjectByKey,
|
||||
makeArrayIfIsNot,
|
||||
} from './utils'
|
||||
|
||||
|
||||
|
||||
export type Handlers = {
|
||||
[command: string]: (args: string[], flags: Flags) => void
|
||||
}
|
||||
|
||||
const parseBackend = (flags: Flags): Backends => {
|
||||
if (!flags.all && !flags.backend)
|
||||
throw new Error(
|
||||
'No backends specified.'.red +
|
||||
'\n--all [-a]\t\t\t\tCheck all.' +
|
||||
'\n--backend [-b] myBackend\t\tSpecify one or more backend',
|
||||
)
|
||||
if (flags.all) return config.backends
|
||||
else {
|
||||
const backends = makeArrayIfIsNot<string>(flags.backend)
|
||||
for (const backend of backends)
|
||||
if (!config.backends[backend])
|
||||
throw new Error('Invalid backend: '.red + backend)
|
||||
return filterObjectByKey(config.backends, backends)
|
||||
}
|
||||
}
|
||||
|
||||
const parseLocations = (flags: Flags): Locations => {
|
||||
if (!flags.all && !flags.location)
|
||||
throw new Error(
|
||||
'No locations specified.'.red +
|
||||
'\n--all [-a]\t\t\t\tBackup all.' +
|
||||
'\n--location [-l] site1\t\t\tSpecify one or more locations',
|
||||
)
|
||||
|
||||
if (flags.all) {
|
||||
return config.locations
|
||||
} else {
|
||||
const locations = makeArrayIfIsNot<string>(flags.location)
|
||||
for (const location of locations)
|
||||
if (!config.locations[location])
|
||||
throw new Error('Invalid location: '.red + location)
|
||||
return filterObjectByKey(config.locations, locations)
|
||||
}
|
||||
}
|
||||
|
||||
const handlers: Handlers = {
|
||||
check(args, flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const backends = parseBackend(flags)
|
||||
checkAndConfigureBackends(backends)
|
||||
},
|
||||
backup(args, flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const locations: Locations = parseLocations(flags)
|
||||
checkAndConfigureBackendsForLocations(locations)
|
||||
backupAll(locations)
|
||||
|
||||
console.log('\nFinished!'.underline + ' 🎉')
|
||||
},
|
||||
cron(args, flags) {
|
||||
checkIfResticIsAvailable()
|
||||
runCron()
|
||||
},
|
||||
restore(args, flags) {
|
||||
checkIfResticIsAvailable()
|
||||
|
||||
const locations = parseLocations(flags)
|
||||
const keys = Object.keys(locations)
|
||||
if (keys.length < 1) throw new Error(`You need to specify the location to restore with --location`.red)
|
||||
if (keys.length > 2) throw new Error(`Only one location is supported at a time when restoring`.red)
|
||||
|
||||
restoreSingle(keys[0], flags.from, flags.to)
|
||||
},
|
||||
forget(args, flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const locations: Locations = parseLocations(flags)
|
||||
checkAndConfigureBackendsForLocations(locations)
|
||||
forgetAll(locations, flags)
|
||||
|
||||
console.log('\nFinished!'.underline + ' 🎉')
|
||||
},
|
||||
exec(args, flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const backends = parseBackend(flags)
|
||||
for (const [name, backend] of Object.entries(backends)) {
|
||||
console.log(`\n${name}:\n`.grey.underline)
|
||||
const env = getEnvFromBackend(backend)
|
||||
|
||||
const { out, err } = exec('restic', args, { env })
|
||||
console.log(out, err)
|
||||
}
|
||||
},
|
||||
info() {
|
||||
showAll()
|
||||
},
|
||||
async install() {
|
||||
try {
|
||||
checkIfResticIsAvailable()
|
||||
console.log('Restic is already installed')
|
||||
return
|
||||
} catch {
|
||||
}
|
||||
|
||||
const w = new Writer('Checking latest version... ⏳')
|
||||
checkIfCommandIsAvailable('bzip2')
|
||||
const { data: json } = await axios({
|
||||
method: 'get',
|
||||
url: 'https://api.github.com/repos/restic/restic/releases/latest',
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
const archMap: { [a: string]: string } = {
|
||||
x32: '386',
|
||||
x64: 'amd64',
|
||||
}
|
||||
|
||||
w.replaceLn('Downloading binary... 🌎')
|
||||
const name = `${json.name.replace(' ', '_')}_${process.platform}_${archMap[process.arch]}.bz2`
|
||||
const dl = json.assets.find((asset: any) => asset.name === name)
|
||||
if (!dl)
|
||||
return console.log(
|
||||
'Cannot get the right binary.'.red,
|
||||
'Please see https://bit.ly/2Y1Rzai',
|
||||
)
|
||||
|
||||
const tmp = join(tmpdir(), name)
|
||||
const extracted = tmp.slice(0, -4) //without the .bz2
|
||||
|
||||
await downloadFile(dl.browser_download_url, tmp)
|
||||
|
||||
w.replaceLn('Decompressing binary... 📦')
|
||||
exec('bzip2', ['-dk', tmp])
|
||||
unlinkSync(tmp)
|
||||
|
||||
w.replaceLn(`Moving to ${INSTALL_DIR} 🚙`)
|
||||
chmodSync(extracted, 0o755)
|
||||
renameSync(extracted, INSTALL_DIR + '/restic')
|
||||
|
||||
w.done(
|
||||
`\nFinished! restic is installed under: ${INSTALL_DIR}`.underline + ' 🎉',
|
||||
)
|
||||
},
|
||||
uninstall() {
|
||||
for (const bin of ['restic', 'autorestic'])
|
||||
try {
|
||||
unlinkSync(INSTALL_DIR + '/' + bin)
|
||||
console.log(`Finished! ${bin} was uninstalled`)
|
||||
} catch (e) {
|
||||
console.log(`${bin} is already uninstalled`.red)
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
checkIfResticIsAvailable()
|
||||
const w = new Writer('Checking for latest restic version... ⏳')
|
||||
exec('restic', ['self-update'])
|
||||
|
||||
w.replaceLn('Checking for latest autorestic version... ⏳')
|
||||
const { data: json } = await axios({
|
||||
method: 'get',
|
||||
url:
|
||||
'https://api.github.com/repos/cupcakearmy/autorestic/releases/latest',
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
if (json.tag_name != VERSION) {
|
||||
const platformMap: { [key: string]: string } = {
|
||||
darwin: 'macos',
|
||||
}
|
||||
|
||||
const name = `autorestic_${platformMap[process.platform] || process.platform}_${process.arch}`
|
||||
const dl = json.assets.find((asset: any) => asset.name === name)
|
||||
|
||||
const to = INSTALL_DIR + '/autorestic'
|
||||
w.replaceLn('Downloading binary... 🌎')
|
||||
await downloadFile(dl.browser_download_url, to)
|
||||
|
||||
chmodSync(to, 0o755)
|
||||
}
|
||||
|
||||
w.done('All up to date! 🚀')
|
||||
},
|
||||
version() {
|
||||
console.log('version'.grey, VERSION)
|
||||
},
|
||||
}
|
||||
|
||||
export const help = () => {
|
||||
console.log(
|
||||
'\nAutorestic'.blue +
|
||||
` - ${VERSION} - Easy Restic CLI Utility` +
|
||||
'\n' +
|
||||
'\nOptions:'.yellow +
|
||||
`\n -c, --config Specify config file. Default: .autorestic.yml` +
|
||||
'\n' +
|
||||
'\nCommands:'.yellow +
|
||||
'\n info Show all locations and backends' +
|
||||
'\n check [-b, --backend] [-a, --all] Check backends' +
|
||||
'\n backup [-l, --location] [-a, --all] Backup all or specified locations' +
|
||||
'\n forget [-l, --location] [-a, --all] [--dry-run] Forget old snapshots according to declared policies' +
|
||||
'\n restore [-l, --location] [--from backend] [--to <out dir>] Restore all or specified locations' +
|
||||
'\n' +
|
||||
'\n exec [-b, --backend] [-a, --all] <command> -- [native options] Execute native restic command' +
|
||||
'\n' +
|
||||
'\n install install restic' +
|
||||
'\n uninstall uninstall restic' +
|
||||
'\n update update restic' +
|
||||
'\n help Show help' +
|
||||
'\n' +
|
||||
'\nExamples: '.yellow +
|
||||
'https://git.io/Jf0x6' +
|
||||
'\n',
|
||||
)
|
||||
}
|
||||
|
||||
export const error = () => {
|
||||
help()
|
||||
console.log(
|
||||
`Invalid Command:`.red.underline,
|
||||
`${process.argv.slice(2).join(' ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
export default handlers
|
@ -0,0 +1,13 @@
|
||||
import { checkAndConfigureBackendsForLocations } from '../backend'
|
||||
import { backupAll } from '../backup'
|
||||
import { Flags, Locations } from '../types'
|
||||
import { checkIfResticIsAvailable, parseLocations } from '../utils'
|
||||
|
||||
export default function backup({ location, all }: Flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const locations: Locations = parseLocations(location, all)
|
||||
checkAndConfigureBackendsForLocations(locations)
|
||||
backupAll(locations)
|
||||
|
||||
console.log('\nFinished!'.underline + ' 🎉')
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { checkAndConfigureBackends } from '../backend'
|
||||
import { Flags } from '../types'
|
||||
import { checkIfResticIsAvailable, parseBackend } from '../utils'
|
||||
|
||||
export default function check({ backend, all }: Flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const backends = parseBackend(backend, all)
|
||||
checkAndConfigureBackends(backends)
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { runCron } from '../cron'
|
||||
import { checkIfResticIsAvailable } from '../utils'
|
||||
|
||||
export function cron() {
|
||||
checkIfResticIsAvailable()
|
||||
runCron()
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { getEnvFromBackend } from '../backend'
|
||||
import { Flags } from '../types'
|
||||
import { checkIfResticIsAvailable, exec as execCLI, parseBackend } from '../utils'
|
||||
|
||||
export default function exec({ backend, all }: Flags, args: string[]) {
|
||||
checkIfResticIsAvailable()
|
||||
const backends = parseBackend(backend, all)
|
||||
for (const [name, backend] of Object.entries(backends)) {
|
||||
console.log(`\n${name}:\n`.grey.underline)
|
||||
const env = getEnvFromBackend(backend)
|
||||
const { out, err } = execCLI('restic', args, { env })
|
||||
console.log(out, err)
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { checkAndConfigureBackendsForLocations } from '../backend'
|
||||
import { forgetAll } from '../forget'
|
||||
import { Flags, Locations } from '../types'
|
||||
import { checkIfResticIsAvailable, parseLocations } from '../utils'
|
||||
|
||||
export default function forget({ location, all, dryRun }: Flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const locations: Locations = parseLocations(location, all)
|
||||
checkAndConfigureBackendsForLocations(locations)
|
||||
forgetAll(locations, dryRun)
|
||||
|
||||
console.log('\nFinished!'.underline + ' 🎉')
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { config } from '../'
|
||||
import { fill, treeToString } from '../utils'
|
||||
|
||||
const showAll = () => {
|
||||
console.log('\n\n' + fill(32, '_') + 'LOCATIONS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.locations)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(data, ['to:', 'from:', 'hooks:', 'options:', 'cron:']))
|
||||
}
|
||||
|
||||
console.log('\n\n' + fill(32, '_') + 'BACKENDS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.backends)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(data, ['type:', 'path:', 'key:']))
|
||||
}
|
||||
}
|
||||
|
||||
export default showAll
|
@ -0,0 +1,50 @@
|
||||
import { join } from 'path'
|
||||
import { chmodSync, renameSync, unlinkSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
import axios from 'axios'
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { INSTALL_DIR } from '..'
|
||||
import { checkIfCommandIsAvailable, checkIfResticIsAvailable, downloadFile, exec } from '../utils'
|
||||
|
||||
export default async function install() {
|
||||
try {
|
||||
checkIfResticIsAvailable()
|
||||
console.log('Restic is already installed')
|
||||
return
|
||||
} catch {}
|
||||
|
||||
const w = new Writer('Checking latest version... ⏳')
|
||||
checkIfCommandIsAvailable('bzip2')
|
||||
const { data: json } = await axios({
|
||||
method: 'get',
|
||||
url: 'https://api.github.com/repos/restic/restic/releases/latest',
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
const archMap: { [a: string]: string } = {
|
||||
x32: '386',
|
||||
x64: 'amd64',
|
||||
}
|
||||
|
||||
w.replaceLn('Downloading binary... 🌎')
|
||||
const name = `${json.name.replace(' ', '_')}_${process.platform}_${archMap[process.arch]}.bz2`
|
||||
const dl = json.assets.find((asset: any) => asset.name === name)
|
||||
if (!dl) return console.log('Cannot get the right binary.'.red, 'Please see https://bit.ly/2Y1Rzai')
|
||||
|
||||
const tmp = join(tmpdir(), name)
|
||||
const extracted = tmp.slice(0, -4) //without the .bz2
|
||||
|
||||
await downloadFile(dl.browser_download_url, tmp)
|
||||
|
||||
w.replaceLn('Decompressing binary... 📦')
|
||||
exec('bzip2', ['-dk', tmp])
|
||||
unlinkSync(tmp)
|
||||
|
||||
w.replaceLn(`Moving to ${INSTALL_DIR} 🚙`)
|
||||
chmodSync(extracted, 0o755)
|
||||
renameSync(extracted, INSTALL_DIR + '/restic')
|
||||
|
||||
w.done(`\nFinished! restic is installed under: ${INSTALL_DIR}`.underline + ' 🎉')
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { restoreSingle } from '../restore'
|
||||
import { Flags } from '../types'
|
||||
import { checkIfResticIsAvailable, checkIfValidLocation } from '../utils'
|
||||
|
||||
export default function restore({ location, to, from }: Flags) {
|
||||
checkIfResticIsAvailable()
|
||||
checkIfValidLocation(location)
|
||||
restoreSingle(location, from, to)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { unlinkSync } from 'fs'
|
||||
|
||||
import { INSTALL_DIR } from '..'
|
||||
|
||||
export function uninstall() {
|
||||
for (const bin of ['restic', 'autorestic'])
|
||||
try {
|
||||
unlinkSync(INSTALL_DIR + '/' + bin)
|
||||
console.log(`Finished! ${bin} was uninstalled`)
|
||||
} catch (e) {
|
||||
console.log(`${bin} is already uninstalled`.red)
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { chmodSync } from 'fs'
|
||||
|
||||
import axios from 'axios'
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { INSTALL_DIR, VERSION } from '..'
|
||||
import { checkIfResticIsAvailable, downloadFile, exec } from '../utils'
|
||||
|
||||
export async function upgrade() {
|
||||
checkIfResticIsAvailable()
|
||||
const w = new Writer('Checking for latest restic version... ⏳')
|
||||
exec('restic', ['self-update'])
|
||||
|
||||
w.replaceLn('Checking for latest autorestic version... ⏳')
|
||||
const { data: json } = await axios({
|
||||
method: 'get',
|
||||
url: 'https://api.github.com/repos/cupcakearmy/autorestic/releases/latest',
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
if (json.tag_name != VERSION) {
|
||||
const platformMap: { [key: string]: string } = {
|
||||
darwin: 'macos',
|
||||
}
|
||||
|
||||
const name = `autorestic_${platformMap[process.platform] || process.platform}_${process.arch}`
|
||||
const dl = json.assets.find((asset: any) => asset.name === name)
|
||||
|
||||
const to = INSTALL_DIR + '/autorestic'
|
||||
w.replaceLn('Downloading binary... 🌎')
|
||||
await downloadFile(dl.browser_download_url, to)
|
||||
|
||||
chmodSync(to, 0o755)
|
||||
}
|
||||
|
||||
w.done('All up to date! 🚀')
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
import 'colors'
|
||||
import { program } from 'commander'
|
||||
|
||||
import { unlock, readLock, writeLock } from './lock'
|
||||
import { Config } from './types'
|
||||
import { init } from './config'
|
||||
|
||||
import info from './handlers/info'
|
||||
import check from './handlers/check'
|
||||
import backup from './handlers/backup'
|
||||
import restore from './handlers/restore'
|
||||
import forget from './handlers/forget'
|
||||
import { cron } from './handlers/cron'
|
||||
import exec from './handlers/exec'
|
||||
import install from './handlers/install'
|
||||
import { uninstall } from './handlers/uninstall'
|
||||
import { upgrade } from './handlers/upgrade'
|
||||
|
||||
export const VERSION = '0.20'
|
||||
export const INSTALL_DIR = '/usr/local/bin'
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.log(err.message)
|
||||
unlock()
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
let queue: Function = () => {}
|
||||
const enqueue = (fn: Function) => (cmd: any) => {
|
||||
queue = () => fn(cmd.opts())
|
||||
}
|
||||
|
||||
program.storeOptionsAsProperties()
|
||||
program.name('autorestic').version(VERSION)
|
||||
|
||||
program.option('-c, --config <path>', 'Config file').option('-v, --verbose', 'Verbosity', false)
|
||||
|
||||
program.command('info').action(enqueue(info))
|
||||
|
||||
program
|
||||
.command('check')
|
||||
.description('Checks and initializes backend as needed')
|
||||
.option('-b, --backend <backends...>')
|
||||
.option('-a, --all')
|
||||
.action(enqueue(check))
|
||||
|
||||
program.command('backup').description('Performs a backup').option('-b, --backend <backends...>').option('-a, --all').action(enqueue(backup))
|
||||
|
||||
program
|
||||
.command('restore')
|
||||
.description('Restores data to a specified folder from a location')
|
||||
.requiredOption('-l, --location <location>')
|
||||
.option('--from <backend>')
|
||||
.requiredOption('--to <path>', 'Path to save the restored data to')
|
||||
.action(enqueue(restore))
|
||||
|
||||
program
|
||||
.command('forget')
|
||||
.description('This will prune and remove data according to your policies')
|
||||
.option('-l, --location <locations...>')
|
||||
.option('-a, --all')
|
||||
.option('--dry-run')
|
||||
.action(enqueue(forget))
|
||||
|
||||
program
|
||||
.command('cron')
|
||||
.description('Intended to be triggered by an automated system like systemd or crontab.')
|
||||
.option('-a, --all')
|
||||
.action(enqueue(cron))
|
||||
|
||||
program
|
||||
.command('exec')
|
||||
.description('Run any native restic command on desired backends')
|
||||
.option('-b, --backend <backends...>')
|
||||
.option('-a, --all')
|
||||
.action(({ args, all, backend }) => {
|
||||
queue = () => exec({ all, backend }, args)
|
||||
})
|
||||
|
||||
program.command('install').description('Installs both restic and autorestic to /usr/local/bin').action(enqueue(install))
|
||||
|
||||
program.command('uninstall').description('Uninstalls autorestic from the system').action(enqueue(uninstall))
|
||||
|
||||
program.command('upgrade').alias('update').description('Checks and installs new autorestic versions').action(enqueue(upgrade))
|
||||
|
||||
const { verbose, config: configFile } = program.parse(process.argv)
|
||||
|
||||
export const VERBOSE = verbose
|
||||
export let config: Config = init(configFile)
|
||||
|
||||
try {
|
||||
const lock = readLock()
|
||||
if (lock.running) throw new Error('An instance of autorestic is already running for this config file'.red)
|
||||
|
||||
writeLock({
|
||||
...lock,
|
||||
running: true,
|
||||
})
|
||||
queue()
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
} finally {
|
||||
unlock()
|
||||
}
|
@ -1,26 +1,18 @@
|
||||
import { config } from './autorestic'
|
||||
import { config } from './'
|
||||
import { fill, treeToString } from './utils'
|
||||
|
||||
|
||||
|
||||
const showAll = () => {
|
||||
console.log('\n\n' + fill(32, '_') + 'LOCATIONS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.locations)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(
|
||||
data,
|
||||
['to:', 'from:', 'hooks:', 'options:', 'cron:'],
|
||||
))
|
||||
}
|
||||
console.log('\n\n' + fill(32, '_') + 'LOCATIONS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.locations)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(data, ['to:', 'from:', 'hooks:', 'options:', 'cron:']))
|
||||
}
|
||||
|
||||
console.log('\n\n' + fill(32, '_') + 'BACKENDS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.backends)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(
|
||||
data,
|
||||
['type:', 'path:', 'key:'],
|
||||
))
|
||||
}
|
||||
console.log('\n\n' + fill(32, '_') + 'BACKENDS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.backends)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(data, ['type:', 'path:', 'key:']))
|
||||
}
|
||||
}
|
||||
|
||||
export default showAll
|
Loading…
Reference in new issue