mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2024-12-22 08:16:25 +00:00
ordered gitignore
This commit is contained in:
parent
2fd9e2dd22
commit
e80db74af4
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,10 +1,19 @@
|
||||
# Editors
|
||||
.idea
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
package-lock.json
|
||||
.idea
|
||||
yarn.lock
|
||||
|
||||
# Build & Runtime
|
||||
bin
|
||||
lib
|
||||
data
|
||||
restore
|
||||
docker
|
||||
Dockerfile
|
||||
|
||||
.autorestic.yml
|
||||
# Config
|
||||
.autorestic.yml
|
||||
.docker.yml
|
@ -25,7 +25,7 @@ export const { _: commands, ...flags } = minimist(process.argv.slice(2), {
|
||||
string: ['l', 'b'],
|
||||
})
|
||||
|
||||
export const VERSION = '0.12'
|
||||
export const VERSION = '0.13'
|
||||
export const INSTALL_DIR = '/usr/local/bin'
|
||||
export const VERBOSE = flags.verbose
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { Writer } from 'clitastic'
|
||||
|
||||
import { config, VERBOSE } from './autorestic'
|
||||
import { Backend, Backends, Locations } from './types'
|
||||
import { exec, ConfigError } from './utils'
|
||||
import { exec, ConfigError, pathRelativeToConfigFile } from './utils'
|
||||
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ const ALREADY_EXISTS = /(?=.*already)(?=.*config).*/
|
||||
export const getPathFromBackend = (backend: Backend): string => {
|
||||
switch (backend.type) {
|
||||
case 'local':
|
||||
return backend.path
|
||||
return pathRelativeToConfigFile(backend.path)
|
||||
case 'b2':
|
||||
case 'azure':
|
||||
case 'gs':
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { Writer } from 'clitastic'
|
||||
import { mkdirSync } from 'fs'
|
||||
|
||||
import { config, VERBOSE } from './autorestic'
|
||||
import { getEnvFromBackend } from './backend'
|
||||
import { Locations, Location } from './types'
|
||||
import { LocationFromPrefixes } from './config'
|
||||
import { Locations, Location, Backend } from './types'
|
||||
import {
|
||||
exec,
|
||||
ConfigError,
|
||||
@ -10,27 +12,67 @@ import {
|
||||
getFlagsFromLocation,
|
||||
makeArrayIfIsNot,
|
||||
execPlain,
|
||||
MeasureDuration, fill,
|
||||
MeasureDuration,
|
||||
fill,
|
||||
decodeLocationFromPrefix,
|
||||
hash, checkIfDockerVolumeExistsOrFail,
|
||||
} from './utils'
|
||||
|
||||
|
||||
|
||||
export const backupFromFilesystem = (from: string, location: Location, backend: Backend, tags?: string[]) => {
|
||||
const path = pathRelativeToConfigFile(from)
|
||||
|
||||
const cmd = exec(
|
||||
'restic',
|
||||
['backup', '.', ...getFlagsFromLocation(location, 'backup')],
|
||||
{ env: getEnvFromBackend(backend), cwd: path },
|
||||
)
|
||||
|
||||
if (VERBOSE) console.log(cmd.out, cmd.err)
|
||||
}
|
||||
|
||||
export const backupFromVolume = (volume: string, location: Location, backend: Backend) => {
|
||||
const tmp = pathRelativeToConfigFile(hash(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)
|
||||
} finally {
|
||||
execPlain(`rm -rf ${tmp}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const backupSingle = (name: string, to: string, location: Location) => {
|
||||
if (!config) throw ConfigError
|
||||
const delta = new MeasureDuration()
|
||||
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
|
||||
|
||||
const backend = config.backends[to]
|
||||
const path = pathRelativeToConfigFile(location.from)
|
||||
try {
|
||||
const backend = config.backends[to]
|
||||
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||
|
||||
const cmd = exec(
|
||||
'restic',
|
||||
['backup', path, ...getFlagsFromLocation(location, 'backup')],
|
||||
{ env: getEnvFromBackend(backend) },
|
||||
)
|
||||
switch (type) {
|
||||
|
||||
if (VERBOSE) console.log(cmd.out, cmd.err)
|
||||
writer.done(`${name}${to.blue} : ${'Done ✓'.green} (${delta.finished(true)})`)
|
||||
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) => {
|
||||
@ -40,8 +82,8 @@ export const backupLocation = (name: string, location: Location) => {
|
||||
|
||||
if (location.hooks && location.hooks.before)
|
||||
for (const command of makeArrayIfIsNot(location.hooks.before)) {
|
||||
const cmd = execPlain(command)
|
||||
if (cmd) console.log(cmd.out, cmd.err)
|
||||
const cmd = execPlain(command, {})
|
||||
console.log(cmd.out, cmd.err)
|
||||
}
|
||||
|
||||
for (const t of makeArrayIfIsNot(location.to)) {
|
||||
@ -52,7 +94,7 @@ export const backupLocation = (name: string, location: Location) => {
|
||||
if (location.hooks && location.hooks.after)
|
||||
for (const command of makeArrayIfIsNot(location.hooks.after)) {
|
||||
const cmd = execPlain(command)
|
||||
if (cmd) console.log(cmd.out, cmd.err)
|
||||
console.log(cmd.out, cmd.err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,12 @@ import { makeArrayIfIsNot, makeObjectKeysLowercase, rand } from './utils'
|
||||
|
||||
|
||||
|
||||
export enum LocationFromPrefixes {
|
||||
Filesystem,
|
||||
DockerVolume
|
||||
}
|
||||
|
||||
|
||||
export const normalizeAndCheckBackends = (config: Config) => {
|
||||
config.backends = makeObjectKeysLowercase(config.backends)
|
||||
|
||||
|
@ -10,6 +10,7 @@ import { checkAndConfigureBackends, getBackendsFromLocations, getEnvFromBackend
|
||||
import { backupAll } from './backup'
|
||||
import { forgetAll } from './forget'
|
||||
import showAll from './info'
|
||||
import { restoreSingle } from './restore'
|
||||
import { Backends, Flags, Locations } from './types'
|
||||
import {
|
||||
checkIfCommandIsAvailable,
|
||||
@ -86,36 +87,12 @@ const handlers: Handlers = {
|
||||
if (!config) throw ConfigError
|
||||
checkIfResticIsAvailable()
|
||||
|
||||
if (!flags.to) {
|
||||
console.log(`You need to specify the restore path with --to`.red)
|
||||
return
|
||||
}
|
||||
|
||||
const locations = parseLocations(flags)
|
||||
for (const [name, location] of Object.entries(locations)) {
|
||||
const baseText = name.green + '\t\t'
|
||||
const w = new Writer(baseText + `Starting...`)
|
||||
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)
|
||||
|
||||
let backend: string = Array.isArray(location.to) ? location.to[0] : location.to
|
||||
if (flags.from) {
|
||||
if (!location.to.includes(flags.from)) {
|
||||
w.done(baseText + `Backend ${flags.from} is not a valid location for ${name}`.red)
|
||||
continue
|
||||
}
|
||||
backend = flags.from
|
||||
w.replaceLn(baseText + `Restoring from ${backend.blue}...`)
|
||||
} else if (Array.isArray(location.to) && location.to.length > 1) {
|
||||
w.replaceLn(baseText + `Restoring from ${backend.blue}...\tTo select a specific backend pass the ${'--from'.blue} flag`)
|
||||
}
|
||||
const env = getEnvFromBackend(config.backends[backend])
|
||||
|
||||
exec(
|
||||
'restic',
|
||||
['restore', 'latest', '--path', resolve(location.from), '--target', flags.to],
|
||||
{ env },
|
||||
)
|
||||
w.done(name.green + '\t\tDone 🎉')
|
||||
}
|
||||
restoreSingle(keys[0], flags.from, flags.to)
|
||||
},
|
||||
forget(args, flags) {
|
||||
if (!config) throw ConfigError
|
||||
@ -140,7 +117,7 @@ const handlers: Handlers = {
|
||||
console.log(out, err)
|
||||
}
|
||||
},
|
||||
async info() {
|
||||
info() {
|
||||
showAll()
|
||||
},
|
||||
async install() {
|
||||
@ -224,7 +201,7 @@ const handlers: Handlers = {
|
||||
w.replaceLn('Downloading binary... 🌎')
|
||||
await downloadFile(dl.browser_download_url, to)
|
||||
|
||||
exec('chmod', ['+x', to])
|
||||
chmodSync(to, 0o755)
|
||||
}
|
||||
|
||||
w.done('All up to date! 🚀')
|
||||
|
82
src/restore.ts
Normal file
82
src/restore.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { Writer } from 'clitastic'
|
||||
import { resolve } from 'path'
|
||||
|
||||
import { config } from './autorestic'
|
||||
import { getEnvFromBackend } from './backend'
|
||||
import { LocationFromPrefixes } from './config'
|
||||
import { Backend } from './types'
|
||||
import {
|
||||
checkIfDockerVolumeExistsOrFail,
|
||||
ConfigError,
|
||||
decodeLocationFromPrefix,
|
||||
exec, execPlain,
|
||||
hash,
|
||||
pathRelativeToConfigFile,
|
||||
} from './utils'
|
||||
|
||||
|
||||
|
||||
export const restoreToFilesystem = (from: string, to: string, backend: Backend) => {
|
||||
exec(
|
||||
'restic',
|
||||
['restore', 'latest', '--path', resolve(from), '--target', to],
|
||||
{ env: getEnvFromBackend(backend) },
|
||||
)
|
||||
}
|
||||
|
||||
export const restoreToVolume = (volume: string, backend: Backend) => {
|
||||
const tmp = pathRelativeToConfigFile(hash(volume))
|
||||
|
||||
try {
|
||||
restoreToFilesystem(tmp, tmp, backend)
|
||||
try {
|
||||
checkIfDockerVolumeExistsOrFail(volume)
|
||||
} catch {
|
||||
execPlain(`docker volume create ${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 /backup /data`)
|
||||
execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar xf /backup/archive.tar -C /data`)
|
||||
} finally {
|
||||
execPlain(`rm -rf ${tmp}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const restoreSingle = (locationName: string, from: string, to?: string) => {
|
||||
if (!config) throw ConfigError
|
||||
|
||||
const location = config.locations[locationName]
|
||||
|
||||
const baseText = locationName.green + '\t\t'
|
||||
const w = new Writer(baseText + `Starting...`)
|
||||
|
||||
let backendName: string = Array.isArray(location.to) ? location.to[0] : location.to
|
||||
if (from) {
|
||||
if (!location.to.includes(from)) {
|
||||
w.done(baseText + `Backend ${from} is not a valid location for ${locationName}`.red)
|
||||
return
|
||||
}
|
||||
backendName = from
|
||||
w.replaceLn(baseText + `Restoring from ${backendName.blue}...`)
|
||||
} else if (Array.isArray(location.to) && location.to.length > 1) {
|
||||
w.replaceLn(baseText + `Restoring from ${backendName.blue}...\tTo select a specific backend pass the ${'--from'.blue} flag`)
|
||||
}
|
||||
const backend = config.backends[backendName]
|
||||
|
||||
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||
switch (type) {
|
||||
|
||||
case LocationFromPrefixes.Filesystem:
|
||||
if (!to) throw new Error(`You need to specify the restore path with --to`.red)
|
||||
restoreToFilesystem(value, to, backend)
|
||||
break
|
||||
|
||||
case LocationFromPrefixes.DockerVolume:
|
||||
restoreToVolume(value, backend)
|
||||
break
|
||||
|
||||
}
|
||||
w.done(locationName.green + '\t\tDone 🎉')
|
||||
}
|
||||
|
60
src/utils.ts
60
src/utils.ts
@ -1,23 +1,18 @@
|
||||
import { spawnSync, SpawnSyncOptions } from 'child_process'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { createWriteStream, unlinkSync, renameSync } from 'fs'
|
||||
import { dirname, isAbsolute, join, resolve } from 'path'
|
||||
import { homedir, tmpdir } from 'os'
|
||||
|
||||
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 } from './config'
|
||||
import { CONFIG_FILE, LocationFromPrefixes } from './config'
|
||||
import { Location } from './types'
|
||||
|
||||
|
||||
|
||||
export const exec = (
|
||||
command: string,
|
||||
args: string[],
|
||||
{ env, ...rest }: SpawnSyncOptions = {},
|
||||
) => {
|
||||
const cmd = spawnSync(command, args, {
|
||||
export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyncOptions = {}) => {
|
||||
const { stdout, stderr, status } = spawnSync(command, args, {
|
||||
...rest,
|
||||
env: {
|
||||
...process.env,
|
||||
@ -25,18 +20,15 @@ export const exec = (
|
||||
},
|
||||
})
|
||||
|
||||
const out = cmd.stdout && cmd.stdout.toString().trim()
|
||||
const err = cmd.stderr && cmd.stderr.toString().trim()
|
||||
const out = stdout && stdout.toString().trim()
|
||||
const err = stderr && stderr.toString().trim()
|
||||
|
||||
return { out, err }
|
||||
return { out, err, status }
|
||||
}
|
||||
|
||||
export const execPlain = (command: string, opt: SpawnSyncOptions = {}) => {
|
||||
const split = command.split(' ')
|
||||
if (split.length < 1) {
|
||||
console.log(`The command ${command} is not valid`.red)
|
||||
return
|
||||
}
|
||||
if (split.length < 1) throw new Error(`The command ${command} is not valid`.red)
|
||||
|
||||
return exec(split[0], split.slice(1), opt)
|
||||
}
|
||||
@ -175,3 +167,31 @@ export class MeasureDuration {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
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 checkIfDockerVolumeExistsOrFail = (volume: string) => {
|
||||
const cmd = exec('docker', [
|
||||
'volume', 'inspect', volume,
|
||||
])
|
||||
if (cmd.err.length > 0)
|
||||
throw new Error('Volume not found')
|
||||
}
|
Loading…
Reference in New Issue
Block a user