mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-09-06 10:30:39 +00:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
678aa96c06 | ||
|
e51eacf13c | ||
12d2e010bb | |||
e25e65e052 | |||
4491cfd536 | |||
d0e82b47e1 | |||
|
90f9a998e8 | ||
|
b40adcae1f | ||
|
ad5afab355 | ||
|
5b0011330c |
120
README.md
120
README.md
@@ -14,13 +14,14 @@ Autorestic is a wrapper around the amazing [restic](https://restic.net/). While
|
|||||||
- Simple interface
|
- Simple interface
|
||||||
- Fully encrypted
|
- Fully encrypted
|
||||||
|
|
||||||
###### 📒 Docs
|
### 📒 Docs
|
||||||
|
|
||||||
- [Locations](#-locations)
|
* [Locations](#-locations)
|
||||||
- [Pruning & Deleting old files](#pruning-and-snapshot-policies)
|
* [Pruning & Deleting old files](#pruning-and-snapshot-policies)
|
||||||
- [Excluding files](#excluding-filesfolders)
|
* [Excluding files](#excluding-filesfolders)
|
||||||
- [Hooks](#before--after-hooks)
|
* [Hooks](#before--after-hooks)
|
||||||
- [Backends](#-backends)
|
* [Backends](#-backends)
|
||||||
|
* [Commands](#-commands)
|
||||||
|
|
||||||
## 🛳 Installation
|
## 🛳 Installation
|
||||||
|
|
||||||
@@ -78,19 +79,10 @@ autorestic backup -a
|
|||||||
|
|
||||||
### 📼 Restore
|
### 📼 Restore
|
||||||
|
|
||||||
```
|
|
||||||
autorestic restore -a --to /path/where/to/restore
|
|
||||||
```
|
|
||||||
|
|
||||||
This will restore all the locations to the selected target. If for one location there are more than one backends specified autorestic will take the first one.
|
|
||||||
|
|
||||||
Lets see a more realistic example (from the config above)
|
|
||||||
```
|
```
|
||||||
autorestic restore -l home --from hdd --to /path/where/to/restore
|
autorestic restore -l home --from hdd --to /path/where/to/restore
|
||||||
```
|
```
|
||||||
|
|
||||||
This will restore the location `home` to the `/path/where/to/restore` folder and taking the data from the backend `hdd`
|
|
||||||
|
|
||||||
### 📲 Updates
|
### 📲 Updates
|
||||||
|
|
||||||
Autorestic can update itself! Super handy right? Simply run `autorestic update` and we will check for you if there are updates for restic and autorestic and install them if necessary.
|
Autorestic can update itself! Super handy right? Simply run `autorestic update` and we will check for you if there are updates for restic and autorestic and install them if necessary.
|
||||||
@@ -233,6 +225,104 @@ backends:
|
|||||||
B2_ACCOUNT_KEY: backblaze_account_key
|
B2_ACCOUNT_KEY: backblaze_account_key
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 👉 Commands
|
||||||
|
|
||||||
|
* [info](#info)
|
||||||
|
* [check](#check)
|
||||||
|
* [backup](#backup)
|
||||||
|
* [forget](#forget)
|
||||||
|
* [restore](#restore)
|
||||||
|
* [exec](#exec)
|
||||||
|
* [intall](#install)
|
||||||
|
* [uninstall](#uninstall)
|
||||||
|
* [upgrade](#upgrade)
|
||||||
|
|
||||||
|
|
||||||
|
### Info
|
||||||
|
|
||||||
|
```
|
||||||
|
autorestic info
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows all the information in the config file. Usefull for a quick overview of what location backups where.
|
||||||
|
|
||||||
|
Pro tip: if it gets a bit long you can read it more easily with `autorestic info | less` 😉
|
||||||
|
|
||||||
|
### Check
|
||||||
|
|
||||||
|
```
|
||||||
|
autorestic check [-b, --backend] [-a, --all]
|
||||||
|
```
|
||||||
|
|
||||||
|
Checks the backends and configures them if needed. Can be applied to all with the `-a` flag or by specifying one or more backends with the `-b` or `--backend` flag.
|
||||||
|
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
```
|
||||||
|
autorestic backup [-l, --location] [-a, --all]
|
||||||
|
```
|
||||||
|
|
||||||
|
Performes a backup of all locations if the `-a` flag is passed. To only backup some locations pass one or more `-l` or `--location` flags.
|
||||||
|
|
||||||
|
|
||||||
|
### Restore
|
||||||
|
|
||||||
|
```
|
||||||
|
autorestic restore [-l, --location] [--from backend] [--to <out dir>]
|
||||||
|
```
|
||||||
|
|
||||||
|
This will restore all the locations to the selected target. If for one location there are more than one backends specified autorestic will take the first one.
|
||||||
|
|
||||||
|
Lets see a more realistic example (from the config above)
|
||||||
|
```
|
||||||
|
autorestic restore -l home --from hdd --to /path/where/to/restore
|
||||||
|
```
|
||||||
|
|
||||||
|
This will restore the location `home` to the `/path/where/to/restore` folder and taking the data from the backend `hdd`
|
||||||
|
|
||||||
|
```
|
||||||
|
autorestic restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Performes a backup of all locations if the `-a` flag is passed. To only backup some locations pass one or more `-l` or `--location` flags.
|
||||||
|
|
||||||
|
### Forget
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
autorestic forget [-l, --location] [-a, --all] [--dry-run]
|
||||||
|
```
|
||||||
|
|
||||||
|
This will prune and remove old data form the backends according to the [keep policy you have specified for the location](#pruning-and-snapshot-policies)
|
||||||
|
|
||||||
|
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.
|
||||||
|
|
||||||
|
|
||||||
|
### Exec
|
||||||
|
|
||||||
|
```
|
||||||
|
autorestic exec [-b, --backend] [-a, --all] <command> -- [native options]
|
||||||
|
```
|
||||||
|
|
||||||
|
This is avery handy command which enables you to run any native restic command on desired backends. An example would be listing all the snapshots of all your backends:
|
||||||
|
|
||||||
|
```
|
||||||
|
autorestic exec -a -- snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install
|
||||||
|
|
||||||
|
Installs both restic and autorestic
|
||||||
|
|
||||||
|
#### Uninstall
|
||||||
|
|
||||||
|
Uninstall both restic and autorestic
|
||||||
|
|
||||||
|
#### Upgrade
|
||||||
|
|
||||||
|
Upgrades both restic and autorestic automagically
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
This amazing people helped the project!
|
This amazing people helped the project!
|
||||||
|
@@ -19,6 +19,7 @@
|
|||||||
"clitastic": "0.0.1",
|
"clitastic": "0.0.1",
|
||||||
"colors": "^1.3.3",
|
"colors": "^1.3.3",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0",
|
||||||
|
"uhrwerk": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,7 +25,7 @@ export const { _: commands, ...flags } = minimist(process.argv.slice(2), {
|
|||||||
string: ['l', 'b'],
|
string: ['l', 'b'],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const VERSION = '0.9'
|
export const VERSION = '0.11'
|
||||||
export const INSTALL_DIR = '/usr/local/bin'
|
export const INSTALL_DIR = '/usr/local/bin'
|
||||||
export const VERBOSE = flags.verbose
|
export const VERBOSE = flags.verbose
|
||||||
|
|
||||||
|
@@ -3,12 +3,21 @@ import { Writer } from 'clitastic'
|
|||||||
import { config, VERBOSE } from './autorestic'
|
import { config, VERBOSE } from './autorestic'
|
||||||
import { getEnvFromBackend } from './backend'
|
import { getEnvFromBackend } from './backend'
|
||||||
import { Locations, Location } from './types'
|
import { Locations, Location } from './types'
|
||||||
import { exec, ConfigError, pathRelativeToConfigFile, getFlagsFromLocation, makeArrayIfIsNot, execPlain } from './utils'
|
import {
|
||||||
|
exec,
|
||||||
|
ConfigError,
|
||||||
|
pathRelativeToConfigFile,
|
||||||
|
getFlagsFromLocation,
|
||||||
|
makeArrayIfIsNot,
|
||||||
|
execPlain,
|
||||||
|
MeasureDuration, fill,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const backupSingle = (name: string, to: string, location: Location) => {
|
export const backupSingle = (name: string, to: string, location: Location) => {
|
||||||
if (!config) throw ConfigError
|
if (!config) throw ConfigError
|
||||||
|
const delta = new MeasureDuration()
|
||||||
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
|
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
|
||||||
|
|
||||||
const backend = config.backends[to]
|
const backend = config.backends[to]
|
||||||
@@ -21,12 +30,12 @@ export const backupSingle = (name: string, to: string, location: Location) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (VERBOSE) console.log(cmd.out, cmd.err)
|
if (VERBOSE) console.log(cmd.out, cmd.err)
|
||||||
writer.done(name + to.blue + ' : ' + 'Done ✓'.green)
|
writer.done(`${name}${to.blue} : ${'Done ✓'.green} (${delta.finished(true)})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const backupLocation = (name: string, location: Location) => {
|
export const backupLocation = (name: string, location: Location) => {
|
||||||
const display = name.yellow + ' ▶ '
|
const display = name.yellow + ' ▶ '
|
||||||
const filler = new Array(name.length + 3).fill(' ').join('')
|
const filler = fill(name.length + 3)
|
||||||
let first = true
|
let first = true
|
||||||
|
|
||||||
if (location.hooks && location.hooks.before)
|
if (location.hooks && location.hooks.before)
|
||||||
|
@@ -3,7 +3,14 @@ import { Writer } from 'clitastic'
|
|||||||
import { config, VERBOSE } from './autorestic'
|
import { config, VERBOSE } from './autorestic'
|
||||||
import { getEnvFromBackend } from './backend'
|
import { getEnvFromBackend } from './backend'
|
||||||
import { Locations, Location, Flags } from './types'
|
import { Locations, Location, Flags } from './types'
|
||||||
import { exec, ConfigError, pathRelativeToConfigFile, getFlagsFromLocation, makeArrayIfIsNot } from './utils'
|
import {
|
||||||
|
exec,
|
||||||
|
ConfigError,
|
||||||
|
pathRelativeToConfigFile,
|
||||||
|
getFlagsFromLocation,
|
||||||
|
makeArrayIfIsNot,
|
||||||
|
fill,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +42,7 @@ export const forgetSingle = (name: string, to: string, location: Location, dryRu
|
|||||||
|
|
||||||
export const forgetLocation = (name: string, backup: Location, dryRun: boolean) => {
|
export const forgetLocation = (name: string, backup: Location, dryRun: boolean) => {
|
||||||
const display = name.yellow + ' ▶ '
|
const display = name.yellow + ' ▶ '
|
||||||
const filler = new Array(name.length + 3).fill(' ').join('')
|
const filler = fill(name.length + 3)
|
||||||
let first = true
|
let first = true
|
||||||
|
|
||||||
for (const t of makeArrayIfIsNot(backup.to)) {
|
for (const t of makeArrayIfIsNot(backup.to)) {
|
||||||
|
@@ -8,6 +8,7 @@ import { config, INSTALL_DIR, VERSION } from './autorestic'
|
|||||||
import { checkAndConfigureBackends, getBackendsFromLocations, getEnvFromBackend } from './backend'
|
import { checkAndConfigureBackends, getBackendsFromLocations, getEnvFromBackend } from './backend'
|
||||||
import { backupAll } from './backup'
|
import { backupAll } from './backup'
|
||||||
import { forgetAll } from './forget'
|
import { forgetAll } from './forget'
|
||||||
|
import showAll from './info'
|
||||||
import { Backends, Flags, Locations } from './types'
|
import { Backends, Flags, Locations } from './types'
|
||||||
import {
|
import {
|
||||||
checkIfCommandIsAvailable,
|
checkIfCommandIsAvailable,
|
||||||
@@ -138,6 +139,9 @@ const handlers: Handlers = {
|
|||||||
console.log(out, err)
|
console.log(out, err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async info() {
|
||||||
|
showAll()
|
||||||
|
},
|
||||||
async install() {
|
async install() {
|
||||||
try {
|
try {
|
||||||
checkIfResticIsAvailable()
|
checkIfResticIsAvailable()
|
||||||
@@ -240,6 +244,7 @@ export const help = () => {
|
|||||||
`\n -c, --config Specify config file. Default: .autorestic.yml` +
|
`\n -c, --config Specify config file. Default: .autorestic.yml` +
|
||||||
'\n' +
|
'\n' +
|
||||||
'\nCommands:'.yellow +
|
'\nCommands:'.yellow +
|
||||||
|
'\n info Show all locations and backends' +
|
||||||
'\n check [-b, --backend] [-a, --all] Check backends' +
|
'\n check [-b, --backend] [-a, --all] Check backends' +
|
||||||
'\n backup [-l, --location] [-a, --all] Backup all or specified locations' +
|
'\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 forget [-l, --location] [-a, --all] [--dry-run] Forget old snapshots according to declared policies' +
|
||||||
|
28
src/info.ts
Normal file
28
src/info.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { config } from './autorestic'
|
||||||
|
import { ConfigError, fill, treeToString } from './utils'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const showAll = () => {
|
||||||
|
if (!config) throw ConfigError
|
||||||
|
|
||||||
|
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:'],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
61
src/utils.ts
61
src/utils.ts
@@ -1,8 +1,12 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { spawnSync, SpawnSyncOptions } from 'child_process'
|
import { spawnSync, SpawnSyncOptions } from 'child_process'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { createWriteStream } from 'fs'
|
import { createWriteStream } from 'fs'
|
||||||
import { dirname, isAbsolute, resolve } from 'path'
|
import { dirname, isAbsolute, join, resolve } from 'path'
|
||||||
|
import { homedir } from 'os'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Duration, Humanizer } from 'uhrwerk'
|
||||||
|
|
||||||
import { CONFIG_FILE } from './config'
|
import { CONFIG_FILE } from './config'
|
||||||
import { Location } from './types'
|
import { Location } from './types'
|
||||||
|
|
||||||
@@ -93,6 +97,11 @@ export const pathRelativeToConfigFile = (path: string): string => isAbsolute(pat
|
|||||||
? path
|
? path
|
||||||
: resolve(dirname(CONFIG_FILE), 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 ConfigError = new Error('Config file not found')
|
||||||
|
|
||||||
export const getFlagsFromLocation = (location: Location, command?: string): string[] => {
|
export const getFlagsFromLocation = (location: Location, command?: string): string[] => {
|
||||||
@@ -106,10 +115,54 @@ export const getFlagsFromLocation = (location: Location, command?: string): stri
|
|||||||
let flags: string[] = []
|
let flags: string[] = []
|
||||||
// Map the flags to an array for the exec function.
|
// Map the flags to an array for the exec function.
|
||||||
for (let [flag, values] of Object.entries(all))
|
for (let [flag, values] of Object.entries(all))
|
||||||
for (const value of makeArrayIfIsNot(values))
|
for (const value of makeArrayIfIsNot(values)) {
|
||||||
flags = [...flags, `--${String(flag)}`, String(value)]
|
const stringValue = String(value)
|
||||||
|
const resolvedTilde = resolveTildePath(stringValue)
|
||||||
|
flags = [...flags, `--${String(flag)}`, resolvedTilde === null ? stringValue : resolvedTilde]
|
||||||
|
}
|
||||||
|
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeArrayIfIsNot = <T>(maybeArray: T | T[]): T[] => Array.isArray(maybeArray) ? maybeArray : [maybeArray]
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user