Compare commits

..

219 Commits
0.13 ... v1.1.0

Author SHA1 Message Date
478e193d78 forgot version bump 2021-05-06 16:17:45 +02:00
11d4c67dce docs 2021-05-06 16:14:29 +02:00
1643309957 changelog 2021-05-06 15:57:28 +02:00
e05386b0b5 add failure and success hooks 2021-05-06 15:55:32 +02:00
aebaf0a225 Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2021-05-06 15:12:37 +02:00
c090013bf5 Update README.md 2021-05-06 15:12:11 +02:00
88c6949208 custom restic binary 2021-05-06 15:04:35 +02:00
9256cdc38c changelog 2021-05-01 22:57:01 +02:00
a8c611e8ce Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2021-05-01 22:54:57 +02:00
d4522c7ffe fix validation for docker volumes 2021-05-01 22:54:52 +02:00
21185d894e Update contrib.md 2021-05-01 17:25:50 +02:00
b119fc7ea5 Merge pull request #72 from themorlan/master
Correcting some typos in the Documentation
2021-05-01 17:25:20 +02:00
themorlan
73f4bcfd48 missing words in sentence 2021-05-01 15:18:05 +02:00
themorlan
3567319314 typo 2021-05-01 15:16:13 +02:00
themorlan
bf19c983d1 typo 2021-05-01 15:15:44 +02:00
themorlan
12e4de110b typo 2021-05-01 15:14:50 +02:00
c9f425ef64 docs on native flags 2021-04-30 10:47:29 +02:00
974f555dff Update contrib.md 2021-04-29 15:18:03 +02:00
1688c1f3c3 add brew docs 2021-04-29 15:16:33 +02:00
29e46d3b5c brew stuff 2021-04-29 14:45:17 +02:00
0335abb669 add sign 2021-04-28 14:51:56 +02:00
b2f9b9a54e typo 2021-04-28 13:50:11 +02:00
2b13a6e13d installtion docs 2021-04-28 13:44:20 +02:00
cc293ea256 Update installation.md 2021-04-28 13:38:20 +02:00
a4ddd5bbcb Update contrib.md 2021-04-28 13:34:58 +02:00
7cbf43b75d Update README.md 2021-04-28 11:13:16 +02:00
bae77c4673 Update README.md 2021-04-28 11:10:36 +02:00
12adeb2b06 changelog 2021-04-28 10:54:33 +02:00
37b26dfc31 consistent casing 2021-04-28 10:54:30 +02:00
c1795b2acc lean flag 2021-04-28 10:54:07 +02:00
b8d12e518c completion docs 2021-04-28 10:34:43 +02:00
50060cf539 explain verbose flag 2021-04-27 18:38:51 +02:00
c33aac42dc quickstart 2021-04-27 18:35:32 +02:00
c359053e0e Update contrib.md 2021-04-27 09:02:39 +02:00
c16340ab26 Update available.md 2021-04-27 09:01:01 +02:00
edc85c4ac3 Merge pull request #60 from david-boles/backblaze
Update Backblaze docs
2021-04-27 08:59:44 +02:00
68682777f2 Merge pull request #59 from david-boles/consistent-config-location
Recommend a consistent config location
2021-04-27 08:26:22 +02:00
David Boles
b6c7922df5 Recommend a consistent config location 2021-04-26 16:47:00 -07:00
David Boles
991b8bec22 Update backblaze docs 2021-04-26 16:38:08 -07:00
bbc32568ad parallel builds 2021-04-26 13:29:26 +02:00
f3c038c716 changelog & version bump 2021-04-26 13:18:02 +02:00
59612a97b6 Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2021-04-26 13:16:01 +02:00
33319a00ef add arm for darwin 2021-04-26 13:15:58 +02:00
8eb14ea14f Update quick.md 2021-04-25 11:19:39 +02:00
70eb9e441f add rclone support 2021-04-24 16:40:53 +02:00
be25af2d76 Merge pull request #53 from TheForcer/master
Fix typo in check output
2021-04-24 13:19:34 +02:00
1c436369f0 check folder 2021-04-24 12:40:33 +02:00
6efcce07b7 show error and exit with bad code if not found 2021-04-23 23:53:57 +02:00
Felix
dc6dd2e712 Fix typo in check output 2021-04-23 19:19:03 +02:00
Felix
68628d3776 Merge pull request #1 from cupcakearmy/master
Update
2021-04-23 19:18:08 +02:00
40988ef3b4 Update contrib.md 2021-04-23 17:53:48 +02:00
fad33fcdaa Merge pull request #51 from TheForcer/master
Fix typos in docs
2021-04-23 17:53:15 +02:00
Felix
8cf8a77558 Fix typos in docs 2021-04-23 17:45:02 +02:00
36998cfd3b changelog 2021-04-23 13:47:21 +02:00
cf03562ea2 docs 2021-04-23 13:45:54 +02:00
188560395d exclude items from config when empty and rest options 2021-04-23 13:11:15 +02:00
bacbd0f806 docs 2021-04-23 12:31:24 +02:00
93bf0388a4 add force flag description 2021-04-22 10:00:52 +02:00
ec8fdbd135 updated contrib list 2021-04-22 01:14:47 +02:00
420934489c Merge pull request #46 from FuzzyMistborn/patch-1
Fix typo in install script
2021-04-22 01:07:11 +02:00
Fuzzy
2ba767c8c3 Fix typo in install script
Fix typo
2021-04-21 14:32:11 -04:00
b489c662c7 docs 2021-04-21 09:41:14 +02:00
6862529a89 move envs to new format 2021-04-21 09:36:47 +02:00
aa96a95600 typos 2021-04-21 09:34:01 +02:00
89e32c298c update the quick guide 2021-04-21 09:27:35 +02:00
873170c6d1 fix auto update 2021-04-20 23:26:57 +02:00
ea82fea8e1 add cron tag 2021-04-20 20:49:09 +02:00
a35edcaea5 version bump 2021-04-17 20:55:09 +02:00
86d44eafad exit with better message 2021-04-17 20:51:40 +02:00
e927fd5a64 panic on already running 2021-04-17 20:45:52 +02:00
d5e13d4e27 changelog 2021-04-17 17:54:24 +02:00
824c90904c docs 2021-04-17 17:51:23 +02:00
58fb5e073a Update README.md 2021-04-17 17:50:53 +02:00
541a7c2a72 add autocompletetions 2021-04-17 17:48:31 +02:00
e671780e54 fix version upgrade 2021-04-17 17:41:53 +02:00
a5471efa21 check self update 2021-04-17 17:32:05 +02:00
769ae4c566 Merge pull request #44 from cupcakearmy/rewrite
Rewrite
2021-04-17 17:21:19 +02:00
f2934f60ae docs 2021-04-17 17:19:59 +02:00
f6bc91e9b5 merge 2021-04-17 17:16:58 +02:00
fb073627a8 Merge remote-tracking branch 'origin/master' into rewrite 2021-04-17 17:16:52 +02:00
976a3beab5 upgrade logic 2021-04-17 17:13:03 +02:00
ec2377287e docs 2021-04-17 17:04:43 +02:00
7b7eec2dba install script 2021-04-17 15:57:15 +02:00
33be94ba55 licensing 2021-04-17 15:12:44 +02:00
8f7513b103 v30 2021-04-17 14:49:29 +02:00
cf417e360d roadmap 2021-04-17 13:13:17 +02:00
b5caad2d70 docs 2021-04-17 13:12:25 +02:00
2e594cade6 docs 2021-04-17 13:12:20 +02:00
276c424106 pass env of backends to restic 2021-04-17 13:04:40 +02:00
b314912821 describe backends 2021-04-17 12:02:00 +02:00
b1054f3512 info command 2021-04-16 22:37:15 +02:00
1bbd3879fe make back compatible 2021-04-16 22:02:25 +02:00
aa0b81023f vercel 2021-04-16 00:31:34 +02:00
09e0ee4dec shorthand for config 2021-04-16 00:27:06 +02:00
952334fecb docs 2021-04-16 00:26:57 +02:00
a45eee6009 only when tagged 2021-04-15 23:36:01 +02:00
a658b6bb21 workflow 2021-04-15 23:35:00 +02:00
8e13e7bc40 linting 2021-04-15 23:16:47 +02:00
6449b8999f support for volume locations 2021-04-15 21:55:35 +02:00
7629626ae0 comment on major version 2021-04-15 01:08:56 +02:00
190eca6f6e check for mahor version for updates 2021-04-15 00:57:15 +02:00
da6d9c53aa docs 2021-04-12 16:41:46 +02:00
604354741e remove tmp file 2021-04-12 16:16:08 +02:00
3ccaee4066 cleaner output and ctrl-c 2021-04-12 16:15:17 +02:00
640b60c47f current state 2021-04-12 10:55:57 +02:00
d293e93fa8 cron job 2021-04-12 00:17:29 +02:00
03ca0c8677 add key if not present 2021-04-12 00:02:35 +02:00
19e75c1dad fixes 2021-04-11 18:17:21 +02:00
6e25b90915 unlock on error and named arrays for config 2021-04-11 17:02:34 +02:00
8a1fe41825 cron 2021-04-11 15:02:27 +02:00
5d92b5bcc1 dry run for forget cmd 2021-04-11 14:22:46 +02:00
05be58a3a7 restore cmd 2021-04-11 14:03:38 +02:00
9ba58ee7f8 decsriptions 2021-04-11 13:08:50 +02:00
335724cce7 progress 2021-04-11 13:04:11 +02:00
43244302be build file 2021-04-11 13:03:47 +02:00
03cbbfd91c go rewrite 2021-04-09 01:55:10 +02:00
805bed7db1 design 2021-04-08 21:05:34 +02:00
6c59aa25db lfs 2021-04-08 21:05:30 +02:00
ff648f0017 don't make autorestic return 1 on call 2021-01-24 10:27:37 +01:00
c79b45308b Create config.yml 2021-01-10 11:27:41 +01:00
43eabdb204 Update issue templates 2021-01-10 11:25:48 +01:00
0ead9e0da1 remove version from package 2020-12-19 17:32:13 +01:00
d49e0d3836 Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2020-12-09 00:07:45 +01:00
2008ba2771 changelog 2020-12-09 00:07:43 +01:00
1f6c13a595 fix locking issue 2020-12-09 00:07:03 +01:00
8e9b9dcebf Update README.md 2020-12-08 23:46:06 +01:00
fde4edc05f use version from package, stricter compiler 2020-11-29 15:37:39 +01:00
7e6cc7bb32 lock only if config required 2020-11-29 15:27:32 +01:00
878a7bd752 disable colors in ci mode 2020-11-16 17:30:32 +01:00
f6860115a3 remove ununsed log 2020-11-14 00:40:16 +01:00
f43e73ce41 process end in exit 2020-11-14 00:33:52 +01:00
6b4277b57b changelog 2020-11-13 21:47:33 +01:00
d4b8a7223f don't require config for update 2020-11-13 16:27:19 +01:00
cfcc010bc5 update some pkgs 2020-11-13 15:48:48 +01:00
1fd009b819 add ci mode 2020-11-13 15:48:40 +01:00
91e902d7ef docs 2020-11-13 15:48:20 +01:00
b690c1c3a9 correct link 2020-11-07 12:55:16 +01:00
b025aeeeab version bump 2020-11-07 12:50:05 +01:00
01cafb9436 Update README.md 2020-11-07 12:47:06 +01:00
6a2c34df48 vercel 2020-11-07 12:41:51 +01:00
88e47efc68 Merge pull request #30 from cupcakearmy/rewrite
Rewrite with commander & docs cleanup
2020-11-06 23:51:57 +01:00
60d7e0b561 rewrite with commander 2020-11-06 23:51:23 +01:00
e9a7e03af7 docs 2020-11-06 22:48:19 +01:00
d959a50b5a docs 2020-11-06 22:42:00 +01:00
b5bc1a4cee Merge pull request #27 from sumnerboy12/patch-1
fix typo on skipping/scheduled log message
2020-07-24 10:38:07 +02:00
Ben Jones
e61c940d23 fix typo on skipping/scheduled log message 2020-07-24 11:53:27 +12:00
7fb0fbd467 new version 2020-07-23 13:34:23 +02:00
cbc1b33750 docs 2020-07-23 13:33:14 +02:00
e7be01da37 Merge pull request #26 from sumnerboy12/patch-1
add support for rest server backends
2020-07-23 13:29:20 +02:00
Ben Jones
309073fe4d Update available.md 2020-07-23 23:26:31 +12:00
Ben Jones
1243721a7e add support for rest server backends
Backend configuration would be;

```
rest_repo:
  type: rest
  path: http://backup:8001/repo_name
```

Would result in the following env var;
```
RESTIC_REPOSITORY=rest:http://backup:8001/repo_name
```
2020-07-23 15:21:55 +12:00
ebb934d1c5 docs 2020-06-28 22:48:00 +02:00
8417a6e0aa update docs version 2020-06-28 22:47:46 +02:00
2b5efd0499 cron docs 2020-06-28 22:36:44 +02:00
af6e715b27 generate docs 2020-06-28 22:33:22 +02:00
9484299cd9 cron working now 2020-06-28 22:33:15 +02:00
ef93fef36a help for releasing 2020-06-25 09:36:17 +02:00
995f2250a1 version bump 2020-06-25 09:36:11 +02:00
3498e6da7b fix check for executable 2020-06-25 09:33:08 +02:00
7128983581 lock file 2020-06-25 09:21:33 +02:00
158407927a Merge branch 'master' of https://github.com/cupcakearmy/autorestic 2020-06-25 09:17:23 +02:00
ea8bf83799 always unlock 2020-06-25 09:17:20 +02:00
e8a8ccccd0 use longer wrap length 2020-06-25 09:17:02 +02:00
b332897713 upgrade packages, included lock file, fixed types 2020-06-25 09:16:53 +02:00
369037ea77 Merge pull request #23 from jin-park-dev/feature/spell-fix
Feature/spell fix
2020-06-25 08:43:59 +02:00
Jin Park
52cd6fe3fd Fix e to E 2020-06-25 01:40:04 +01:00
Jin Park
b6f7ef6577 Minor spell fixes in doc 2020-06-25 01:23:31 +01:00
1a891fffbd funding 2020-05-26 10:31:59 +02:00
ed68160a9a Update README.md 2020-05-20 19:31:59 +02:00
20ecd21928 Update README.md 2020-05-20 19:30:48 +02:00
df6fd75ca4 Update README.md 2020-05-20 19:30:31 +02:00
c918fb6ded Update README.md 2020-05-20 19:30:05 +02:00
2bc8fe4ee7 logo 2020-05-20 19:26:13 +02:00
757a134362 default open 2020-05-17 19:04:44 +02:00
6bb36cbde0 docs 2020-05-17 18:56:14 +02:00
5c0b67b7fb version bump 2020-05-17 18:14:09 +02:00
1520e10a59 do not allow multiple instances 2020-05-17 18:13:50 +02:00
f9c645120b run in shell to set paths and find restic 2020-05-17 18:12:58 +02:00
8fd0240929 changelog 2020-05-17 15:42:51 +02:00
da7b70c7a7 rebuild links 2020-05-17 15:37:38 +02:00
c138ac9882 Update README.md 2020-05-17 15:33:07 +02:00
af04f1cded Update README.md 2020-05-17 15:32:47 +02:00
3a43b2b113 Update README.md 2020-05-17 15:31:54 +02:00
885c7ca778 finished docs 2020-05-17 15:30:23 +02:00
44a48aab9b Update README.md 2020-05-17 15:14:00 +02:00
f5f550f5b1 fix paths 2020-05-17 15:11:09 +02:00
f1d5a6b5dd docs build 2020-05-17 14:52:35 +02:00
734c962006 docs 2020-05-17 14:52:30 +02:00
d20a2fda68 move image 2020-05-17 14:52:08 +02:00
8dcd0b1f12 add cron to format 2020-05-17 14:48:45 +02:00
9acb6296e4 add cron command 2020-05-17 11:13:02 +02:00
82f6942ff1 remove async 2020-05-17 09:35:18 +02:00
22f5f61ee0 check if cron is valid & give better feedback on init. also make sync 2020-05-17 09:35:07 +02:00
ddce8bf8a7 add cron parser 2020-05-17 09:34:35 +02:00
abeaacf182 check config in the beginning to avoid doing it all over the place 2020-05-17 09:11:33 +02:00
db436587ee version updates 2020-05-17 09:09:57 +02:00
32837e7e45 added extension 2020-05-17 09:09:45 +02:00
500abfbd27 Update README.md 2020-05-15 12:41:06 +02:00
4ee54110a6 Update README.md 2020-03-03 16:17:57 +01:00
92e5071343 Update README.md 2020-03-03 16:07:55 +01:00
7e577c439a notify user if config file was overwritten and make a copy of it as backup 2020-03-02 19:08:20 +01:00
bc36a39de4 Update README.md 2020-03-02 19:01:39 +01:00
9e6b393e62 Update README.md 2020-03-02 18:59:40 +01:00
de34396b93 Update README.md 2020-02-26 12:18:04 +01:00
ebbe10608a Update README.md 2020-02-26 12:13:46 +01:00
8a34270934 Update README.md 2020-02-26 12:12:40 +01:00
e459e393a9 Update README.md 2020-01-23 11:27:00 +01:00
b1a3074f33 changelog 2020-01-23 11:20:44 +01:00
ae63d8b12e protected drone file 2020-01-23 11:19:16 +01:00
7aa937dd41 automatic signing 2020-01-23 11:09:57 +01:00
cupcakearmy
37361727ba Merge remote-tracking branch 'origin/master' 2020-01-08 00:48:13 +01:00
f1874438e5 Update README.md 2020-01-08 00:46:36 +01:00
cupcakearmy
066342a7b7 changelog 2020-01-08 00:45:39 +01:00
cupcakearmy
f620bb1764 version bump and help command in addition to flag 2020-01-08 00:34:36 +01:00
cupcakearmy
e3506e44b5 enable sftp 2020-01-08 00:32:33 +01:00
cupcakearmy
f65a83991b Merge remote-tracking branch 'origin/master' 2020-01-08 00:30:16 +01:00
f10b8c7990 Update README.md 2020-01-08 00:29:12 +01:00
cupcakearmy
a8af085d9c dont' get stuck if backend is not supported 2020-01-08 00:22:49 +01:00
fa89d2941f Update README.md 2019-12-24 19:05:26 +01:00
100 changed files with 16105 additions and 1495 deletions

31
.drone.yml Normal file
View File

@@ -0,0 +1,31 @@
---
kind: pipeline
name: default
steps:
- name: build
image: node
pull: always
commands:
- yarn
- yarn run bin
when:
event: tag
- name: publish
image: plugins/github-release
pull: always
settings:
api_key:
from_secret: github
files: bin/*
checksum:
- sha512
note: CHANGELOG.md
when:
event: tag
---
kind: signature
hmac: 3b1f235f6a6f0ee1aa3f572d0833c4f0eec931dbe0378f31b9efa336a7462912
...

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.afdesign filter=lfs diff=lfs merge=lfs -text

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: cupcakearmy

21
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,21 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Environment**
- OS: [e.g. iOS]
- Version: [e.g. 22]
**Additional context**
<!-- Add any other context about the problem here. -->

4
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
contact_links:
- name: Questions & Help
url: https://github.com/cupcakearmy/autorestic/discussions
about: Please ask and answer questions here.

View File

@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->

BIN
.github/logo.afdesign (Stored with Git LFS) vendored Normal file

Binary file not shown.

BIN
.github/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

28
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Main
on:
push:
tags:
- 'v*.*.*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.16.3'
- name: Build
run: go run build/build.go
- name: Sign
uses: tristan-weil/ghaction-checksum-sign-artifact@v1.0.1
with:
path: dist/*
- name: Release
uses: softprops/action-gh-release@v1
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

23
.gitignore vendored
View File

@@ -1,19 +1,12 @@
# Editors
.idea
# Node
node_modules/
package-lock.json
yarn.lock
# Build & Runtime
bin
lib
data
restore
docker
Dockerfile
.vscode
# Config
.autorestic.yml
.docker.yml
.autorestic*
# Build & Dev
test
autorestic
data
dist

View File

@@ -1,3 +0,0 @@
semi: false
singleQuote: true
trailingComma: 'es5'

View File

@@ -1,53 +1,89 @@
# Changelog
## 0.13
All notable changes to this project will be documented in this file.
- Restored files are now without the prefix path.
- Support for making backups of docker volumes and restoring them (not incremental).
- Show error to user during backup
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.12
## [1.1.0] - 2021-05-06
- fix self update on linux (Fix #15)
### Added
## 0.11
- use custom restic binary
- success & failure hooks
- tilde in arguments (Fix #14)
### Fixed
## 0.10
- don't skip other locations on failure
- Show elapsed time (Fix #12)
- Remove some code duplication
- New info command to quickly show an overview of your config (Fix #11)
## [1.0.9] - 2021-05-01
## 0.9
### Fixed
- Hooks
- Cleanup
- Validation for docker volumes
## 0.8
## [1.0.8] - 2021-04-28
- Support for native flags in the backup and forget commands.
- Forget cleanup
### Added
## 0.7
- `--lean` flag to cron command for less output about skipping backups.
- Cleanup
- Support for excluding files
- Ability to prune keeping the last x snapshots according to restic policy rules
### Fixed
## 0.6
- consistent lower casing in usage descriptions.
- support for absolute paths
## [1.0.7] - 2021-04-26
## 0.5
### Added
- config optional if not required for current operation
- Support for `darwin/arm64` aka Apple Silicon.
- Added support for `arm64` and `aarch64` in install scripts.
## 0.4
## [1.0.6] - 2021-04-24
- show version number
### Added
## 0.3
- Support for rclone
- test autoupdate function
## [1.0.5] - 2021-04-24
### Fixed
- Correct exit code on backup failure and better logging/output/feedback.
- Check if `from` key is an actual directory.
## [1.0.4] - 2021-04-23
### Added
- Options to add rest username and password in config
### Fixed
- Don't add empty strings when saving config
## [1.0.3] - 2021-04-20
### Fixed
- Auto upgrade script was not working on linux as linux does not support writing to the binary that is being executed
## [1.0.2] - 2021-04-20
### Added
- Add the `cron` tag to backup to backups made with cron.
### Fixed
- Don't unlock lockfile if process is already running.
## [1.0.1] - 2021-04-17
### Added
- Completion command for various shells
## [1.0.0] - 2021-04-17
- Rewrite in go. See https://autorestic.vercel.app/upgrade for migration.

30
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,30 @@
# Development
## Coding
The easiest way (imo) is to run [`gowatch`](https://github.com/silenceper/gowatch) in a separate terminal and the simply run `./autorestic ...`. `gowatch` will watch the code and automatically rebuild the binary when changes are saved to disk.
## Building
```bash
go run build/build.go
```
This will build and compress binaries for multiple platforms. The output will be put in the `dist` folder.
## Releasing
Releases are automatically built by the github workflow and uploaded to the release.
1. Bump `VERSION` in `internal/config.go`.
2. Update `CHANGELOG.md`
3. Commit to master
4. Create a new release with the `v1.2.3` tag and mark as draft.
5. The Github action will build the binaries, upload and mark the release as ready when done.
### Brew
1. Download the latest release.
2. Check the checksum with `shasum -a 256 autorestic-1.2.3.tar.gz`
3. Update `url` and `sha256` in the brew repo.
4. Submit PR

215
LICENSE
View File

@@ -1,21 +1,202 @@
MIT License
Copyright (c) 2019 Nicco
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
1. Definitions.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

370
README.md
View File

@@ -1,342 +1,50 @@
# autorestic
High backup level CLI utility for [restic](https://restic.net/).
<p align="center">
<br>
<br>
<br>
<img align="center" src="https://github.com/cupcakearmy/autorestic/raw/master/.github/logo.png" height="50" alt="autorestic logo">
<br>
<br>
<p align="center">
Config driven, easy backup cli for <a href="https://restic.net/">restic</a>.
<br>
<strong><a href="https://autorestic.vercel.app/">»»» Docs & Getting Started »»»</a></strong>
<br><br>
<a target="_blank" href="https://discord.gg/wS7RpYTYd2">
<img src="https://img.shields.io/discord/252403122348097536" alt="discord badge" />
<img src="https://img.shields.io/github/contributors/cupcakearmy/autorestic" alt="contributor badge" />
<img src="https://img.shields.io/github/downloads/cupcakearmy/autorestic/total" alt="downloads badge" />
<img src="https://img.shields.io/github/v/release/cupcakearmy/autorestic" alt="version badge" />
</a>
</p>
</p>
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 🙂
<br>
<br>
![Sketch](./docs/Sketch.png)
### 💭 Why / What?
## 🌈 Features
Autorestic is a wrapper around the amazing [restic](https://restic.net/). While being amazing the restic cli can be a bit overwhelming and difficult to manage if you have many different location that you want to backup to multiple locations. This utility is aimed at making this easier 🙂
- Config files, no CLI
- Predictable
### 🌈 Features
- YAML config files, no CLI
- Incremental -> Minimal space is used
- Backup locations to multiple backends
- Snapshot policies and pruning
- Simple interface
- Fully encrypted
- Pre/After hooks
- Exclude pattern/files
- Cron jobs for automatic backup
- Backup & Restore docker volume
- Generated completions for `[bash|zsh|fish|powershell]`
### 📒 Docs
### ❓ Questions / Support
* [Locations](#-locations)
* [Pruning & Deleting old files](#pruning-and-snapshot-policies)
* [Excluding files](#excluding-filesfolders)
* [Hooks](#before--after-hooks)
* [Backends](#-backends)
* [Commands](#-commands)
* [Examples](#-examples)
Check the [discussions page](https://github.com/cupcakearmy/autorestic/discussions) or [join on discord](https://discord.gg/wS7RpYTYd2)
## 🛳 Installation
## Contributing / Developing
Linux & macOS. Windows is not supported.
```
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 -l home --from hdd --to /path/where/to/restore
```
### 📲 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
```
## 👉 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`
### 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
## 🐣 Examples
### List all the snapshots for all the backends
```
autorestic -a exec snapshots
```
### Unlock a locked repository
⚠️ Only do this if you know what you are doing. E.g. if you accidentally cancelled a running operation
```
autorestic -b my-backend exec unlock
```
## Contributors
This amazing people helped the project!
- @ChanceM [Docs]
- @EliotBerriot [Docs, Pruning, S3]
PRs, feature requests, etc. are welcomed :)
Have a look at [the dev docs](./DEVELOPMENT.md)

83
build/build.go Normal file
View File

@@ -0,0 +1,83 @@
// Heavily inspired (copied) by the restic build file
// https://github.com/restic/restic/blob/aa0faa8c7d7800b6ba7b11164fa2d3683f7f78aa/helpers/build-release-binaries/main.go#L225
package main
import (
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"sync"
"github.com/cupcakearmy/autorestic/internal"
)
var DIR, _ = filepath.Abs("./dist")
var targets = map[string][]string{
"darwin": {"amd64", "arm64"},
"freebsd": {"386", "amd64", "arm"},
"linux": {"386", "amd64", "arm", "arm64"},
"netbsd": {"386", "amd64"},
"openbsd": {"386", "amd64"},
}
type buildOptions struct {
Target, Arch, Version string
}
func build(options buildOptions, wg *sync.WaitGroup) {
fmt.Printf("Building %s %s\n", options.Target, options.Arch)
out := fmt.Sprintf("autorestic_%s_%s_%s", options.Version, options.Target, options.Arch)
out = path.Join(DIR, out)
out, _ = filepath.Abs(out)
fmt.Println(out)
// Build
{
c := exec.Command("go", "build", "-o", out, "./main.go")
c.Stdout = os.Stdout
c.Stderr = os.Stderr
c.Env = append(os.Environ(),
"CGO_ENABLED=0",
"GOOS="+options.Target,
"GOARCH="+options.Arch,
)
err := c.Run()
if err != nil {
panic(err)
}
}
// Compress
{
c := exec.Command("bzip2", out)
c.Dir = DIR
c.Stdout = os.Stdout
c.Stderr = os.Stderr
err := c.Run()
if err != nil {
panic(err)
}
}
wg.Done()
}
func main() {
os.RemoveAll(DIR)
v := internal.VERSION
var wg sync.WaitGroup
for target, archs := range targets {
for _, arch := range archs {
wg.Add(1)
build(buildOptions{
Target: target,
Arch: arch,
Version: v,
}, &wg)
}
}
wg.Wait()
}

42
cmd/backup.go Normal file
View File

@@ -0,0 +1,42 @@
package cmd
import (
"fmt"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
var backupCmd = &cobra.Command{
Use: "backup",
Short: "Create backups for given locations",
Run: func(cmd *cobra.Command, args []string) {
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
internal.GetConfig()
selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err)
errors := 0
for _, name := range selected {
location, _ := internal.GetLocation(name)
errs := location.Backup(false)
for err := range errs {
colors.Error.Println(err)
errors++
}
}
if errors > 0 {
CheckErr(fmt.Errorf("%d errors were found", errors))
}
},
}
func init() {
rootCmd.AddCommand(backupCmd)
internal.AddFlagsToCommand(backupCmd, false)
}

26
cmd/check.go Normal file
View File

@@ -0,0 +1,26 @@
package cmd
import (
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
var checkCmd = &cobra.Command{
Use: "check",
Short: "Check if everything is setup",
Run: func(cmd *cobra.Command, args []string) {
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
CheckErr(internal.CheckConfig())
colors.Success.Println("Everything is fine.")
},
}
func init() {
rootCmd.AddCommand(checkCmd)
}

70
cmd/completion.go Normal file
View File

@@ -0,0 +1,70 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: `To load completions:
Bash:
$ source <(autorestic completion bash)
# To load completions for each session, execute once:
# Linux:
$ autorestic completion bash > /etc/bash_completion.d/autorestic
# macOS:
$ autorestic completion bash > /usr/local/etc/bash_completion.d/autorestic
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ autorestic completion zsh > "${fpath[1]}/_autorestic"
# You will need to start a new shell for this setup to take effect.
fish:
$ autorestic completion fish | source
# To load completions for each session, execute once:
$ autorestic completion fish > ~/.config/fish/completions/autorestic.fish
PowerShell:
PS> autorestic completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> autorestic completion powershell > autorestic.ps1
# and source this file from your PowerShell profile.
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}
func init() {
rootCmd.AddCommand(completionCmd)
}

27
cmd/cron.go Normal file
View File

@@ -0,0 +1,27 @@
package cmd
import (
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
var cronCmd = &cobra.Command{
Use: "cron",
Short: "Run cron job for automated backups",
Long: `Intended to be mainly triggered by an automated system like systemd or crontab. For each location checks if a cron backup is due and runs it.`,
Run: func(cmd *cobra.Command, args []string) {
internal.CRON_LEAN, _ = cmd.Flags().GetBool("lean")
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
err = internal.RunCron()
CheckErr(err)
},
}
func init() {
rootCmd.AddCommand(cronCmd)
cronCmd.Flags().Bool("lean", false, "only output information about actual backups")
}

33
cmd/exec.go Normal file
View File

@@ -0,0 +1,33 @@
package cmd
import (
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
var execCmd = &cobra.Command{
Use: "exec",
Short: "Execute arbitrary native restic commands for given backends",
Run: func(cmd *cobra.Command, args []string) {
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
CheckErr(internal.CheckConfig())
selected, err := internal.GetAllOrSelected(cmd, true)
CheckErr(err)
for _, name := range selected {
colors.PrimaryPrint(" Executing on \"%s\" ", name)
backend, _ := internal.GetBackend(name)
backend.Exec(args)
}
},
}
func init() {
rootCmd.AddCommand(execCmd)
internal.AddFlagsToCommand(execCmd, true)
}

36
cmd/forget.go Normal file
View File

@@ -0,0 +1,36 @@
package cmd
import (
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
var forgetCmd = &cobra.Command{
Use: "forget",
Short: "Forget and optionally prune snapshots according the specified policies",
Run: func(cmd *cobra.Command, args []string) {
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
CheckErr(internal.CheckConfig())
selected, err := internal.GetAllOrSelected(cmd, false)
CheckErr(err)
prune, _ := cmd.Flags().GetBool("prune")
dry, _ := cmd.Flags().GetBool("dry-run")
for _, name := range selected {
location, _ := internal.GetLocation(name)
err := location.Forget(prune, dry)
CheckErr(err)
}
},
}
func init() {
rootCmd.AddCommand(forgetCmd)
internal.AddFlagsToCommand(forgetCmd, false)
forgetCmd.Flags().Bool("prune", false, "also prune repository")
forgetCmd.Flags().Bool("dry-run", false, "do not write changes, show what would be affected")
}

18
cmd/info.go Normal file
View File

@@ -0,0 +1,18 @@
package cmd
import (
"github.com/cupcakearmy/autorestic/internal"
"github.com/spf13/cobra"
)
var infoCmd = &cobra.Command{
Use: "info",
Short: "Show info about the config",
Run: func(cmd *cobra.Command, args []string) {
internal.GetConfig().Describe()
},
}
func init() {
rootCmd.AddCommand(infoCmd)
}

19
cmd/install.go Normal file
View File

@@ -0,0 +1,19 @@
package cmd
import (
"github.com/cupcakearmy/autorestic/internal/bins"
"github.com/spf13/cobra"
)
var installCmd = &cobra.Command{
Use: "install",
Short: "Install restic if missing",
Run: func(cmd *cobra.Command, args []string) {
err := bins.InstallRestic()
CheckErr(err)
},
}
func init() {
rootCmd.AddCommand(installCmd)
}

39
cmd/restore.go Normal file
View File

@@ -0,0 +1,39 @@
package cmd
import (
"fmt"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
)
var restoreCmd = &cobra.Command{
Use: "restore",
Short: "Restore backup for location",
Run: func(cmd *cobra.Command, args []string) {
err := lock.Lock()
CheckErr(err)
defer lock.Unlock()
location, _ := cmd.Flags().GetString("location")
l, ok := internal.GetLocation(location)
if !ok {
CheckErr(fmt.Errorf("invalid location \"%s\"", location))
}
target, _ := cmd.Flags().GetString("to")
from, _ := cmd.Flags().GetString("from")
force, _ := cmd.Flags().GetBool("force")
err = l.Restore(target, from, force)
CheckErr(err)
},
}
func init() {
rootCmd.AddCommand(restoreCmd)
restoreCmd.Flags().BoolP("force", "f", false, "Force, target folder will be overwritten")
restoreCmd.Flags().String("from", "", "Which backend to use")
restoreCmd.Flags().String("to", "", "Where to restore the data")
restoreCmd.Flags().StringP("location", "l", "", "Location to be restored")
restoreCmd.MarkFlagRequired("location")
}

61
cmd/root.go Normal file
View File

@@ -0,0 +1,61 @@
package cmd
import (
"os"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/spf13/cobra"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
)
func CheckErr(err error) {
if err != nil {
colors.Error.Fprintln(os.Stderr, "Error:", err)
lock.Unlock()
os.Exit(1)
}
}
var cfgFile string
var rootCmd = &cobra.Command{
Version: internal.VERSION,
Use: "autorestic",
Short: "CLI Wrapper for restic",
Long: "Documentation: https://autorestic.vercel.app",
}
func Execute() {
CheckErr(rootCmd.Execute())
}
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.autorestic.yml or ./.autorestic.yml)")
rootCmd.PersistentFlags().BoolVar(&internal.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
rootCmd.PersistentFlags().BoolVarP(&internal.VERBOSE, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().StringVar(&internal.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
cobra.OnInitialize(initConfig)
}
func initConfig() {
if ci, _ := rootCmd.Flags().GetBool("ci"); ci {
colors.DisableColors(true)
internal.VERBOSE = true
}
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := homedir.Dir()
CheckErr(err)
viper.AddConfigPath(".")
viper.AddConfigPath(home)
viper.SetConfigName(".autorestic")
}
viper.AutomaticEnv()
}

20
cmd/uninstall.go Normal file
View File

@@ -0,0 +1,20 @@
package cmd
import (
"github.com/cupcakearmy/autorestic/internal/bins"
"github.com/spf13/cobra"
)
var uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Uninstall restic and autorestic",
Run: func(cmd *cobra.Command, args []string) {
noRestic, _ := cmd.Flags().GetBool("no-restic")
bins.Uninstall(!noRestic)
},
}
func init() {
rootCmd.AddCommand(uninstallCmd)
uninstallCmd.Flags().Bool("no-restic", false, "do not uninstall restic.")
}

21
cmd/upgrade.go Normal file
View File

@@ -0,0 +1,21 @@
package cmd
import (
"github.com/cupcakearmy/autorestic/internal/bins"
"github.com/spf13/cobra"
)
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrade autorestic and restic",
Run: func(cmd *cobra.Command, args []string) {
noRestic, _ := cmd.Flags().GetBool("no-restic")
err := bins.Upgrade(!noRestic)
CheckErr(err)
},
}
func init() {
rootCmd.AddCommand(upgradeCmd)
upgradeCmd.Flags().Bool("no-restic", false, "also update restic")
}

15
docs/.codedoc/build.ts Normal file
View File

@@ -0,0 +1,15 @@
import { build } from '@codedoc/core';
import { config } from './config';
import { installTheme$ } from './content/theme';
import { content } from './content';
build(config, content, installTheme$, {
resolve: {
modules: ['.codedoc/node_modules']
},
resolveLoader: {
modules: ['.codedoc/node_modules']
}
});

24
docs/.codedoc/config.ts Normal file
View File

@@ -0,0 +1,24 @@
import { configuration } from '@codedoc/core'
export const config = configuration({
src: {
base: 'markdown',
},
dest: {
html: './build',
assets: './build',
bundle: './_',
styles: './_',
},
page: {
title: {
base: 'Autorestic',
},
},
misc: {
github: {
user: 'cupcakearmy',
repo: 'autorestic',
},
},
})

View File

@@ -0,0 +1,19 @@
import { CodedocConfig } from '@codedoc/core';
import { Footer as _Footer, GitterToggle$, Watermark} from '@codedoc/core/components';
export function Footer(config: CodedocConfig, renderer: any) {
let github$;
if (config.misc?.github)
github$ = <a href={`https://github.com/${config.misc.github.user}/${config.misc.github.repo}/`}
target="_blank">GitHub</a>;
let community$;
if (config.misc?.gitter)
community$ = <GitterToggle$ room={config.misc.gitter.room}/>
if (github$ && community$) return <_Footer>{github$}<hr/>{community$}</_Footer>;
else if (github$) return <_Footer>{github$}</_Footer>;
else if (community$) return <_Footer>{community$}</_Footer>;
else return <_Footer><Watermark/></_Footer>;
}

View File

@@ -0,0 +1,21 @@
import { CodedocConfig } from '@codedoc/core';
import { Header as _Header, GithubButton, Watermark } from '@codedoc/core/components';
export function Header(config: CodedocConfig, renderer: any) {
return (
<_Header>{config.misc?.github ?
<fragment>
<GithubButton action={config.misc.github.action || 'Star'}
repo={config.misc.github.repo}
user={config.misc.github.user}
large={config.misc.github.large === true}
count={config.misc.github.count !== false}
standardIcon={config.misc.github.standardIcon !== false}/>
<br/><br/>
</fragment>
: ''}
<Watermark/>
</_Header>
)
}

View File

@@ -0,0 +1,57 @@
import { RendererLike } from '@connectv/html'
import { File } from 'rxline/fs'
import {
Page,
Meta,
ContentNav,
Fonts,
ToC,
GithubSearch$,
} from '@codedoc/core/components'
import { config } from '../config'
import { Header } from './header'
import { Footer } from './footer'
export function content(
_content: HTMLElement,
toc: HTMLElement,
renderer: RendererLike<any, any>,
file: File<string>
) {
return (
<Page
title={config.page.title.extractor(_content, config, file)}
favicon={config.page.favicon}
meta={<Meta {...config.page.meta} />}
fonts={<Fonts {...config.page.fonts} />}
scripts={config.page.scripts}
stylesheets={config.page.stylesheets}
header={<Header {...config} />}
footer={<Footer {...config} />}
toc={
<ToC
default={'open'}
search={
config.misc?.github ? (
<GithubSearch$
repo={config.misc.github.repo}
user={config.misc.github.user}
root={config.src.base}
pick={config.src.pick.source}
drop={config.src.drop.source}
/>
) : (
false
)
}
>
{toc}
</ToC>
}
>
{_content}
<ContentNav content={_content} />
</Page>
)
}

View File

@@ -0,0 +1,8 @@
import { funcTransport } from '@connectv/sdh/transport';
import { useTheme } from '@codedoc/core/transport';
import { theme } from '../theme';
export function installTheme() { useTheme(theme); }
export const installTheme$ = /*#__PURE__*/funcTransport(installTheme);

10811
docs/.codedoc/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"@codedoc/core": "^0.2.23"
}
}

18
docs/.codedoc/serve.ts Normal file
View File

@@ -0,0 +1,18 @@
import { join } from 'path';
import { serve } from '@codedoc/core';
import { config } from './config';
import { content } from './content';
import { installTheme$ } from './content/theme';
const root = join(__dirname, '../');
serve(root, config, content, installTheme$, {
resolve: {
modules: ['.codedoc/node_modules']
},
resolveLoader: {
modules: ['.codedoc/node_modules']
}
});

11
docs/.codedoc/theme.ts Normal file
View File

@@ -0,0 +1,11 @@
import { createTheme } from '@codedoc/core/transport';
export const theme = /*#__PURE__*/createTheme({
light: {
primary: '#1eb2a6'
},
dark: {
primary: '#1eb2a6'
}
});

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es6",
"noImplicitAny": true,
"declaration": false,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"alwaysStrict": true,
"sourceMap": true,
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"jsx": "react",
"jsxFactory": "renderer.create",
"lib": [
"es2017",
"dom"
]
},
"include": [
"./**/*"
]
}

22
docs/.codedoc/watch.ts Normal file
View File

@@ -0,0 +1,22 @@
import { exec, spawn } from 'child_process';
import { config } from './config';
const cmd = 'ts-node-dev';
const params = `--project .codedoc/tsconfig.json`
+ ` -T --watch ${config.src.base},.codedoc`
+ ` --ignore-watch .codedoc/node_modules`
+ ` .codedoc/serve`;
if (process.platform === 'win32') {
const child = exec(cmd + ' ' + params);
child.stdout?.pipe(process.stdout);
child.stderr?.pipe(process.stderr);
child.on('close', () => {});
}
else {
const child = spawn(cmd, [params], { stdio: 'inherit', shell: 'bash' });
child.on('close', () => {});
}

2
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
build

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

45
docs/markdown/_toc.md Normal file
View File

@@ -0,0 +1,45 @@
[Home](/)
[Quick Start](/quick)
[Installation](/installation)
[Configuration](/config)
[Upgrade](/upgrade)
> :Collapse label=Locations
>
> [Overview](/location/overview)
> [Hooks](/location/hooks)
>
> > :Collapse label=Options
> >
> > [Overview](/location/options)
> > [Excluding Files](/location/exclude)
> > [Forget Policy](/location/forget)
>
> [Cron](/location/cron)
> [Docker Volumes](/location/docker)
> :Collapse label=Backend
>
> [Overview](/backend/overview)
> [Available Backends](/backend/available)
> :Collapse label=CLI
>
> [General](/cli/general)
> [Info](/cli/info)
> [Check](/cli/check)
> [Completion](/cli/completion)
> [Backup](/cli/backup)
> [Restore](/cli/restore)
> [Forget](/cli/forget)
> [Cron](/cli/cron)
> [Exec](/cli/exec)
> [Install](/cli/install)
> [Uninstall](/cli/uninstall)
> [Update](/cli/update)
[Examples](/examples)
[QA](/qa)
[Contributors](/contrib)

View File

@@ -0,0 +1,85 @@
# Available Backends
In theory [all the restic backends](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html) are supported.
Those tested are the following:
## Local
```yaml
backends:
name-of-backend:
type: local
path: /data/my/backups
```
## Backblaze
```yaml
backends:
name-of-backend:
type: b2
path: 'backblaze_bucketID'
# Or With a path
# path: 'backblaze_bucketID:/some/path'
env:
B2_ACCOUNT_ID: 'backblaze_keyID'
B2_ACCOUNT_KEY: 'backblaze_applicationKey'
```
#### API Keys gotcha
If you use a _File name prefix_ when making the application key, do not include a leading slash. Make sure to include this prefix in the path (e.g. `path: 'backblaze_bucketID:my/path'`).
## S3 / Minio
```yaml
backends:
name-of-backend:
type: s3
path: s3.amazonaws.com/bucket_name
# Minio
# path: http://localhost:9000/bucket_name
env:
AWS_ACCESS_KEY_ID: my_key
AWS_SECRET_ACCESS_KEY: my_secret
```
## SFTP
For SFTP to work you need to use configure your host inside of ~/.ssh/config as password prompt is not supported. For more information on this topic please see the [official docs](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#sftp) on the matter.
```yaml
backends:
name-of-backend:
type: sftp
path: my-host:/remote/path/on/the/server
```
## Rest Server
See [here](https://github.com/restic/rest-server) for how to install a rest server backend and [here](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server) for further documentation.
```yaml
backends:
name-of-backend:
type: rest
path: http://localhost:8000/repo_name
# Or authenticated
path: https://user:pass@host:6969/path
```
Optionally you can set user and password separately
```yaml
backends:
rest:
type: rest
path: http://localhost:6969/path
key: ...
rest:
user: user
password: pass
```
> :ToCPrevNext

View File

@@ -0,0 +1,16 @@
# 💽 Backends
Backends are the outputs of the backup process. Each location needs at least one.
```yaml | .autorestic.yml
backends:
name-of-backend:
type: local
path: /data/my/backups
```
## Types
We restic supports multiple types of backends. See the [full list](/backend/available) for details.
> :ToCPrevNext

View File

@@ -0,0 +1,17 @@
# Backup
```bash
autorestic backup [-l, --location] [-a, --all]
```
Performs a backup of all locations if the `-a` flag is passed. To only backup some locations pass one or more `-l` or `--location` flags.
```bash
# All
autorestic backup -a
# Some
autorestic backup -l foo -l bar
```
> :ToCPrevNext

View File

@@ -0,0 +1,11 @@
# Check
```bash
autorestic check
```
Checks locations and backends are configured properly and initializes them if they are not already.
This is mostly an internal command, but useful to verify if a backend is configured correctly.
> :ToCPrevNext

View File

@@ -0,0 +1,17 @@
# Completion
```bash
autorestic completion [bash|zsh|fish|powershell]
```
Autorestic can generate shell completions automatically to make the experience even easier.
Supported shells are
- bash
- zsh
- fish
- powershell
To see how to install run `autorestic help completion` and follow the instructions for your specific shell
> :ToCPrevNext

13
docs/markdown/cli/cron.md Normal file
View File

@@ -0,0 +1,13 @@
# Cron
```bash
autorestic cron [--lean]
```
This command is mostly intended to be triggered by an automated system like systemd or crontab.
It will run cron jobs as [specified in the cron section](/location/cron) of a specific location.
The `--lean` flag will omit output like _skipping location x: not due yet_. This can be useful if you are dumping the output of the cron job to a log file and don't want to be overwhelmed by the output log.
> :ToCPrevNext

15
docs/markdown/cli/exec.md Normal file
View File

@@ -0,0 +1,15 @@
# Exec
```bash
autorestic exec [-b, --backend] [-a, --all] <command> -- [native options]
```
This is a very handy command which enables you to run any native restic command on desired backends. Generally you will want to include the verbose flag `-v, --verbose` to see the output. An example would be listing all the snapshots of all your backends:
```bash
autorestic exec -av -- snapshots
```
With `exec` you can basically run every cli command that you would be able to run with the restic cli. It only pre-fills path, key, etc.
> :ToCPrevNext

View File

@@ -0,0 +1,13 @@
# Forget
```bash
autorestic forget [-l, --location] [-a, --all] [--dry-run] [--prune]
```
This will prune and remove old data form the backends according to the [keep policy you have specified for the location](/location/forget)
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.
The `--prune` flag will also [prune the data](https://restic.readthedocs.io/en/latest/060_forget.html#removing-backup-snapshots). This is a costly operation that can take longer, however it will free up the actual space.
> :ToCPrevNext

View File

@@ -0,0 +1,38 @@
# General
## `-c, --config`
Specify the config file to be used.
If omitted `autorestic` will search for for a `.autorestic.yml` in the current directory and your home directory.
```bash
autorestic -c /path/to/my/config.yml
```
## `--ci`
Run the CLI in CI Mode, which means there will be no interactivity, no colors and automatically sets the `--verbose` flag.
This can be useful when you want to run cron e.g. as all the output will be saved.
```bash
autorestic --ci backup -a
```
## `-v, --verbose`
Verbose mode will show the output of the native restic commands that are otherwise not printed out. Useful for debugging or logging in automated tasks.
```bash
autorestic --verbose backup -a
```
## `--restic-bin`
With `--restic-bin` you can specify to run a specific restic binary. This can be useful if you want to [create a custom binary with root access that can be executed by any user](https://restic.readthedocs.io/en/stable/080_examples.html#full-backup-without-root).
```bash
autorestic --restic-bin /some/path/to/my/custom/restic/binary
```
> :ToCPrevNext

18
docs/markdown/cli/info.md Normal file
View File

@@ -0,0 +1,18 @@
# Info
Displays the config file that autorestic is referring to.
Useful when you want to quickly see what locations are being backed-up where.
**Pro tip:** if it gets a bit long you can read it more easily with `autorestic info | less` 😉
```bash
autorestic info
```
## With a custom file
```bash
autorestic -c path/to/some/config.yml info
```
> :ToCPrevNext

View File

@@ -0,0 +1,9 @@
# Install
Installs both restic and autorestic to `/usr/local/bin`.
```bash
autorestic install
```
> :ToCPrevNext

View File

@@ -0,0 +1,19 @@
# Restore
```bash
autorestic restore [-l, --location] [--from backend] [--to <out dir>] [-f, --force]
```
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.
The `--to` path has to be empty as no data will be overwritten by default. If you are sure you can pass the `-f, --force` flag and the data will be overwritten in the destination. However note that this will overwrite all the data existent in the backup, not only the 1 file that is missing e.g.
## Example
```bash
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`
> :ToCPrevNext

View File

@@ -0,0 +1,9 @@
# Uninstall
Uninstalls both restic and autorestic from `/usr/local/bin`.
```bash
autorestic uninstall
```
> :ToCPrevNext

View File

@@ -0,0 +1,11 @@
# Update
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.
```bash
autorestic update
```
Updates both restic and autorestic automagically.
> :ToCPrevNext

42
docs/markdown/config.md Normal file
View File

@@ -0,0 +1,42 @@
# 🎛 Config File
## Path
By default autorestic searches for a `.autorestic.yml` file in the current directory and your home folder.
- `./.autorestic.yml`
- `~/.autorestic.yml`
You can also specify a custom file with the `-c path/to/some/config.yml`
> **⚠️ WARNING ⚠️**
>
> 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 or config file somewhere in case your server dies**. Otherwise DATA IS LOST!
## Example configuration
```yaml | .autorestic.yml
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
```
> :ToCPrevNext

16
docs/markdown/contrib.md Normal file
View File

@@ -0,0 +1,16 @@
# 🙋‍♀️🙋‍♂️ Contributors
This amazing people helped the project!
- @agateblue - Docs, Pruning, S3
- @david-boles - Docs
- @SebDanielsson - Brew
- @n194 - AUR Package
- @jin-park-dev - Typos
- @sumnerboy12 - Typos
- @FuzzyMistborn - Typos
- @ChanceM - Typos
- @TheForcer - Typos
- @themorlan - Typos
> :ToCPrevNext

17
docs/markdown/examples.md Normal file
View File

@@ -0,0 +1,17 @@
# 🐣 Examples
## List all the snapshots for all the backends
```bash
autorestic exec -av -- snapshots
```
## Unlock a locked repository
This can come in handy if a backup process crashed or if it was accidentally cancelled. Then the repository would still be locked without an actual process using it. Only do this if you know what you are doing and are sure no other process is actually reading/writing to the repository of course.
```bash
autorestic exec -b my-backend -- unlock
```
> :ToCPrevNext

22
docs/markdown/index.md Normal file
View File

@@ -0,0 +1,22 @@
# autorestic
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 difficult to manage if you have many different location that you want to backup to multiple locations. This utility is aimed at making this easier 🙂
> If you are coming from `0.x` see the [upgrade guide](/upgrade).
## 🌈 Features
- YAML config files, no CLI
- Incremental -> Minimal space is used
- Backup locations to multiple backends
- Snapshot policies and pruning
- Fully encrypted
- Pre/After hooks
- Exclude pattern/files
- Cron jobs for automatic backup
- Backup & Restore docker volumes
- Generated completions for `[bash|zsh|fish|powershell]`
> :ToCPrevNext

View File

@@ -0,0 +1,25 @@
# 🛳 Installation
Linux & macOS. Windows is not supported. If you have problems installing please open an issue :)
Autorestic requires `curl`, `wget` and `bzip2` to be installed. For most systems these should be already installed.
```bash
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
```
## Alternatives
### Manual
You can download the right binary from the release page and simply copy it to `/usr/local/bin` or whatever path you prefer. Autoupdates will still work.
### Brew
If you are on macOS you can install through brew: `brew install autorestic`.
### AUR
If you are on Arch there is an [AUR Package](https://aur.archlinux.org/packages/autorestic-bin/) by @n194.
> :ToCPrevNext

View File

@@ -0,0 +1,49 @@
# Cron
Often it is usefully to trigger backups automatically. For this we can specify a `cron` attribute to each location.
```yaml | .autorestic.yml
locations:
my-location:
from: /data
to: my-backend
cron: '0 3 * * 0' # Every Sunday at 3:00
```
Here is a awesome website with [some examples](https://crontab.guru/examples.html) and an [explorer](https://crontab.guru/)
## Installing the cron
**This has to be done only once, regardless of now many cron jobs you have in your config file.**
To actually enable cron jobs you need something to call `autorestic cron` on a timed schedule.
Note that the schedule has nothing to do with the `cron` attribute in each location.
My advise would be to trigger the command every 5min, but if you have a cronjob that runs only once a week, it's probably enough to schedule it once a day.
### Crontab
Here is an example using crontab, but systemd would do too.
First, open your crontab in edit mode
```bash
crontab -e
```
Then paste this at the bottom of the file and save it. Note that in this specific example the config file is located at one of the default locations (e.g. `~/.autorestic.yml`). If your config is somewhere else you'll need to specify it using the `-c` option.
```bash
# This is required, as it otherwise cannot find restic as a command.
PATH="/usr/local/bin:/usr/bin:/bin"
# Example running every 5 minutes
*/5 * * * * autorestic --ci cron
```
> The `--ci` option is not required, but recommended
Now you can add as many `cron` attributes as you wish in the config file ⏱
> Also note that manually triggered backups with `autorestic backup` will not influence the cron timeline, they are willingly not linked.
> :ToCPrevNext

View File

@@ -0,0 +1,43 @@
# Docker
autorestic supports docker volumes directly, without needing them to be mounted to the host filesystem.
```yaml | docker-compose.yml
version: '3.7'
volumes:
data:
name: my-data
services:
api:
image: alpine
volumes:
- data:/foo/bar
```
```yaml | .autorestic.yml
locations:
- name: hello
from: volume:my-data
to:
- remote
backends:
- name: remote
# ...
```
Now you can backup and restore as always.
```bash
autorestic backup -l hello
```
```bash
autorestic restore -l hello
```
The volume has to exists whenever backing up or restoring.
> :ToCPrevNext

View File

@@ -0,0 +1,20 @@
# Excluding files
If you want to exclude certain files or folders it done easily by specifying 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) so you can use any flag used there.
```yaml
locations:
my-location:
from: /data
to: my-backend
options:
backup:
exclude:
- '*.nope'
- '*.abc'
exclude-file: .gitignore
```
> :ToCPrevNext

View File

@@ -0,0 +1,25 @@
# Forget/Prune 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:
> **Note** This is a full example, of course you also can specify only one of them
```yaml | .autorestic.yml
locations:
etc:
from: /etc
to: local
options:
forget:
keep-last: 5 # always keep at least 5 snapshots
keep-hourly: 3 # keep 3 last hourly snapshots
keep-daily: 4 # keep 4 last daily snapshots
keep-weekly: 1 # keep 1 last weekly snapshots
keep-monthly: 12 # keep 12 last monthly snapshots
keep-yearly: 7 # keep 7 last yearly snapshots
keep-within: '2w' # keep snapshots from the last 2 weeks
```
> :ToCPrevNext

View File

@@ -0,0 +1,42 @@
# Hooks
If you want to perform some commands before and/or after a backup, you can use hooks.
They consist of a list of commands that will be executed in the same directory as the target `from`.
The following hooks groups are supported, none are required:
- `before`
- `after`
- `failure`
- `success`
```yml | .autorestic.yml
locations:
my-location:
from: /data
to: my-backend
hooks:
before:
- echo "One"
- echo "Two"
- echo "Three"
after:
- echo "Byte"
failure:
- echo "Something went wrong"
success:
- echo "Well done!"
```
## Flowchart
1. `before` hook
2. Run backup
3. `after` hook
4. - `success` hook if no errors were found
- `failure` hook if at least error was encountered
If the `before` hook encounters errors the backup and `after` hooks will be skipped and only the `failed` hooks will run.
> :ToCPrevNext

View File

@@ -0,0 +1,19 @@
# Options
For the `backup` and `forget` commands you can pass any native flags to `restic`.
```yaml
locations:
foo:
path: ...
to: ...
options:
backup:
tag:
- foo
- bar
```
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
> :ToCPrevNext

View File

@@ -0,0 +1,27 @@
# 🗂 Locations
Locations can be seen as the input to the backup process. Generally this is simply a folder.
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 | .autorestic.yml
locations:
my-location-name:
from: path/to/backup
to:
- name-of-backend
- also-backup-to-this-backend
```
## `from`
This is the source of the location.
#### How are paths resolved?
Paths can be absolute or relative. If relative they are resolved relative to the location of the config file. Tilde `~` paths are also supported for home folder resolution.
## `to`
This is either a single backend or an array of backends. The backends have to be configured in the same config file.
> :ToCPrevNext

10
docs/markdown/qa.md Normal file
View File

@@ -0,0 +1,10 @@
# ❓ QA
## My config file was moved?
This happens when autorestic needs to write to the config file: e.g. when we are generating a key for you.
Unfortunately during this process formatting and comments are lost because the `yaml` library used is not comment and/or format aware.
That is why autorestic will place a copy of your old config next to the one we are writing to.
> :ToCPrevNext

80
docs/markdown/quick.md Normal file
View File

@@ -0,0 +1,80 @@
# 🚀 Quickstart
## Installation
```bash
curl -s https://raw.githubusercontent.com/CupCakeArmy/autorestic/master/install.sh | bash
```
See [installation](/installation) for alternative options.
## Write a simple config file
```bash
vim ~/.autorestic.yml
```
For a quick overview:
- `locations` can be seen as the inputs and `backends` the output where the data is stored and backed up.
- One `location` can have one or multiple `backends` for redundancy.
- One `backend` can also be the target for multiple `locations`.
> **⚠️ WARNING ⚠️**
>
> 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 or config file somewhere in case your server dies**. Otherwise DATA IS LOST!
```yaml | .autorestic.yml
locations:
home:
from: /home/me
to: remote
important:
from: /path/to/important/stuff
to:
- remote
- hdd
backends:
remote:
type: s3
path: 's3.amazonaws.com/bucket_name'
key: some-random-password-198rc79r8y1029c8yfewj8f1u0ef87yh198uoieufy
env:
AWS_ACCESS_KEY_ID: account_id
AWS_SECRET_ACCESS_KEY: account_key
hdd:
type: local
path: /mnt/my_external_storage
key: 'if not key is set it will be generated for you'
```
## Check
```bash
autorestic check
```
This checks if the config file has any issues. If this is the first time this can take longer as autorestic will setup the backends.
Now is good time to **backup the config**. After you run autorestic at least once we will add the generated encryption keys to the config.
## Backup
```bash
autorestic backup -a
```
This will do a backup of all locations.
## Restore
```bash
autorestic restore -l home --from hdd --to /path/where/to/restore
```
This will restore the location `home` from the backend `hdd` to the given path.
> :ToCPrevNext

28
docs/markdown/upgrade.md Normal file
View File

@@ -0,0 +1,28 @@
# Update
## From `0.x` to `1.0`
Most of the config file is remained compatible, however to clean up the backends custom environment variables were moved from the root object to an `env` object.
```yaml
# Before
remote:
type: b2
path: bucket:path/to/backup
key: some random encryption key
B2_ACCOUNT_ID: id
B2_ACCOUNT_KEY: key
# After
remote:
type: b2
path: bucket:path/to/backup
key: some random encryption key
env:
B2_ACCOUNT_ID: id
B2_ACCOUNT_KEY: key
```
Other than the config file there is a new `-v, --verbose` flag which shows the output of native commands, which are now hidden by default.
> :ToCPrevNext

1828
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

10
docs/package.json Normal file
View File

@@ -0,0 +1,10 @@
{
"private": true,
"scripts": {
"build": "codedoc install && codedoc build",
"dev": "codedoc serve"
},
"dependencies": {
"@codedoc/cli": "^0.2.8"
}
}

3
docs/vercel.json Normal file
View File

@@ -0,0 +1,3 @@
{
"cleanUrls": true
}

13
go.mod Normal file
View File

@@ -0,0 +1,13 @@
module github.com/cupcakearmy/autorestic
go 1.16
require (
github.com/blang/semver/v4 v4.0.0
github.com/buger/goterm v1.0.0
github.com/fatih/color v1.10.0
github.com/mitchellh/go-homedir v1.1.0
github.com/robfig/cron v1.2.0
github.com/spf13/cobra v1.1.3
github.com/spf13/viper v1.7.1
)

328
go.sum Normal file
View File

@@ -0,0 +1,328 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/buger/goterm v1.0.0 h1:ZB6uUlY8+sjJyFGzz2WpRqX2XYPeXVgtZAOJMwOsTWM=
github.com/buger/goterm v1.0.0/go.mod h1:16STi3LquiscTIHA8SXUNKEa/Cnu4ZHBH8NsCaWgso0=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View File

@@ -1,22 +1,41 @@
#!/bin/bash
shopt -s nocaseglob
OUT_FILE=/usr/local/bin/autorestic
if [[ "$OSTYPE" == "linux-gnu" ]]; then
TYPE=linux
elif [[ "$OSTYPE" == "darwin"* ]]; then
TYPE=macos
# Type
NATIVE_OS=$(uname | tr '[:upper:]' '[:lower:]')
if [[ $NATIVE_OS == *"linux"* ]]; then
OS=linux
elif [[ $NATIVE_OS == *"darwin"* ]]; then
OS=darwin
else
echo "Unsupported OS"
exit
echo "Could not determine OS automatically, please check the release page manually: https://github.com/cupcakearmy/autorestic/releases"
exit 1
fi
echo $OS
NATIVE_ARCH=$(uname -m | tr '[:upper:]' '[:lower:]')
if [[ $NATIVE_ARCH == *"x86_64"* ]]; then
ARCH=amd64
elif [[ $NATIVE_ARCH == *"arm64"* || $NATIVE_ARCH == *"aarch64"* ]]; then
ARCH=arm64
elif [[ $NATIVE_ARCH == *"x86"* ]]; then
ARCH=386
else
echo "Could not determine Architecure automatically, please check the release page manually: https://github.com/cupcakearmy/autorestic/releases"
exit 1
fi
echo $ARCH
curl -s https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
| grep "browser_download_url.*_${TYPE}" \
| grep "browser_download_url.*_${OS}_${ARCH}" \
| cut -d : -f 2,3 \
| tr -d \" \
| wget -O ${OUT_FILE} -i -
| wget -O "${OUT_FILE}.bz2" -i -
bzip2 -fd "${OUT_FILE}.bz2"
chmod +x ${OUT_FILE}
autorestic install
autorestic
echo "Successfully installed autorestic"

165
internal/backend.go Normal file
View File

@@ -0,0 +1,165 @@
package internal
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/url"
"os"
"strings"
"github.com/cupcakearmy/autorestic/internal/colors"
)
type BackendRest struct {
User string `yaml:"user,omitempty"`
Password string `yaml:"password,omitempty"`
}
type Backend struct {
name string
Type string `yaml:"type,omitempty"`
Path string `yaml:"path,omitempty"`
Key string `yaml:"key,omitempty"`
Env map[string]string `yaml:"env,omitempty"`
Rest BackendRest `yaml:"rest,omitempty"`
}
func GetBackend(name string) (Backend, bool) {
b, ok := GetConfig().Backends[name]
b.name = name
return b, ok
}
func (b Backend) generateRepo() (string, error) {
switch b.Type {
case "local":
return GetPathRelativeToConfig(b.Path)
case "rest":
parsed, err := url.Parse(b.Path)
if err != nil {
return "", err
}
if b.Rest.User != "" {
if b.Rest.Password == "" {
parsed.User = url.User(b.Rest.User)
} else {
parsed.User = url.UserPassword(b.Rest.User, b.Rest.Password)
}
}
return fmt.Sprintf("%s:%s", b.Type, parsed.String()), nil
case "b2", "azure", "gs", "s3", "sftp", "rclone":
return fmt.Sprintf("%s:%s", b.Type, b.Path), nil
default:
return "", fmt.Errorf("backend type \"%s\" is invalid", b.Type)
}
}
func (b Backend) getEnv() (map[string]string, error) {
env := make(map[string]string)
env["RESTIC_PASSWORD"] = b.Key
repo, err := b.generateRepo()
env["RESTIC_REPOSITORY"] = repo
for key, value := range b.Env {
env[strings.ToUpper(key)] = value
}
return env, err
}
func generateRandomKey() string {
b := make([]byte, 64)
rand.Read(b)
key := base64.StdEncoding.EncodeToString(b)
key = strings.ReplaceAll(key, "=", "")
key = strings.ReplaceAll(key, "+", "")
key = strings.ReplaceAll(key, "/", "")
return key
}
func (b Backend) validate() error {
if b.Type == "" {
return fmt.Errorf(`Backend "%s" has no "type"`, b.name)
}
if b.Path == "" {
return fmt.Errorf(`Backend "%s" has no "path"`, b.name)
}
if b.Key == "" {
key := generateRandomKey()
b.Key = key
c := GetConfig()
tmp := c.Backends[b.name]
tmp.Key = key
c.Backends[b.name] = tmp
if err := c.SaveConfig(); err != nil {
return err
}
}
env, err := b.getEnv()
if err != nil {
return err
}
options := ExecuteOptions{Envs: env}
// Check if already initialized
_, err = ExecuteResticCommand(options, "snapshots")
if err == nil {
return nil
} else {
// If not initialize
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
out, err := ExecuteResticCommand(options, "init")
if VERBOSE {
colors.Faint.Println(out)
}
return err
}
}
func (b Backend) Exec(args []string) error {
env, err := b.getEnv()
if err != nil {
return err
}
options := ExecuteOptions{Envs: env}
out, err := ExecuteResticCommand(options, args...)
if err != nil {
colors.Error.Println(out)
return err
}
if VERBOSE {
colors.Faint.Println(out)
}
return nil
}
func (b Backend) ExecDocker(l Location, args []string) (string, error) {
env, err := b.getEnv()
if err != nil {
return "", err
}
volume := l.getVolumeName()
path, _ := l.getPath()
options := ExecuteOptions{
Command: "docker",
Envs: env,
}
docker := []string{
"run", "--rm",
"--entrypoint", "ash",
"--workdir", path,
"--volume", volume + ":" + path,
}
if hostname, err := os.Hostname(); err == nil {
docker = append(docker, "--hostname", hostname)
}
if b.Type == "local" {
actual := env["RESTIC_REPOSITORY"]
docker = append(docker, "--volume", actual+":"+"/repo")
env["RESTIC_REPOSITORY"] = "/repo"
}
for key, value := range env {
docker = append(docker, "--env", key+"="+value)
}
docker = append(docker, "restic/restic", "-c", "restic "+strings.Join(args, " "))
out, err := ExecuteCommand(options, docker...)
return out, err
}

149
internal/bins/bins.go Normal file
View File

@@ -0,0 +1,149 @@
package bins
import (
"compress/bzip2"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"runtime"
"strings"
"github.com/blang/semver/v4"
"github.com/cupcakearmy/autorestic/internal"
"github.com/cupcakearmy/autorestic/internal/colors"
)
const INSTALL_PATH = "/usr/local/bin"
type GithubReleaseAsset struct {
Name string `json:"name"`
Link string `json:"browser_download_url"`
}
type GithubRelease struct {
Tag string `json:"tag_name"`
Assets []GithubReleaseAsset `json:"assets"`
}
func dlJSON(url string) (GithubRelease, error) {
var parsed GithubRelease
resp, err := http.Get(url)
if err != nil {
return parsed, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return parsed, err
}
json.Unmarshal(body, &parsed)
return parsed, nil
}
func Uninstall(restic bool) error {
if err := os.Remove(path.Join(INSTALL_PATH, "autorestic")); err != nil {
colors.Error.Println(err)
}
if restic {
if err := os.Remove(path.Join(INSTALL_PATH, "restic")); err != nil {
colors.Error.Println(err)
}
}
return nil
}
func downloadAndInstallAsset(body GithubRelease, name string) error {
ending := fmt.Sprintf("_%s_%s.bz2", runtime.GOOS, runtime.GOARCH)
for _, asset := range body.Assets {
if strings.HasSuffix(asset.Name, ending) {
// Download archive
colors.Faint.Println("Downloading:", asset.Link)
resp, err := http.Get(asset.Link)
if err != nil {
return err
}
defer resp.Body.Close()
// Uncompress
bz := bzip2.NewReader(resp.Body)
// Save to tmp
// Linux does not support overwriting the file that is currently being overwritten, but it can be deleted and a new one moved in its place.
tmp, err := ioutil.TempFile(os.TempDir(), "autorestic-")
if err != nil {
return err
}
defer tmp.Close()
tmp.Chmod(0755)
io.Copy(tmp, bz)
to := path.Join(INSTALL_PATH, name)
os.Remove(to) // Delete if current, ignore error if file does not exits.
if err := os.Rename(tmp.Name(), to); err != nil {
return nil
}
colors.Success.Printf("Successfully installed '%s' under %s\n", name, INSTALL_PATH)
return nil
}
}
return errors.New("could not find right binary for your system, please install restic manually. https://bit.ly/2Y1Rzai")
}
func InstallRestic() error {
installed := internal.CheckIfCommandIsCallable("restic")
if installed {
colors.Body.Println("restic already installed")
return nil
} else {
if body, err := dlJSON("https://api.github.com/repos/restic/restic/releases/latest"); err != nil {
return err
} else {
return downloadAndInstallAsset(body, "restic")
}
}
}
func upgradeRestic() error {
out, err := internal.ExecuteCommand(internal.ExecuteOptions{
Command: "restic",
}, "self-update")
colors.Faint.Println(out)
return err
}
func Upgrade(restic bool) error {
// Upgrade restic
if restic {
if err := InstallRestic(); err != nil {
colors.Error.Println(err)
}
upgradeRestic()
}
// Upgrade self
current, err := semver.ParseTolerant(internal.VERSION)
if err != nil {
return err
}
body, err := dlJSON("https://api.github.com/repos/cupcakearmy/autorestic/releases/latest")
if err != nil {
return err
}
latest, err := semver.ParseTolerant(body.Tag)
if err != nil {
return err
}
if current.LT(latest) {
downloadAndInstallAsset(body, "autorestic")
colors.Success.Println("Updated autorestic")
} else {
colors.Body.Println("Already up to date")
}
return nil
}

29
internal/colors/colors.go Normal file
View File

@@ -0,0 +1,29 @@
package colors
import (
"fmt"
"strings"
"github.com/fatih/color"
)
var Body = color.New()
var Primary = color.New(color.Bold, color.BgBlue, color.FgHiWhite)
var Secondary = color.New(color.Bold, color.FgCyan)
var Success = color.New(color.FgGreen)
var Error = color.New(color.FgRed, color.Bold)
var Faint = color.New(color.Faint)
func PrimaryPrint(msg string, args ...interface{}) {
fmt.Printf("\n\n%s\n\n", Primary.Sprintf(" "+msg+" ", args...))
}
func DisableColors(state bool) {
color.NoColor = state
}
func PrintDescription(left string, right string) {
right = strings.Trim(right, "\n")
right = strings.Trim(right, "\t")
Body.Printf("%s\t%s\n", Secondary.Sprint(left), right)
}

222
internal/config.go Normal file
View File

@@ -0,0 +1,222 @@
package internal
import (
"fmt"
"path"
"strings"
"sync"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const VERSION = "1.1.0"
var CI bool = false
var VERBOSE bool = false
var CRON_LEAN bool = false
type Config struct {
Locations map[string]Location `yaml:"locations"`
Backends map[string]Backend `yaml:"backends"`
}
var once sync.Once
var config *Config
func GetConfig() *Config {
if config == nil {
once.Do(func() {
if err := viper.ReadInConfig(); err == nil {
if !CRON_LEAN {
colors.Faint.Println("Using config file:", viper.ConfigFileUsed())
}
} else {
return
}
config = &Config{}
if err := viper.UnmarshalExact(config); err != nil {
panic(err)
}
})
}
return config
}
func GetPathRelativeToConfig(p string) (string, error) {
if path.IsAbs(p) {
return p, nil
} else if strings.HasPrefix(p, "~") {
home, err := homedir.Dir()
return path.Join(home, strings.TrimPrefix(p, "~")), err
} else {
return path.Join(path.Dir(viper.ConfigFileUsed()), p), nil
}
}
func (c *Config) Describe() {
// Locations
for name, l := range c.Locations {
var tmp string
colors.PrimaryPrint(`Location: "%s"`, name)
colors.PrintDescription("From", l.From)
tmp = ""
for _, to := range l.To {
tmp += fmt.Sprintf("\t%s %s\n", colors.Success.Sprint("→"), to)
}
colors.PrintDescription("To", tmp)
if l.Cron != "" {
colors.PrintDescription("Cron", l.Cron)
}
tmp = ""
hooks := map[string][]string{
"Before": l.Hooks.Before,
"After": l.Hooks.After,
"Failure": l.Hooks.Failure,
"Success": l.Hooks.Success,
}
for hook, commands := range hooks {
if len(commands) > 0 {
tmp += "\n\t" + hook
for _, cmd := range commands {
tmp += colors.Faint.Sprintf("\n\t ▶ %s", cmd)
}
}
}
if tmp != "" {
colors.PrintDescription("Hooks", tmp)
}
if len(l.Options) > 0 {
tmp = ""
for t, options := range l.Options {
tmp += "\n\t" + t
for option, values := range options {
for _, value := range values {
tmp += colors.Faint.Sprintf("\n\t ✧ --%s=%s", option, value)
}
}
}
colors.PrintDescription("Options", tmp)
}
}
// Backends
for name, b := range c.Backends {
colors.PrimaryPrint("Backend: \"%s\"", name)
colors.PrintDescription("Type", b.Type)
colors.PrintDescription("Path", b.Path)
if len(b.Env) > 0 {
tmp := ""
for option, value := range b.Env {
tmp += fmt.Sprintf("\n\t%s %s %s", colors.Success.Sprint("✧"), strings.ToUpper(option), colors.Faint.Sprint(value))
}
colors.PrintDescription("Env", tmp)
}
}
}
func CheckConfig() error {
c := GetConfig()
if c == nil {
return fmt.Errorf("config could not be loaded/found")
}
if !CheckIfResticIsCallable() {
return fmt.Errorf(`%s was not found. Install either with "autorestic install" or manually`, RESTIC_BIN)
}
for name, backend := range c.Backends {
backend.name = name
if err := backend.validate(); err != nil {
return err
}
}
for name, location := range c.Locations {
location.name = name
if err := location.validate(c); err != nil {
return err
}
}
return nil
}
func GetAllOrSelected(cmd *cobra.Command, backends bool) ([]string, error) {
var list []string
if backends {
for name := range config.Backends {
list = append(list, name)
}
} else {
for name := range config.Locations {
list = append(list, name)
}
}
all, _ := cmd.Flags().GetBool("all")
if all {
return list, nil
}
var selected []string
if backends {
selected, _ = cmd.Flags().GetStringSlice("backend")
} else {
selected, _ = cmd.Flags().GetStringSlice("location")
}
for _, s := range selected {
found := false
for _, l := range list {
if l == s {
found = true
break
}
}
if !found {
if backends {
return nil, fmt.Errorf("invalid backend \"%s\"", s)
} else {
return nil, fmt.Errorf("invalid location \"%s\"", s)
}
}
}
if len(selected) == 0 {
return selected, fmt.Errorf("nothing selected, aborting")
}
return selected, nil
}
func AddFlagsToCommand(cmd *cobra.Command, backend bool) {
var usage string
if backend {
usage = "all backends"
} else {
usage = "all locations"
}
cmd.PersistentFlags().BoolP("all", "a", false, usage)
if backend {
cmd.PersistentFlags().StringSliceP("backend", "b", []string{}, "select backends")
} else {
cmd.PersistentFlags().StringSliceP("location", "l", []string{}, "select locations")
}
}
func (c *Config) SaveConfig() error {
file := viper.ConfigFileUsed()
if err := CopyFile(file, file+".old"); err != nil {
return err
}
colors.Secondary.Println("Saved a backup copy of your file next the the original.")
viper.Set("backends", c.Backends)
viper.Set("locations", c.Locations)
return viper.WriteConfig()
}

12
internal/cron.go Normal file
View File

@@ -0,0 +1,12 @@
package internal
func RunCron() error {
c := GetConfig()
for name, l := range c.Locations {
l.name = name
if err := l.RunCron(); err != nil {
return err
}
}
return nil
}

337
internal/location.go Normal file
View File

@@ -0,0 +1,337 @@
package internal
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/cupcakearmy/autorestic/internal/lock"
"github.com/robfig/cron"
)
type LocationType string
const (
TypeLocal LocationType = "local"
TypeVolume LocationType = "volume"
VolumePrefix string = "volume:"
)
type HookArray = []string
type Hooks struct {
Before HookArray `yaml:"before,omitempty"`
After HookArray `yaml:"after,omitempty"`
Success HookArray `yaml:"success,omitempty"`
Failure HookArray `yaml:"failure,omitempty"`
}
type Options map[string]map[string][]string
type Location struct {
name string `yaml:",omitempty"`
From string `yaml:"from,omitempty"`
To []string `yaml:"to,omitempty"`
Hooks Hooks `yaml:"hooks,omitempty"`
Cron string `yaml:"cron,omitempty"`
Options Options `yaml:"options,omitempty"`
}
func GetLocation(name string) (Location, bool) {
l, ok := GetConfig().Locations[name]
l.name = name
return l, ok
}
func (l Location) validate(c *Config) error {
if l.From == "" {
return fmt.Errorf(`Location "%s" is missing "from" key`, l.name)
}
if l.getType() == TypeLocal {
if from, err := GetPathRelativeToConfig(l.From); err != nil {
return err
} else {
if stat, err := os.Stat(from); err != nil {
return err
} else {
if !stat.IsDir() {
return fmt.Errorf("\"%s\" is not valid directory for location \"%s\"", from, l.name)
}
}
}
}
if len(l.To) == 0 {
return fmt.Errorf(`Location "%s" has no "to" targets`, l.name)
}
// Check if backends are all valid
for _, to := range l.To {
_, ok := GetBackend(to)
if !ok {
return fmt.Errorf("invalid backend `%s`", to)
}
}
return nil
}
func (l Location) getOptions(key string) []string {
var options []string
saved := l.Options[key]
for k, values := range saved {
for _, value := range values {
options = append(options, fmt.Sprintf("--%s", k), value)
}
}
return options
}
func ExecuteHooks(commands []string, options ExecuteOptions) error {
if len(commands) == 0 {
return nil
}
colors.Secondary.Println("\nRunning hooks")
for _, command := range commands {
colors.Body.Println("> " + command)
out, err := ExecuteCommand(options, "-c", command)
if err != nil {
colors.Error.Println(out)
return err
}
if VERBOSE {
colors.Faint.Println(out)
}
}
colors.Body.Println("")
return nil
}
func (l Location) getType() LocationType {
if strings.HasPrefix(l.From, VolumePrefix) {
return TypeVolume
}
return TypeLocal
}
func (l Location) getVolumeName() string {
return strings.TrimPrefix(l.From, VolumePrefix)
}
func (l Location) getPath() (string, error) {
t := l.getType()
switch t {
case TypeLocal:
if path, err := GetPathRelativeToConfig(l.From); err != nil {
return "", err
} else {
return path, nil
}
case TypeVolume:
return "/volume/" + l.name + "/" + l.getVolumeName(), nil
}
return "", fmt.Errorf("could not get path for location \"%s\"", l.name)
}
func (l Location) Backup(cron bool) []error {
var errors []error
colors.PrimaryPrint(" Backing up location \"%s\" ", l.name)
t := l.getType()
options := ExecuteOptions{
Command: "bash",
}
if t == TypeLocal {
dir, _ := GetPathRelativeToConfig(l.From)
options.Dir = dir
}
// Hooks
if err := ExecuteHooks(l.Hooks.Before, options); err != nil {
errors = append(errors, err)
goto after
}
// Backup
for _, to := range l.To {
backend, _ := GetBackend(to)
colors.Secondary.Printf("Backend: %s\n", backend.name)
env, err := backend.getEnv()
if err != nil {
errors = append(errors, err)
continue
}
flags := l.getOptions("backup")
cmd := []string{"backup"}
cmd = append(cmd, flags...)
if cron {
cmd = append(cmd, "--tag", "cron")
}
cmd = append(cmd, ".")
backupOptions := ExecuteOptions{
Dir: options.Dir,
Envs: env,
}
var out string
switch t {
case TypeLocal:
out, err = ExecuteResticCommand(backupOptions, cmd...)
case TypeVolume:
out, err = backend.ExecDocker(l, cmd)
}
if err != nil {
colors.Error.Println(out)
errors = append(errors, err)
continue
}
if VERBOSE {
colors.Faint.Println(out)
}
}
// After hooks
if err := ExecuteHooks(l.Hooks.After, options); err != nil {
errors = append(errors, err)
}
after:
var commands []string
if len(errors) > 0 {
commands = l.Hooks.Failure
} else {
commands = l.Hooks.Success
}
if err := ExecuteHooks(commands, options); err != nil {
errors = append(errors, err)
}
colors.Success.Println("Done")
return errors
}
func (l Location) Forget(prune bool, dry bool) error {
colors.PrimaryPrint("Forgetting for location \"%s\"", l.name)
path, err := l.getPath()
if err != nil {
return err
}
for _, to := range l.To {
backend, _ := GetBackend(to)
colors.Secondary.Printf("For backend \"%s\"\n", backend.name)
env, err := backend.getEnv()
if err != nil {
return nil
}
options := ExecuteOptions{
Envs: env,
}
flags := l.getOptions("forget")
cmd := []string{"forget", "--path", path}
if prune {
cmd = append(cmd, "--prune")
}
if dry {
cmd = append(cmd, "--dry-run")
}
cmd = append(cmd, flags...)
out, err := ExecuteResticCommand(options, cmd...)
if VERBOSE {
colors.Faint.Println(out)
}
if err != nil {
return err
}
}
colors.Success.Println("Done")
return nil
}
func (l Location) hasBackend(backend string) bool {
for _, b := range l.To {
if b == backend {
return true
}
}
return false
}
func (l Location) Restore(to, from string, force bool) error {
if from == "" {
from = l.To[0]
} else if !l.hasBackend(from) {
return fmt.Errorf("invalid backend: \"%s\"", from)
}
to, err := filepath.Abs(to)
if err != nil {
return err
}
colors.PrimaryPrint("Restoring location \"%s\"", l.name)
backend, _ := GetBackend(from)
path, err := l.getPath()
if err != nil {
return nil
}
colors.Secondary.Println("Restoring lastest snapshot")
colors.Body.Printf("%s → %s.\n", from, path)
switch l.getType() {
case TypeLocal:
// Check if target is empty
if !force {
notEmptyError := fmt.Errorf("target %s is not empty", to)
_, err = os.Stat(to)
if err == nil {
files, err := ioutil.ReadDir(to)
if err != nil {
return err
}
if len(files) > 0 {
return notEmptyError
}
} else {
if !os.IsNotExist(err) {
return err
}
}
}
err = backend.Exec([]string{"restore", "--target", to, "--path", path, "latest"})
case TypeVolume:
_, err = backend.ExecDocker(l, []string{"restore", "--target", ".", "--path", path, "latest"})
}
if err != nil {
return err
}
colors.Success.Println("Done")
return nil
}
func (l Location) RunCron() error {
if l.Cron == "" {
return nil
}
schedule, err := cron.ParseStandard(l.Cron)
if err != nil {
return err
}
last := time.Unix(lock.GetCron(l.name), 0)
next := schedule.Next(last)
now := time.Now()
if now.After(next) {
lock.SetCron(l.name, now.Unix())
l.Backup(true)
} else {
if !CRON_LEAN {
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)
}
}
return nil
}

64
internal/lock/lock.go Normal file
View File

@@ -0,0 +1,64 @@
package lock
import (
"os"
"path"
"sync"
"github.com/cupcakearmy/autorestic/internal/colors"
"github.com/spf13/viper"
)
var lock *viper.Viper
var file string
var once sync.Once
func getLock() *viper.Viper {
if lock == nil {
once.Do(func() {
lock = viper.New()
lock.SetDefault("running", false)
p := path.Dir(viper.ConfigFileUsed())
file = path.Join(p, ".autorestic.lock.yml")
lock.SetConfigFile(file)
lock.SetConfigType("yml")
lock.ReadInConfig()
})
}
return lock
}
func setLock(locked bool) error {
lock := getLock()
if locked {
running := lock.GetBool("running")
if running {
colors.Error.Println("an instance is already running. exiting")
os.Exit(1)
}
}
lock.Set("running", locked)
if err := lock.WriteConfigAs(file); err != nil {
return err
}
return nil
}
func GetCron(location string) int64 {
lock := getLock()
return lock.GetInt64("cron." + location)
}
func SetCron(location string, value int64) {
lock.Set("cron."+location, value)
lock.WriteConfigAs(file)
}
func Lock() error {
return setLock(true)
}
func Unlock() error {
return setLock(false)
}

20
internal/terminal/main.go Normal file
View File

@@ -0,0 +1,20 @@
package terminal
import (
tm "github.com/buger/goterm"
)
func Clear() {
tm.Clear()
}
func Append(line string) {
tm.Println(line)
tm.Flush()
}
func Replace(line string) {
tm.MoveCursorUp(1)
tm.Print("\033[K")
Append(line)
}

76
internal/utils.go Normal file
View File

@@ -0,0 +1,76 @@
package internal
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"github.com/cupcakearmy/autorestic/internal/colors"
)
var RESTIC_BIN string
func CheckIfCommandIsCallable(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func CheckIfResticIsCallable() bool {
return CheckIfCommandIsCallable(RESTIC_BIN)
}
type ExecuteOptions struct {
Command string
Envs map[string]string
Dir string
}
func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) {
cmd := exec.Command(options.Command, args...)
env := os.Environ()
for k, v := range options.Envs {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
cmd.Env = env
cmd.Dir = options.Dir
if VERBOSE {
colors.Faint.Printf("> Executing: %s\n", cmd)
}
var out bytes.Buffer
var error bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &error
err := cmd.Run()
if err != nil {
return error.String(), err
}
return out.String(), nil
}
func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error) {
options.Command = RESTIC_BIN
return ExecuteCommand(options, args...)
}
func CopyFile(from, to string) error {
original, err := os.Open("original.txt")
if err != nil {
return nil
}
defer original.Close()
new, err := os.Create("new.txt")
if err != nil {
return nil
}
defer new.Close()
if _, err := io.Copy(new, original); err != nil {
return err
}
return nil
}

42
main.go Normal file
View File

@@ -0,0 +1,42 @@
/*
Copyright © 2021 NAME HERE <EMAIL ADDRESS>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/cupcakearmy/autorestic/cmd"
"github.com/cupcakearmy/autorestic/internal/lock"
)
func handleCtrlC() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
sig := <-c
fmt.Println("Signal:", sig)
lock.Unlock()
os.Exit(0)
}()
}
func main() {
handleCtrlC()
cmd.Execute()
}

View File

@@ -1,25 +0,0 @@
{
"private": true,
"scripts": {
"build": "tsc",
"build:watch": "tsc -w",
"dev": "tsnd --no-notify --respawn ./src/autorestic.ts",
"bin": "yarn run build && pkg lib/autorestic.js --targets latest-macos-x64,latest-linux-x64 --out-path bin"
},
"devDependencies": {
"@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.7"
},
"dependencies": {
"axios": "^0.19.0",
"clitastic": "0.0.1",
"colors": "^1.3.3",
"js-yaml": "^3.13.1",
"minimist": "^1.2.0",
"uhrwerk": "^1.0.0"
}
}

View File

@@ -1,46 +0,0 @@
import 'colors'
import minimist from 'minimist'
import { init } from './config'
import handlers, { error, help } from './handlers'
process.on('uncaughtException', err => {
console.log(err.message)
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.13'
export const INSTALL_DIR = '/usr/local/bin'
export const VERBOSE = flags.verbose
export const config = init()
async function main() {
if (commands.length < 1) 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))

View File

@@ -1,67 +0,0 @@
import { Writer } from 'clitastic'
import { config, VERBOSE } from './autorestic'
import { Backend, Backends, Locations } from './types'
import { exec, ConfigError, pathRelativeToConfigFile } 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':
return `${backend.type}:${backend.path}`
case 'sftp':
case 'rest':
throw new Error(`Unsupported backend type: "${backend.type}"`)
default:
throw new Error(`Unknown backend type.`)
}
}
export const getEnvFromBackend = (backend: Backend) => {
const { type, path, key, ...rest } = backend
return {
RESTIC_PASSWORD: key,
RESTIC_REPOSITORY: getPathFromBackend(backend),
...rest,
}
}
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... ⏳')
const env = getEnvFromBackend(backend)
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 (VERBOSE && out.length > 0) console.log(out)
writer.done(name.blue + ' : ' + 'Done ✓'.green)
}
export const checkAndConfigureBackends = (backends?: Backends) => {
if (!backends) {
if (!config) throw ConfigError
backends = config.backends
}
console.log('\nConfiguring Backends'.grey.underline)
for (const [name, backend] of Object.entries(backends))
checkAndConfigureBackend(name, backend)
}

View File

@@ -1,115 +0,0 @@
import { Writer } from 'clitastic'
import { mkdirSync } from 'fs'
import { config, VERBOSE } from './autorestic'
import { getEnvFromBackend } from './backend'
import { LocationFromPrefixes } from './config'
import { Locations, Location, Backend } from './types'
import {
exec,
ConfigError,
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 { 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)
}
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}`)
}
}
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... ⏳')
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)
}
}
export const backupAll = (locations?: Locations) => {
if (!locations) {
if (!config) throw ConfigError
locations = config.locations
}
console.log('\nBacking Up'.underline.grey)
for (const [name, location] of Object.entries(locations))
backupLocation(name, location)
}

View File

@@ -1,97 +0,0 @@
import { readFileSync, writeFileSync, statSync } from 'fs'
import { resolve } from 'path'
import { homedir } from 'os'
import yaml from 'js-yaml'
import { flags } from './autorestic'
import { Backend, Config } from './types'
import { makeArrayIfIsNot, makeObjectKeysLowercase, rand } from './utils'
export enum LocationFromPrefixes {
Filesystem,
DockerVolume
}
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`,
)
const tmp: any = {
type,
path,
key: key || rand(128),
}
for (const [key, value] of Object.entries(rest))
tmp[key.toUpperCase()] = value
config.backends[name] = tmp as Backend
}
}
export const normalizeAndCheckBackups = (config: Config) => {
config.locations = makeObjectKeysLowercase(config.locations)
const backends = Object.keys(config.backends)
const checkDestination = (backend: string, backup: string) => {
if (!backends.includes(backend))
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 t of makeArrayIfIsNot(to))
checkDestination(t, 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 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)
writeFileSync(CONFIG_FILE, yaml.safeDump(raw))
return raw
}

View File

@@ -1,78 +0,0 @@
import { Writer } from 'clitastic'
import { config, VERBOSE } from './autorestic'
import { getEnvFromBackend } from './backend'
import { LocationFromPrefixes } from './config'
import { Locations, Location, Flags } from './types'
import {
exec,
ConfigError,
pathRelativeToConfigFile,
getFlagsFromLocation,
makeArrayIfIsNot,
fill, decodeLocationFromPrefix, getPathFromVolume,
} 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 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
}
}
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)
}

View File

@@ -1,250 +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, getBackendsFromLocations, getEnvFromBackend } from './backend'
import { backupAll } from './backup'
import { forgetAll } from './forget'
import showAll from './info'
import { restoreSingle } from './restore'
import { Backends, Flags, Locations } from './types'
import {
checkIfCommandIsAvailable,
checkIfResticIsAvailable,
downloadFile,
exec,
filterObjectByKey,
ConfigError, makeArrayIfIsNot,
} from './utils'
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',
)
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 (!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',
)
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) {
if (!config) throw ConfigError
checkIfResticIsAvailable()
const locations: Locations = parseLocations(flags)
checkAndConfigureBackends(
filterObjectByKey(config.backends, getBackendsFromLocations(locations)),
)
backupAll(locations)
console.log('\nFinished!'.underline + ' 🎉')
},
restore(args, flags) {
if (!config) throw ConfigError
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) {
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)
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/fjVbg' +
'\n',
)
}
export const error = () => {
help()
console.log(
`Invalid Command:`.red.underline,
`${process.argv.slice(2).join(' ')}`,
)
}
export default handlers

View File

@@ -1,28 +0,0 @@
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

View File

@@ -1,81 +0,0 @@
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,
getPathFromVolume,
} 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 = getPathFromVolume(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 + `Restoring...`)
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 🎉')
}

View File

@@ -1,94 +0,0 @@
export type StringOrArray = string | string[]
// BACKENDS
type BackendLocal = {
type: 'local'
key: string
path: string
}
type BackendSFTP = {
type: 'sftp'
key: string
path: string
password?: string
}
type BackendREST = {
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 BackendB2 = {
type: 'b2'
key: string
path: string
b2_account_id: string
b2_account_key: string
}
type BackendAzure = {
type: 'azure'
key: string
path: string
azure_account_name: string
azure_account_key: string
}
type BackendGS = {
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 Backends = { [name: string]: Backend }
// LOCATIONS
export type Location = {
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
}
export type Flags = { [arg: string]: any }

View File

@@ -1,200 +0,0 @@
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, LocationFromPrefixes } from './config'
import { Location } from './types'
export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyncOptions = {}) => {
const { stdout, stderr, status } = spawnSync(command, args, {
...rest,
env: {
...process.env,
...env,
},
})
const out = stdout && stdout.toString().trim()
const err = stderr && stderr.toString().trim()
return { out, err, status }
}
export const execPlain = (command: string, opt: SpawnSyncOptions = {}) => {
const split = command.split(' ')
if (split.length < 1) throw new Error(`The command ${command} is not valid`.red)
return exec(split[0], split.slice(1), opt)
}
export const checkIfResticIsAvailable = () =>
checkIfCommandIsAvailable(
'restic',
'restic is not installed'.red +
'\nEither run ' + 'autorestic install'.green +
'\nOr go to https://restic.readthedocs.io/en/latest/020_installation.html#stable-releases',
)
export const checkIfCommandIsAvailable = (cmd: string, errorMsg?: string) => {
if (spawnSync(cmd).error)
throw new Error(errorMsg ? errorMsg : `"${cmd}" is not installed`.red)
}
export const makeObjectKeysLowercase = (object: Object): any =>
Object.fromEntries(
Object.entries(object).map(([key, value]) => [key.toLowerCase(), value]),
)
export function rand(length = 32): string {
return randomBytes(length / 2).toString('hex')
}
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 downloadFile = async (url: string, to: string) =>
new Promise<void>(async res => {
const { data: file } = await axios({
method: 'get',
url: url,
responseType: 'stream',
})
const tmp = join(tmpdir(), rand(64))
const stream = createWriteStream(tmp)
const writer = file.pipe(stream)
writer.on('close', () => {
stream.close()
try {
// Delete file if already exists. Needed if the binary wants to replace itself.
// Unix does not allow to overwrite a file that is being executed, but you can remove it and save other one at its place
unlinkSync(to)
} catch {
}
renameSync(tmp, to)
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 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 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)) {
const stringValue = String(value)
const resolvedTilde = resolveTildePath(stringValue)
flags = [...flags, `--${String(flag)}`, resolvedTilde === null ? stringValue : resolvedTilde]
}
return flags
}
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
}
}
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 getPathFromVolume = (volume: string) => pathRelativeToConfigFile(hash(volume))
export const checkIfDockerVolumeExistsOrFail = (volume: string) => {
const cmd = exec('docker', [
'volume', 'inspect', volume,
])
if (cmd.err.length > 0)
throw new Error('Volume not found')
}

View File

@@ -1,9 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"outDir": "./lib",
"strict": true,
"esModuleInterop": true
}
}