mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-09-06 02:20:39 +00:00
Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
fd2fd91635 | |||
9c09ce1d79 | |||
c2f6f51789 | |||
|
f09cf90653 | ||
|
d352aced37 | ||
|
563d4ffb96 | ||
|
1c6a061dd1 | ||
|
504ad639ab | ||
f7a15c6d86 | |||
|
2f0092befe | ||
|
1026e68b68 | ||
2389c59aa9 | |||
087aeaf578 | |||
3b7062f733 | |||
|
96b63c744b | ||
|
9669b70e20 | ||
|
bcb081234c | ||
|
336f44e9dc | ||
|
d0cda7f1d5 | ||
|
a8f4c23254 | ||
|
1c9f6d7d91 | ||
|
18c3f4a06f | ||
632062a23f | |||
3d1d7ba256 | |||
|
417c54db4d | ||
|
a9696bbc0c | ||
|
45f7506478 | ||
|
d7cdeafe60 | ||
|
cf09cdbb30 | ||
|
88059fe405 | ||
|
cdf18430b6 | ||
|
352754dad9 | ||
|
b68dc75053 | ||
|
6a055d3114 | ||
|
b5daff07eb | ||
b2d01d77d9 | |||
f41c042fce | |||
a81498ac42 | |||
1731ee30b3 | |||
1f4f1a1855 | |||
13cb764067 | |||
8058f37368 | |||
|
57ffa1e3fa | ||
|
671542cd30 | ||
|
322df9f0bd | ||
|
652158d1ed | ||
|
06ce8180fb | ||
81f513d77b | |||
e32521e6ea | |||
f5c5b39b30 | |||
e016c8defc | |||
a2e0a0c9cc | |||
|
f9b04ea342 | ||
770c9dd7d4 | |||
|
851bbe5776 | ||
|
8fb6bdb3c6 | ||
|
47f5d91e89 | ||
|
de27034b94 | ||
|
9dafe9d36a | ||
|
d47e7d0912 | ||
|
e47d6be854 | ||
|
993fe072e2 | ||
|
3d1e28e574 | ||
|
3c0ebdfb4a | ||
|
2653633c91 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,8 +1,10 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
.idea
|
||||
yarn.lock
|
||||
|
||||
config.yml
|
||||
bin
|
||||
lib
|
||||
data
|
||||
|
||||
.autorestic.yml
|
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
||||
semi: false
|
||||
singleQuote: true
|
||||
trailingComma: 'es5'
|
238
README.md
238
README.md
@@ -1,9 +1,241 @@
|
||||
# autorestic
|
||||
High level CLI utility for restic
|
||||
High backup level CLI utility for [restic](https://restic.net/).
|
||||
|
||||
Autorestic is a wrapper around the amazing [restic](https://restic.net/). While being amazing the restic cli can be a bit overwhelming and difficoult to manage if you have many different location that you want to backup to multiple locations. This utility is aimed at making this easier 🙂
|
||||
|
||||
## Installation
|
||||

|
||||
|
||||
## 🌈 Features
|
||||
|
||||
- Config files, no CLI
|
||||
- Predictable
|
||||
- Backup locations to multiple backends
|
||||
- Snapshot policies and pruning
|
||||
- Simple interface
|
||||
- Fully encrypted
|
||||
|
||||
###### 📒 Docs
|
||||
|
||||
- [Locations](#-locations)
|
||||
- [Pruning & Deleting old files](#pruning-and-snapshot-policies)
|
||||
- [Excluding files](#excluding-filesfolders)
|
||||
- [Hooks](#before--after-hooks)
|
||||
- [Backends](#-backends)
|
||||
|
||||
## 🛳 Installation
|
||||
|
||||
Linux & macOS. Windows is not supported.
|
||||
|
||||
```
|
||||
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | sh
|
||||
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
|
||||
```
|
||||
|
||||
## 🚀 Quickstart
|
||||
|
||||
### Setup
|
||||
|
||||
First we need to configure our locations and backends. Simply create a `.autorestic.yml` either in your home directory of in the folder from which you will execute `autorestic`.
|
||||
|
||||
Optionally you can specify the location of your config file by passing it as argument: `autorestic -c ../path/config.yml`
|
||||
|
||||
```yaml
|
||||
locations:
|
||||
home:
|
||||
from: /home/me
|
||||
to: remote
|
||||
|
||||
important:
|
||||
from: /path/to/important/stuff
|
||||
to:
|
||||
- remote
|
||||
- hdd
|
||||
|
||||
backends:
|
||||
remote:
|
||||
type: b2
|
||||
path: 'myBucket:backup/home'
|
||||
B2_ACCOUNT_ID: account_id
|
||||
B2_ACCOUNT_KEY: account_key
|
||||
|
||||
hdd:
|
||||
type: local
|
||||
path: /mnt/my_external_storage
|
||||
```
|
||||
|
||||
Then we check if everything is correct by running the `check` command. We will pass the `-a` (or `--all`) to tell autorestic to check all the locations.
|
||||
|
||||
If we would check only one location we could run the following: `autorestic check -l home`. Otherwise simpply check all locations with `autorestic check -a`
|
||||
|
||||
##### Note
|
||||
|
||||
Note that the data is automatically encrypted on the server. The key will be generated and added to your config file. Every backend will have a separate key. You should keep a copy of the keys somewhere in case your server dies. Otherwise DATA IS LOST!
|
||||
|
||||
### 📦 Backup
|
||||
|
||||
```
|
||||
autorestic backup -a
|
||||
```
|
||||
|
||||
### 📼 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
|
||||
```
|
||||
|
||||
This will restore the location `home` to the `/path/where/to/restore` folder and taking the data from the backend `hdd`
|
||||
|
||||
### 📲 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.
|
||||
|
||||
## 🗂 Locations
|
||||
|
||||
A location simply a folder on your machine that restic will backup. The paths can be relative from the config file. A location can have multiple backends, so that the data is secured across multiple servers.
|
||||
|
||||
```yaml
|
||||
locations:
|
||||
my-location-name:
|
||||
from: path/to/backup
|
||||
to:
|
||||
- name-of-backend
|
||||
- also-backup-to-this-backend
|
||||
```
|
||||
|
||||
#### Pruning and snapshot policies
|
||||
|
||||
Autorestic supports declaring snapshot policies for location to avoid keeping old snapshot around if you don't need them.
|
||||
|
||||
This is based on [Restic's snapshots policies](https://restic.readthedocs.io/en/latest/060_forget.html#removing-snapshots-according-to-a-policy), and can be enabled for each location as shown below:
|
||||
|
||||
```yaml
|
||||
locations:
|
||||
etc:
|
||||
from: /etc
|
||||
to: local
|
||||
options:
|
||||
forget:
|
||||
keep-last: 5 # always keep at least 5 snapshots
|
||||
keep-hourly: 3 # keep 3 last hourly shapshots
|
||||
keep-daily: 4 # keep 4 last daily shapshots
|
||||
keep-weekly: 1 # keep 1 last weekly shapshots
|
||||
keep-monthly: 12 # keep 12 last monthly shapshots
|
||||
keep-yearly: 7 # keep 7 last yearly shapshots
|
||||
keep-within: "2w" # keep snapshots from the last 2 weeks
|
||||
```
|
||||
|
||||
Pruning can be triggered using `autorestic forget -a`, for all locations, or selectively with `autorestic forget -l <location>`. **please note that contrary to the restic CLI, `restic forget` will call `restic prune` internally.**
|
||||
|
||||
Run with the `--dry-run` flag to only print information about the process without actually pruning the snapshots. This is especially useful for debugging or testing policies:
|
||||
```
|
||||
$ autorestic forget -a --dry-run --verbose
|
||||
|
||||
Configuring Backends
|
||||
local : Done ✓
|
||||
|
||||
Removing old shapshots according to policy
|
||||
etc ▶ local : Removing old spnapshots… ⏳
|
||||
etc ▶ local : Running in dry-run mode, not touching data
|
||||
etc ▶ local : Forgeting old snapshots… ⏳Applying Policy: all snapshots within 2d of the newest
|
||||
keep 3 snapshots:
|
||||
ID Time Host Tags Reasons Paths
|
||||
-----------------------------------------------------------------------------
|
||||
531b692a 2019-12-02 12:07:28 computer within 2w /etc
|
||||
51659674 2019-12-02 12:08:46 computer within 2w /etc
|
||||
f8f8f976 2019-12-02 12:11:08 computer within 2w /etc
|
||||
-----------------------------------------------------------------------------
|
||||
3 snapshots
|
||||
```
|
||||
|
||||
#### Excluding files/folders
|
||||
|
||||
If you want to exclude certain files or folders it done easily by specifiyng the right flags in the location you desire to filter. The flags are taken straight from the [restic cli exclude rules](https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files).
|
||||
|
||||
```yaml
|
||||
locations:
|
||||
my-location:
|
||||
from: /data
|
||||
to:
|
||||
- local
|
||||
- remote
|
||||
options:
|
||||
backup:
|
||||
exclude:
|
||||
- '*.nope'
|
||||
- '*.abc'
|
||||
exclude-file: .gitignore
|
||||
|
||||
backends:
|
||||
local:
|
||||
...
|
||||
remote:
|
||||
...
|
||||
```
|
||||
|
||||
#### Before / After hooks
|
||||
|
||||
Sometimes you might want to stop an app/db before backing up data and start the service again after the backup has completed. This is what the hooks are made for. Simply add them to your location config. You can have as many commands as you wish.
|
||||
|
||||
```yaml
|
||||
locations:
|
||||
my-location:
|
||||
from: /data
|
||||
to:
|
||||
- local
|
||||
- remote
|
||||
hooks:
|
||||
before:
|
||||
- echo "Hello"
|
||||
- echo "Human"
|
||||
after:
|
||||
- echo "kthxbye"
|
||||
```
|
||||
|
||||
## 💽 Backends
|
||||
|
||||
Backends are the place where you data will be saved. Backups are incremental and encrypted.
|
||||
|
||||
### Fields
|
||||
|
||||
##### `type`
|
||||
|
||||
Type of the backend see a list [here](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html)
|
||||
|
||||
Supported are:
|
||||
- [Local](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#local)
|
||||
- [Backblaze B2](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#backblaze-b2)
|
||||
- [Amazon S3](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#amazon-s3)
|
||||
- [Minio](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#minio-server)
|
||||
- [Google Cloud Storage](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#google-cloud-storage)
|
||||
- [Microsoft Azure Storage](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#microsoft-azure-blob-storage)
|
||||
|
||||
For each backend you need to specify the right variables as shown in the example below.
|
||||
|
||||
##### `path`
|
||||
|
||||
The path on the remote server.
|
||||
For object storages as
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
backends:
|
||||
name-of-backend:
|
||||
type: b2
|
||||
path: 'myAccount:myBucket/my/path'
|
||||
B2_ACCOUNT_ID: backblaze_account_id
|
||||
B2_ACCOUNT_KEY: backblaze_account_key
|
||||
```
|
||||
|
||||
## Contributors
|
||||
|
||||
This amazing people helped the project!
|
||||
|
||||
- @ChanceM [Docs]
|
||||
- @EliotBerriot [Docs, Pruning, S3]
|
||||
|
@@ -1,18 +0,0 @@
|
||||
locations:
|
||||
home:
|
||||
from: /home/myUser
|
||||
to: remote
|
||||
important:
|
||||
from: /path/to/important/stuff
|
||||
to:
|
||||
- remote
|
||||
- hdd
|
||||
backends:
|
||||
remote:
|
||||
type: b2
|
||||
path: 'myBucket:backup/home'
|
||||
B2_ACCOUNT_ID: account_id
|
||||
B2_ACCOUNT_KEY: account_key
|
||||
hdd:
|
||||
type: local
|
||||
path: /mnt/my_external_storage
|
BIN
docs/Sketch.png
Normal file
BIN
docs/Sketch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
OUT_FILE=/usr/local/bin/autorestic
|
||||
|
||||
|
@@ -4,15 +4,15 @@
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc -w",
|
||||
"dev": "tsnd --no-notify --respawn ./src/autorestic.ts",
|
||||
"bin": "npm run build && pkg lib/autorestic.js --out-path bin"
|
||||
"bin": "yarn run build && pkg lib/autorestic.js --targets latest-macos-x64,latest-linux-x64 --out-path bin"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/decompress": "^4.2.3",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/minimist": "^1.2.0",
|
||||
"@types/node": "^12.11.7",
|
||||
"pkg": "^4.4.0",
|
||||
"ts-node-dev": "^1.0.0-pre.40",
|
||||
"typescript": "^3.5.1"
|
||||
"typescript": "^3.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.0",
|
||||
|
@@ -1,11 +1,9 @@
|
||||
import 'colors'
|
||||
import minimist from 'minimist'
|
||||
import { homedir } from 'os'
|
||||
import { resolve } from 'path'
|
||||
|
||||
import { init } from './config'
|
||||
import handlers, { error, help } from './handlers'
|
||||
import { Config } from './types'
|
||||
|
||||
|
||||
|
||||
process.on('uncaughtException', err => {
|
||||
@@ -15,36 +13,32 @@ process.on('uncaughtException', err => {
|
||||
|
||||
export const { _: commands, ...flags } = minimist(process.argv.slice(2), {
|
||||
alias: {
|
||||
'c': 'config',
|
||||
'v': 'verbose',
|
||||
'h': 'help',
|
||||
'a': 'all',
|
||||
'l': 'location',
|
||||
'b': 'backend',
|
||||
c: 'config',
|
||||
v: 'version',
|
||||
h: 'help',
|
||||
a: 'all',
|
||||
l: 'location',
|
||||
b: 'backend',
|
||||
d: 'dry-run',
|
||||
},
|
||||
boolean: ['a'],
|
||||
boolean: ['a', 'd'],
|
||||
string: ['l', 'b'],
|
||||
})
|
||||
|
||||
export const VERSION = '0.4'
|
||||
export const DEFAULT_CONFIG = '/.autorestic.yml'
|
||||
export const VERSION = '0.9'
|
||||
export const INSTALL_DIR = '/usr/local/bin'
|
||||
export const CONFIG_FILE: string = resolve(flags.config || homedir() + DEFAULT_CONFIG)
|
||||
export const VERBOSE = flags.verbose
|
||||
|
||||
export const config: Config = init()
|
||||
export const config = init()
|
||||
|
||||
|
||||
function main() {
|
||||
if (flags.version)
|
||||
return console.log('version'.grey, VERSION)
|
||||
|
||||
if (commands.length < 1)
|
||||
return help()
|
||||
|
||||
if (commands.length < 1) return help()
|
||||
|
||||
const command: string = commands[0]
|
||||
const args: string[] = commands.slice(1)
|
||||
;(handlers[command] || error)(args, flags)
|
||||
}
|
||||
|
||||
|
||||
main()
|
@@ -1,12 +1,13 @@
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { config, VERBOSE } from './autorestic'
|
||||
import { Backend, Backends } from './types'
|
||||
import { exec } from './utils'
|
||||
import { Backend, Backends, Locations } from './types'
|
||||
import { exec, ConfigError } from './utils'
|
||||
|
||||
const ALREADY_EXISTS = /(?=.*exists)(?=.*already)(?=.*config).*/
|
||||
|
||||
|
||||
const ALREADY_EXISTS = /(?=.*already)(?=.*config).*/
|
||||
|
||||
export const getPathFromBackend = (backend: Backend): string => {
|
||||
switch (backend.type) {
|
||||
case 'local':
|
||||
@@ -24,7 +25,6 @@ export const getPathFromBackend = (backend: Backend): string => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const getEnvFromBackend = (backend: Backend) => {
|
||||
const { type, path, key, ...rest } = backend
|
||||
return {
|
||||
@@ -34,6 +34,12 @@ export const getEnvFromBackend = (backend: Backend) => {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export const checkAndConfigureBackend = (name: string, backend: Backend) => {
|
||||
const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳')
|
||||
@@ -49,8 +55,12 @@ export const checkAndConfigureBackend = (name: string, backend: Backend) => {
|
||||
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)
|
||||
|
@@ -3,35 +3,57 @@ 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, pathRelativeToConfigFile, getFlagsFromLocation, makeArrayIfIsNot, execPlain } from './utils'
|
||||
|
||||
|
||||
export const backupSingle = (name: string, from: string, to: string) => {
|
||||
|
||||
export const backupSingle = (name: string, to: string, location: Location) => {
|
||||
if (!config) throw ConfigError
|
||||
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
|
||||
|
||||
const backend = config.backends[to]
|
||||
const cmd = exec('restic', ['backup', from], { env: getEnvFromBackend(backend) })
|
||||
const path = pathRelativeToConfigFile(location.from)
|
||||
|
||||
const cmd = exec(
|
||||
'restic',
|
||||
['backup', path, ...getFlagsFromLocation(location, 'backup')],
|
||||
{ env: getEnvFromBackend(backend) },
|
||||
)
|
||||
|
||||
if (VERBOSE) console.log(cmd.out, cmd.err)
|
||||
writer.done(name + to.blue + ' : ' + 'Done ✓'.green)
|
||||
}
|
||||
|
||||
|
||||
export const backupLocation = (name: string, backup: Location) => {
|
||||
export const backupLocation = (name: string, location: Location) => {
|
||||
const display = name.yellow + ' ▶ '
|
||||
if (Array.isArray(backup.to)) {
|
||||
const filler = new Array(name.length + 3).fill(' ').join('')
|
||||
let first = true
|
||||
for (const t of backup.to) {
|
||||
const nameOrBlankSpaces: string = first ? display : new Array(name.length + 3).fill(' ').join('')
|
||||
backupSingle(nameOrBlankSpaces, backup.from, t)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
for (const t of makeArrayIfIsNot(location.to)) {
|
||||
backupSingle(first ? display : filler, t, location)
|
||||
if (first) first = false
|
||||
}
|
||||
} else
|
||||
backupSingle(display, backup.from, backup.to)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
export const backupAll = (locations?: Locations) => {
|
||||
if (!locations) {
|
||||
if (!config) throw ConfigError
|
||||
locations = config.locations
|
||||
}
|
||||
|
||||
export const backupAll = (backups: Locations = config.locations) => {
|
||||
console.log('\nBacking Up'.underline.grey)
|
||||
for (const [name, backup] of Object.entries(backups))
|
||||
backupLocation(name, backup)
|
||||
for (const [name, location] of Object.entries(locations))
|
||||
backupLocation(name, location)
|
||||
}
|
@@ -1,16 +1,23 @@
|
||||
import { readFileSync, writeFileSync } from 'fs'
|
||||
import { readFileSync, writeFileSync, statSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import yaml from 'js-yaml'
|
||||
import { CONFIG_FILE } from './autorestic'
|
||||
import { flags } from './autorestic'
|
||||
import { Backend, Config } from './types'
|
||||
import { makeObjectKeysLowercase, rand } from './utils'
|
||||
import { makeArrayIfIsNot, makeObjectKeysLowercase, rand } from './utils'
|
||||
import { homedir } from 'os'
|
||||
|
||||
|
||||
|
||||
export const normalizeAndCheckBackends = (config: Config) => {
|
||||
config.backends = makeObjectKeysLowercase(config.backends)
|
||||
|
||||
for (const [name, { type, path, key, ...rest }] of Object.entries(config.backends)) {
|
||||
|
||||
if (!type || !path) throw new Error(`The backend "${name}" is missing some required attributes`)
|
||||
for (const [name, { type, path, key, ...rest }] of Object.entries(
|
||||
config.backends,
|
||||
)) {
|
||||
if (!type || !path)
|
||||
throw new Error(
|
||||
`The backend "${name}" is missing some required attributes`,
|
||||
)
|
||||
|
||||
const tmp: any = {
|
||||
type,
|
||||
@@ -24,7 +31,6 @@ export const normalizeAndCheckBackends = (config: Config) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const normalizeAndCheckBackups = (config: Config) => {
|
||||
config.locations = makeObjectKeysLowercase(config.locations)
|
||||
const backends = Object.keys(config.backends)
|
||||
@@ -34,20 +40,45 @@ export const normalizeAndCheckBackups = (config: Config) => {
|
||||
throw new Error(`Cannot find the backend "${backend}" for "${backup}"`)
|
||||
}
|
||||
|
||||
for (const [name, { from, to, ...rest }] of Object.entries(config.locations)) {
|
||||
if (!from || !to) throw new Error(`The backup "${name}" is missing some required attributes`)
|
||||
for (const [name, { from, to, ...rest }] of Object.entries(
|
||||
config.locations,
|
||||
)) {
|
||||
if (!from || !to)
|
||||
throw new Error(
|
||||
`The backup "${name}" is missing some required attributes`,
|
||||
)
|
||||
|
||||
if (Array.isArray(to))
|
||||
for (const t of to)
|
||||
for (const t of makeArrayIfIsNot(to))
|
||||
checkDestination(t, name)
|
||||
else
|
||||
checkDestination(to, name)
|
||||
}
|
||||
}
|
||||
|
||||
const findConfigFile = (): string | undefined => {
|
||||
const config = '.autorestic.yml'
|
||||
const paths = [
|
||||
resolve(flags.config || ''),
|
||||
resolve('./' + config),
|
||||
homedir() + '/' + config,
|
||||
]
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const file = statSync(path)
|
||||
if (file.isFile()) return path
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const init = (): Config => {
|
||||
const raw: Config = makeObjectKeysLowercase(yaml.safeLoad(readFileSync(CONFIG_FILE).toString()))
|
||||
export let CONFIG_FILE: string = ''
|
||||
|
||||
export const init = (): Config | undefined => {
|
||||
const file = findConfigFile()
|
||||
if (file) CONFIG_FILE = file
|
||||
else return
|
||||
|
||||
const raw: Config = makeObjectKeysLowercase(
|
||||
yaml.safeLoad(readFileSync(CONFIG_FILE).toString()),
|
||||
)
|
||||
|
||||
normalizeAndCheckBackends(raw)
|
||||
normalizeAndCheckBackups(raw)
|
||||
|
60
src/forget.ts
Normal file
60
src/forget.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { config, VERBOSE } from './autorestic'
|
||||
import { getEnvFromBackend } from './backend'
|
||||
import { Locations, Location, Flags } from './types'
|
||||
import { exec, ConfigError, pathRelativeToConfigFile, getFlagsFromLocation, makeArrayIfIsNot } from './utils'
|
||||
|
||||
|
||||
|
||||
export const forgetSingle = (name: string, to: string, location: Location, dryRun: boolean) => {
|
||||
if (!config) throw ConfigError
|
||||
const base = name + to.blue + ' : '
|
||||
const writer = new Writer(base + 'Removing old snapshots… ⏳')
|
||||
|
||||
const backend = config.backends[to]
|
||||
const path = pathRelativeToConfigFile(location.from)
|
||||
const flags = getFlagsFromLocation(location, 'forget')
|
||||
|
||||
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 = new Array(name.length + 3).fill(' ').join('')
|
||||
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 (!config) throw ConfigError
|
||||
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)
|
||||
|
||||
for (const [name, backup] of Object.entries(backups))
|
||||
forgetLocation(name, backup, dryRun)
|
||||
}
|
157
src/handlers.ts
157
src/handlers.ts
@@ -4,9 +4,10 @@ import { unlinkSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join, resolve } from 'path'
|
||||
|
||||
import { config, CONFIG_FILE, INSTALL_DIR, VERSION } from './autorestic'
|
||||
import { checkAndConfigureBackends, getEnvFromBackend } from './backend'
|
||||
import { config, INSTALL_DIR, VERSION } from './autorestic'
|
||||
import { checkAndConfigureBackends, getBackendsFromLocations, getEnvFromBackend } from './backend'
|
||||
import { backupAll } from './backup'
|
||||
import { forgetAll } from './forget'
|
||||
import { Backends, Flags, Locations } from './types'
|
||||
import {
|
||||
checkIfCommandIsAvailable,
|
||||
@@ -14,21 +15,26 @@ import {
|
||||
downloadFile,
|
||||
exec,
|
||||
filterObjectByKey,
|
||||
singleToArray,
|
||||
ConfigError, makeArrayIfIsNot,
|
||||
} from './utils'
|
||||
|
||||
export type Handlers = { [command: string]: (args: string[], flags: Flags) => void }
|
||||
|
||||
|
||||
export type Handlers = {
|
||||
[command: string]: (args: string[], flags: Flags) => void
|
||||
}
|
||||
|
||||
const parseBackend = (flags: Flags): Backends => {
|
||||
if (!config) throw ConfigError
|
||||
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',
|
||||
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
|
||||
if (flags.all) return config.backends
|
||||
else {
|
||||
const backends = singleToArray<string>(flags.backend)
|
||||
const backends = makeArrayIfIsNot<string>(flags.backend)
|
||||
for (const backend of backends)
|
||||
if (!config.backends[backend])
|
||||
throw new Error('Invalid backend: '.red + backend)
|
||||
@@ -37,16 +43,18 @@ 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
|
||||
+ '\n--all [-a]\t\t\t\tBackup all.'
|
||||
+ '\n--location [-l] site1\t\t\tSpecify one or more locations',
|
||||
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 = singleToArray<string>(flags.location)
|
||||
const locations = makeArrayIfIsNot<string>(flags.location)
|
||||
for (const location of locations)
|
||||
if (!config.locations[location])
|
||||
throw new Error('Invalid location: '.red + location)
|
||||
@@ -61,33 +69,64 @@ const handlers: Handlers = {
|
||||
checkAndConfigureBackends(backends)
|
||||
},
|
||||
backup(args, flags) {
|
||||
if (!config) throw ConfigError
|
||||
checkIfResticIsAvailable()
|
||||
const locations: Locations = parseLocations(flags)
|
||||
|
||||
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)
|
||||
|
||||
checkAndConfigureBackends(filterObjectByKey(config.backends, Array.from(backends)))
|
||||
checkAndConfigureBackends(
|
||||
filterObjectByKey(config.backends, getBackendsFromLocations(locations)),
|
||||
)
|
||||
backupAll(locations)
|
||||
|
||||
console.log('\nFinished!'.underline + ' 🎉')
|
||||
},
|
||||
restore(args, flags) {
|
||||
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 w = new Writer(name.green + `\t\tRestoring... ⏳`)
|
||||
const env = getEnvFromBackend(config.backends[Array.isArray(location.to) ? location.to[0] : location.to])
|
||||
const baseText = name.green + '\t\t'
|
||||
const w = new Writer(baseText + `Starting...`)
|
||||
|
||||
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), ...args],
|
||||
['restore', 'latest', '--path', resolve(location.from), '--target', flags.to],
|
||||
{ env },
|
||||
)
|
||||
w.done(name.green + '\t\tDone 🎉')
|
||||
}
|
||||
},
|
||||
forget(args, flags) {
|
||||
if (!config) throw ConfigError
|
||||
checkIfResticIsAvailable()
|
||||
const locations: Locations = parseLocations(flags)
|
||||
|
||||
checkAndConfigureBackends(
|
||||
filterObjectByKey(config.backends, getBackendsFromLocations(locations)),
|
||||
)
|
||||
forgetAll(locations, flags)
|
||||
|
||||
console.log('\nFinished!'.underline + ' 🎉')
|
||||
},
|
||||
exec(args, flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const backends = parseBackend(flags)
|
||||
@@ -116,14 +155,17 @@ const handlers: Handlers = {
|
||||
})
|
||||
|
||||
const archMap: { [a: string]: string } = {
|
||||
'x32': '386',
|
||||
'x64': 'amd64',
|
||||
x32: '386',
|
||||
x64: 'amd64',
|
||||
}
|
||||
|
||||
w.replaceLn('Downloading binary... 🌎')
|
||||
const name = `${json.name.replace(' ', '_')}_${process.platform}_${archMap[process.arch]}.bz2`
|
||||
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(
|
||||
if (!dl)
|
||||
return console.log(
|
||||
'Cannot get the right binary.'.red,
|
||||
'Please see https://bit.ly/2Y1Rzai',
|
||||
)
|
||||
@@ -133,8 +175,6 @@ const handlers: Handlers = {
|
||||
|
||||
await downloadFile(dl.browser_download_url, tmp)
|
||||
|
||||
// TODO: Native bz2
|
||||
// Decompress
|
||||
w.replaceLn('Decompressing binary... 📦')
|
||||
exec('bzip2', ['-dk', tmp])
|
||||
unlinkSync(tmp)
|
||||
@@ -143,7 +183,9 @@ const handlers: Handlers = {
|
||||
exec('chmod', ['+x', extracted])
|
||||
exec('mv', [extracted, INSTALL_DIR + '/restic'])
|
||||
|
||||
w.done(`\nFinished! restic is installed under: ${INSTALL_DIR}`.underline + ' 🎉')
|
||||
w.done(
|
||||
`\nFinished! restic is installed under: ${INSTALL_DIR}`.underline + ' 🎉',
|
||||
)
|
||||
},
|
||||
uninstall() {
|
||||
for (const bin of ['restic', 'autorestic'])
|
||||
@@ -159,17 +201,17 @@ const handlers: Handlers = {
|
||||
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',
|
||||
url:
|
||||
'https://api.github.com/repos/cupcakearmy/autorestic/releases/latest',
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
if (json.tag_name != VERSION) {
|
||||
const platformMap: { [key: string]: string } = {
|
||||
'darwin': 'macos',
|
||||
darwin: 'macos',
|
||||
}
|
||||
|
||||
const name = `autorestic_${platformMap[process.platform] || process.platform}_${process.arch}`
|
||||
@@ -184,33 +226,44 @@ const handlers: Handlers = {
|
||||
|
||||
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: ${CONFIG_FILE}`
|
||||
+ '\n'
|
||||
+ '\nCommands:'.yellow
|
||||
+ '\n check [-b, --backend] [-a, --all] Check backends'
|
||||
+ '\n backup [-l, --location] [-a, --all] Backup all or specified locations'
|
||||
+ '\n restore [-l, --location] [-- --target <out dir>] Check backends'
|
||||
+ '\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/fjVbg'
|
||||
+ '\n',
|
||||
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 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/fjVbg' +
|
||||
'\n',
|
||||
)
|
||||
}
|
||||
|
||||
export const error = () => {
|
||||
help()
|
||||
console.log(`Invalid Command:`.red.underline, `${process.argv.slice(2).join(' ')}`)
|
||||
console.log(
|
||||
`Invalid Command:`.red.underline,
|
||||
`${process.argv.slice(2).join(' ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
export default handlers
|
84
src/types.ts
84
src/types.ts
@@ -1,67 +1,91 @@
|
||||
export type StringOrArray = string | string[]
|
||||
|
||||
// BACKENDS
|
||||
|
||||
type BackendLocal = {
|
||||
type: 'local',
|
||||
key: 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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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 }
|
||||
|
||||
// LOCATIONS
|
||||
|
||||
export type Location = {
|
||||
from: string,
|
||||
to: string | string[]
|
||||
from: string
|
||||
to: StringOrArray
|
||||
hooks?: {
|
||||
before?: StringOrArray
|
||||
after?: StringOrArray
|
||||
}
|
||||
options?: {
|
||||
[key: string]: {
|
||||
[key: string]: StringOrArray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Locations = { [name: string]: Location }
|
||||
|
||||
// OTHER
|
||||
|
||||
export type Config = {
|
||||
locations: Locations
|
||||
backends: Backends
|
||||
|
70
src/utils.ts
70
src/utils.ts
@@ -2,9 +2,17 @@ import axios from 'axios'
|
||||
import { spawnSync, SpawnSyncOptions } from 'child_process'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { createWriteStream } from 'fs'
|
||||
import { dirname, isAbsolute, resolve } from 'path'
|
||||
import { CONFIG_FILE } from './config'
|
||||
import { Location } from './types'
|
||||
|
||||
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: {
|
||||
@@ -19,9 +27,21 @@ export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyn
|
||||
return { out, err }
|
||||
}
|
||||
|
||||
export const checkIfResticIsAvailable = () => checkIfCommandIsAvailable(
|
||||
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
|
||||
}
|
||||
|
||||
return exec(split[0], split.slice(1), opt)
|
||||
}
|
||||
|
||||
export const checkIfResticIsAvailable = () =>
|
||||
checkIfCommandIsAvailable(
|
||||
'restic',
|
||||
'Restic is not installed'.red + ' https://restic.readthedocs.io/en/latest/020_installation.html#stable-releases',
|
||||
'Restic is not installed'.red +
|
||||
' https://restic.readthedocs.io/en/latest/020_installation.html#stable-releases',
|
||||
)
|
||||
|
||||
export const checkIfCommandIsAvailable = (cmd: string, errorMsg?: string) => {
|
||||
@@ -31,8 +51,7 @@ export const checkIfCommandIsAvailable = (cmd: string, errorMsg?: string) => {
|
||||
|
||||
export const makeObjectKeysLowercase = (object: Object): any =>
|
||||
Object.fromEntries(
|
||||
Object.entries(object)
|
||||
.map(([key, value]) => [key.toLowerCase(), value]),
|
||||
Object.entries(object).map(([key, value]) => [key.toLowerCase(), value]),
|
||||
)
|
||||
|
||||
|
||||
@@ -40,13 +59,20 @@ export function rand(length = 32): string {
|
||||
return randomBytes(length / 2).toString('hex')
|
||||
}
|
||||
|
||||
export const singleToArray = <T>(singleOrArray: T | T[]): T[] => Array.isArray(singleOrArray) ? singleOrArray : [singleOrArray]
|
||||
|
||||
export const filterObject = <T>(obj: { [key: string]: T }, filter: (item: [string, T]) => boolean): { [key: string]: T } => Object.fromEntries(Object.entries(obj).filter(filter))
|
||||
export const filterObject = <T>(
|
||||
obj: { [key: string]: T },
|
||||
filter: (item: [string, T]) => boolean,
|
||||
): { [key: string]: T } =>
|
||||
Object.fromEntries(Object.entries(obj).filter(filter))
|
||||
|
||||
export const filterObjectByKey = <T>(obj: { [key: string]: T }, keys: string[]) => filterObject(obj, ([key]) => keys.includes(key))
|
||||
export const filterObjectByKey = <T>(
|
||||
obj: { [key: string]: T },
|
||||
keys: string[],
|
||||
) => filterObject(obj, ([key]) => keys.includes(key))
|
||||
|
||||
export const downloadFile = async (url: string, to: string) => new Promise<void>(async res => {
|
||||
export const downloadFile = async (url: string, to: string) =>
|
||||
new Promise<void>(async res => {
|
||||
const { data: file } = await axios({
|
||||
method: 'get',
|
||||
url: url,
|
||||
@@ -61,3 +87,29 @@ export const downloadFile = async (url: string, to: string) => new Promise<void>
|
||||
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 ConfigError = new Error('Config file not found')
|
||||
|
||||
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))
|
||||
flags = [...flags, `--${String(flag)}`, String(value)]
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
export const makeArrayIfIsNot = <T>(maybeArray: T | T[]): T[] => Array.isArray(maybeArray) ? maybeArray : [maybeArray]
|
||||
|
Reference in New Issue
Block a user