mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-04-03 16:07:08 +00:00
Compare commits
540 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
bb29a98614 | ||
|
39f4f87ce3 | ||
|
bd36bbe429 | ||
|
48fa20b482 | ||
|
f7d28b486c | ||
|
6895df1c83 | ||
|
8a773856de | ||
|
41e4e4a5f3 | ||
|
6424c64304 | ||
|
d39dafaef1 | ||
|
5a0f7e94f4 | ||
|
6a60d02759 | ||
1f7240c6a0 | |||
592d1093ac | |||
83481072bb | |||
|
776638e6fe | ||
|
bc74d3f13e | ||
|
dd01e97cf3 | ||
|
aabb0989c6 | ||
62a81d1420 | |||
e4b33cad1f | |||
|
a82273ec13 | ||
|
2418da5636 | ||
|
7508df7d66 | ||
6e34196220 | |||
|
dc56911a45 | ||
edb3ba35d8 | |||
|
12f6143bb4 | ||
|
a6bf1d1408 | ||
|
13aa560fda | ||
|
bbb1c85cad | ||
|
4cc44315ab | ||
|
b3440cd87c | ||
|
4848702929 | ||
b5604b8b9f | |||
|
24220f6b62 | ||
ced20801c1 | |||
|
ce9140fa1e | ||
|
046c79fd15 | ||
|
f8603425d1 | ||
068121d722 | |||
8eea7d33f8 | |||
|
fc8b5fdbe2 | ||
|
f67bb7f73c | ||
|
530b1b646c | ||
|
3b57602fe8 | ||
|
045513234f | ||
|
78b0db50e0 | ||
|
62dd371d51 | ||
|
08766b75db | ||
b3b7c8df95 | |||
2c5266c9a0 | |||
087b293c39 | |||
|
112a69d743 | ||
|
37d55c691f | ||
|
715b6f791c | ||
|
38b38c6805 | ||
c82db56069 | |||
27821dc3ef | |||
866975d32d | |||
|
955ac0e323 | ||
|
1512db5b55 | ||
|
046331748c | ||
72a40eaaa2 | |||
ec61effe22 | |||
d6acab94a5 | |||
|
d0d2fcf0f2 | ||
58fd41fafb | |||
|
d0c4a32879 | ||
74979e9a2a | |||
|
3732dcf6ff | ||
|
874ed52e3b | ||
4d9a2b828e | |||
|
b830667264 | ||
|
83eeb847ac | ||
|
a89ba5a40a | ||
|
6990bf6adc | ||
|
2f407cf211 | ||
489f3078fe | |||
|
e07dd0d991 | ||
|
2b9dc9f17c | ||
|
465bc037c2 | ||
|
37a043afff | ||
|
e91b632181 | ||
|
2b30998b9a | ||
|
49b37a0a9a | ||
3bc091f826 | |||
5bcf5c9217 | |||
ff2e3714d1 | |||
2e6764223d | |||
434862ed4e | |||
2da440a1cd | |||
0f6c34dc6b | |||
3457fc01bf | |||
|
1e62f6cb05 | ||
8112aceb30 | |||
|
b4cc0f0ab1 | ||
|
a3e539b0b7 | ||
|
79164e3de0 | ||
8b74a98836 | |||
8a713e497d | |||
71601ae7a6 | |||
|
85d3f06b79 | ||
5afad86e37 | |||
ba9e090695 | |||
|
05def04770 | ||
d85470459f | |||
1f69a7974a | |||
db9f5dea66 | |||
75160d4d6a | |||
92feaef5bb | |||
2a7e233cdb | |||
a373c07fb0 | |||
ec9e2aebcd | |||
7d87160706 | |||
8e1fe6af65 | |||
|
65ba1f6ac1 | ||
|
6bf4953003 | ||
27758a03fa | |||
bbdae05199 | |||
389490c4ea | |||
21b83b4c89 | |||
982f9e0b5c | |||
0c37af5588 | |||
e3c378f2a1 | |||
3b541665ae | |||
e0b2c99ccd | |||
2b14e6b1af | |||
1810af8d02 | |||
252968e15e | |||
26de4385ea | |||
0c71bea93e | |||
3029259d82 | |||
|
389f7c25dd | ||
e055e28cfe | |||
ed3c17d678 | |||
8802b74b47 | |||
e94e7030fc | |||
c55e91b8ff | |||
170bdb81ad | |||
4126436f7f | |||
113a97c283 | |||
c250391f67 | |||
d3b4915d25 | |||
cd7a5cbc13 | |||
3dd3956d64 | |||
59035da46a | |||
90914d2078 | |||
0ae374cd45 | |||
3665cea62d | |||
cf92d400c2 | |||
696bd14ac7 | |||
a68e3e426e | |||
27e82c8529 | |||
7f9251f06b | |||
09cfa4a98e | |||
05c3458a95 | |||
2826f9586d | |||
4fe241e6f3 | |||
d0b1b86fdd | |||
14dd41d60d | |||
bcfc734cd1 | |||
6817f494ef | |||
2c46f0da0c | |||
ac756dfbde | |||
f71425be5b | |||
|
d78fbb6650 | ||
7ad6f7ce9e | |||
de663f287c | |||
439076d7ab | |||
8c30134f7c | |||
11d1da7468 | |||
92b1577634 | |||
a7944aed1f | |||
3b99e301e9 | |||
246e6fc0d8 | |||
c2f9ed9204 | |||
4055ebf8e8 | |||
6be0a80b29 | |||
efd4a7dfea | |||
440609220c | |||
fb6217d868 | |||
1628384e1f | |||
3e80e6d18e | |||
83905d2993 | |||
ddc3accb30 | |||
7874512ec0 | |||
0b5f4017e4 | |||
88198c4fcb | |||
8cd759105f | |||
6137e31c3b | |||
2789502c89 | |||
b87381cd3b | |||
048a5ffed8 | |||
02a8e461d4 | |||
a1abe13a39 | |||
ddf287f6f5 | |||
56f82ae656 | |||
95b1ca3297 | |||
|
7a8830cb2f | ||
959d19cbdb | |||
9dc7763445 | |||
|
50984f6771 | ||
e80f200873 | |||
86ae70672a | |||
5c0788900f | |||
20334a7e83 | |||
7bebd04482 | |||
b9b8857bf4 | |||
dd6e618161 | |||
a4b54f9f64 | |||
c2e88193cd | |||
a2ef69d96d | |||
|
d45949b028 | ||
77d47cc697 | |||
a3239c0f3b | |||
90cd3171e5 | |||
61673bd88b | |||
41736ea3c4 | |||
a7779e04bd | |||
|
1326e7e53c | ||
478e193d78 | |||
11d4c67dce | |||
1643309957 | |||
e05386b0b5 | |||
aebaf0a225 | |||
c090013bf5 | |||
88c6949208 | |||
9256cdc38c | |||
a8c611e8ce | |||
d4522c7ffe | |||
21185d894e | |||
b119fc7ea5 | |||
|
73f4bcfd48 | ||
|
3567319314 | ||
|
bf19c983d1 | ||
|
12e4de110b | ||
c9f425ef64 | |||
974f555dff | |||
1688c1f3c3 | |||
29e46d3b5c | |||
0335abb669 | |||
b2f9b9a54e | |||
2b13a6e13d | |||
cc293ea256 | |||
a4ddd5bbcb | |||
7cbf43b75d | |||
bae77c4673 | |||
12adeb2b06 | |||
37b26dfc31 | |||
c1795b2acc | |||
b8d12e518c | |||
50060cf539 | |||
c33aac42dc | |||
c359053e0e | |||
c16340ab26 | |||
edc85c4ac3 | |||
68682777f2 | |||
|
b6c7922df5 | ||
|
991b8bec22 | ||
bbc32568ad | |||
f3c038c716 | |||
59612a97b6 | |||
33319a00ef | |||
8eb14ea14f | |||
70eb9e441f | |||
be25af2d76 | |||
1c436369f0 | |||
6efcce07b7 | |||
|
dc6dd2e712 | ||
|
68628d3776 | ||
40988ef3b4 | |||
fad33fcdaa | |||
|
8cf8a77558 | ||
36998cfd3b | |||
cf03562ea2 | |||
188560395d | |||
bacbd0f806 | |||
93bf0388a4 | |||
ec8fdbd135 | |||
420934489c | |||
|
2ba767c8c3 | ||
b489c662c7 | |||
6862529a89 | |||
aa96a95600 | |||
89e32c298c | |||
873170c6d1 | |||
ea82fea8e1 | |||
a35edcaea5 | |||
86d44eafad | |||
e927fd5a64 | |||
d5e13d4e27 | |||
824c90904c | |||
58fb5e073a | |||
541a7c2a72 | |||
e671780e54 | |||
a5471efa21 | |||
769ae4c566 | |||
f2934f60ae | |||
f6bc91e9b5 | |||
fb073627a8 | |||
976a3beab5 | |||
ec2377287e | |||
7b7eec2dba | |||
33be94ba55 | |||
8f7513b103 | |||
cf417e360d | |||
b5caad2d70 | |||
2e594cade6 | |||
276c424106 | |||
b314912821 | |||
b1054f3512 | |||
1bbd3879fe | |||
aa0b81023f | |||
09e0ee4dec | |||
952334fecb | |||
a45eee6009 | |||
a658b6bb21 | |||
8e13e7bc40 | |||
6449b8999f | |||
7629626ae0 | |||
190eca6f6e | |||
da6d9c53aa | |||
604354741e | |||
3ccaee4066 | |||
640b60c47f | |||
d293e93fa8 | |||
03ca0c8677 | |||
19e75c1dad | |||
6e25b90915 | |||
8a1fe41825 | |||
5d92b5bcc1 | |||
05be58a3a7 | |||
9ba58ee7f8 | |||
335724cce7 | |||
43244302be | |||
03cbbfd91c | |||
805bed7db1 | |||
6c59aa25db | |||
ff648f0017 | |||
c79b45308b | |||
43eabdb204 | |||
0ead9e0da1 | |||
d49e0d3836 | |||
2008ba2771 | |||
1f6c13a595 | |||
8e9b9dcebf | |||
fde4edc05f | |||
7e6cc7bb32 | |||
878a7bd752 | |||
f6860115a3 | |||
f43e73ce41 | |||
6b4277b57b | |||
d4b8a7223f | |||
cfcc010bc5 | |||
1fd009b819 | |||
91e902d7ef | |||
b690c1c3a9 | |||
b025aeeeab | |||
01cafb9436 | |||
6a2c34df48 | |||
88e47efc68 | |||
60d7e0b561 | |||
e9a7e03af7 | |||
d959a50b5a | |||
b5bc1a4cee | |||
|
e61c940d23 | ||
7fb0fbd467 | |||
cbc1b33750 | |||
e7be01da37 | |||
|
309073fe4d | ||
|
1243721a7e | ||
ebb934d1c5 | |||
8417a6e0aa | |||
2b5efd0499 | |||
af6e715b27 | |||
9484299cd9 | |||
ef93fef36a | |||
995f2250a1 | |||
3498e6da7b | |||
7128983581 | |||
158407927a | |||
ea8bf83799 | |||
e8a8ccccd0 | |||
b332897713 | |||
369037ea77 | |||
|
52cd6fe3fd | ||
|
b6f7ef6577 | ||
1a891fffbd | |||
ed68160a9a | |||
20ecd21928 | |||
df6fd75ca4 | |||
c918fb6ded | |||
2bc8fe4ee7 | |||
757a134362 | |||
6bb36cbde0 | |||
5c0b67b7fb | |||
1520e10a59 | |||
f9c645120b | |||
8fd0240929 | |||
da7b70c7a7 | |||
c138ac9882 | |||
af04f1cded | |||
3a43b2b113 | |||
885c7ca778 | |||
44a48aab9b | |||
f5f550f5b1 | |||
f1d5a6b5dd | |||
734c962006 | |||
d20a2fda68 | |||
8dcd0b1f12 | |||
9acb6296e4 | |||
82f6942ff1 | |||
22f5f61ee0 | |||
ddce8bf8a7 | |||
abeaacf182 | |||
db436587ee | |||
32837e7e45 | |||
500abfbd27 | |||
4ee54110a6 | |||
92e5071343 | |||
7e577c439a | |||
bc36a39de4 | |||
9e6b393e62 | |||
de34396b93 | |||
ebbe10608a | |||
8a34270934 | |||
e459e393a9 | |||
b1a3074f33 | |||
ae63d8b12e | |||
7aa937dd41 | |||
|
37361727ba | ||
f1874438e5 | |||
|
066342a7b7 | ||
|
f620bb1764 | ||
|
e3506e44b5 | ||
|
f65a83991b | ||
f10b8c7990 | |||
|
a8af085d9c | ||
fa89d2941f | |||
|
bcabd467c9 | ||
|
005072b90f | ||
|
d13d4f7cf1 | ||
330e3254f7 | |||
38763ed919 | |||
|
886b6362cd | ||
|
9ece1d867d | ||
|
485ada6599 | ||
|
e80db74af4 | ||
|
2fd9e2dd22 | ||
0c654eacf1 | |||
|
8fdf5188ff | ||
|
22d93f0b9c | ||
|
f940f23338 | ||
|
678aa96c06 | ||
|
e51eacf13c | ||
12d2e010bb | |||
e25e65e052 | |||
4491cfd536 | |||
d0e82b47e1 | |||
|
90f9a998e8 | ||
|
b40adcae1f | ||
|
ad5afab355 | ||
|
5b0011330c | ||
fd2fd91635 | |||
9c09ce1d79 | |||
c2f6f51789 | |||
|
f09cf90653 | ||
|
d352aced37 | ||
|
563d4ffb96 | ||
|
1c6a061dd1 | ||
|
504ad639ab | ||
f7a15c6d86 | |||
|
2f0092befe | ||
|
1026e68b68 | ||
2389c59aa9 | |||
087aeaf578 | |||
3b7062f733 | |||
|
96b63c744b | ||
|
9669b70e20 | ||
|
bcb081234c | ||
|
336f44e9dc | ||
|
d0cda7f1d5 | ||
|
a8f4c23254 | ||
|
1c9f6d7d91 | ||
|
18c3f4a06f | ||
632062a23f | |||
3d1d7ba256 | |||
|
417c54db4d | ||
|
a9696bbc0c | ||
|
45f7506478 | ||
|
d7cdeafe60 | ||
|
cf09cdbb30 | ||
|
88059fe405 | ||
|
cdf18430b6 | ||
|
352754dad9 | ||
|
b68dc75053 | ||
|
6a055d3114 | ||
|
b5daff07eb | ||
b2d01d77d9 | |||
f41c042fce | |||
a81498ac42 | |||
1731ee30b3 | |||
1f4f1a1855 | |||
13cb764067 | |||
8058f37368 | |||
|
57ffa1e3fa | ||
|
671542cd30 | ||
|
322df9f0bd | ||
|
652158d1ed | ||
|
06ce8180fb | ||
81f513d77b | |||
e32521e6ea | |||
f5c5b39b30 | |||
e016c8defc | |||
a2e0a0c9cc | |||
|
f9b04ea342 | ||
770c9dd7d4 | |||
|
851bbe5776 | ||
|
8fb6bdb3c6 | ||
|
47f5d91e89 | ||
|
de27034b94 | ||
|
9dafe9d36a | ||
|
d47e7d0912 | ||
|
e47d6be854 | ||
|
993fe072e2 | ||
|
3d1e28e574 | ||
|
3c0ebdfb4a | ||
|
2653633c91 | ||
|
6a17444c4c | ||
|
9538881f9f | ||
|
edddcebcea | ||
|
793ab1c6fe | ||
|
9832eeab22 | ||
|
b1f2678dc1 | ||
|
64f2eaf16b | ||
|
b1b12f4592 | ||
|
24a364ce08 | ||
|
d59362e82c | ||
|
0802dedb47 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
*
|
||||
!**/*.go
|
||||
!build
|
||||
!cmd
|
||||
!internal
|
||||
!go.*
|
31
.drone.yml
Normal file
31
.drone.yml
Normal 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
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.afdesign filter=lfs diff=lfs merge=lfs -text
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: cupcakearmy
|
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: Questions & Help
|
||||
url: https://github.com/cupcakearmy/autorestic/discussions
|
||||
about: Please ask and answer questions here.
|
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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. -->
|
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
BIN
.github/logo.afdesign
(Stored with Git LFS)
vendored
Normal file
BIN
.github/logo.afdesign
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
.github/logo.png
vendored
Normal file
BIN
.github/logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
51
.github/workflows/build.yml
vendored
Normal file
51
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
name: Main
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Docker Labels
|
||||
id: meta
|
||||
uses: crazy-max/ghaction-docker-meta@v4
|
||||
with:
|
||||
images: cupcakearmy/autorestic
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.21'
|
||||
- name: Build
|
||||
run: go run build/build.go
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: dist/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
24
.github/workflows/ci.yml
vendored
Normal file
24
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.21'
|
||||
- run: go test -v ./...
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.21'
|
||||
- run: go build -v .
|
16
.gitignore
vendored
16
.gitignore
vendored
@ -1,8 +1,12 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
# Editors
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
config.yml
|
||||
bin
|
||||
lib
|
||||
data
|
||||
# Config
|
||||
.autorestic*
|
||||
|
||||
# Build & Dev
|
||||
test
|
||||
autorestic
|
||||
data
|
||||
dist
|
282
CHANGELOG.md
Normal file
282
CHANGELOG.md
Normal file
@ -0,0 +1,282 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
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).
|
||||
|
||||
## [1.7.4] - 2023-01-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Transformer for extracting information. @11mariom
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump docker restic version.
|
||||
- Docs dependencies updated.
|
||||
|
||||
## [1.7.1] - 2022-04-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- #178 Lean flag not working properly.
|
||||
|
||||
## [1.7.0] - 2022-04-27
|
||||
|
||||
### Changed
|
||||
|
||||
- #147 Stream output instead of buffering.
|
||||
|
||||
### Fixed
|
||||
|
||||
- #184 duplicate global options.
|
||||
- #154 add docs for migration.
|
||||
- #182 fix bug with upgrading custom restic with custom path.
|
||||
|
||||
## [1.6.2] - 2022-04-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Version bump in code.
|
||||
|
||||
## [1.6.1] - 2022-04-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bump go version in docker file to 18.
|
||||
|
||||
## [1.6.0] - 2022-04-14
|
||||
|
||||
### Added
|
||||
|
||||
- support for copy command #145
|
||||
- partial restore with `--include`, `--exclude`, `--iinclude`, `--iexclude` flags #161
|
||||
- run forget automatically after backup #158
|
||||
- exit codes to hooks as env variable #142
|
||||
|
||||
### Fixed
|
||||
|
||||
- Lean flag not removing all output #178
|
||||
|
||||
## [1.5.8] - 2022-03-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Better error handling for bad config files.
|
||||
|
||||
## [1.5.7] - 2022-03-11
|
||||
|
||||
### Added
|
||||
|
||||
- SSH in docker image. @fariszr
|
||||
|
||||
### Security
|
||||
|
||||
- Updated dependencies
|
||||
|
||||
## [1.5.6] - 2022-03-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add bash in docker image for hooks. @fariszr
|
||||
|
||||
## [1.5.5] - 2022-02-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Go version was updated from `1.16` to `1.17`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Home directory was not being taken into account for loading configs.
|
||||
|
||||
## [1.5.4] - 2022-02-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Lean flag not omitting all output.
|
||||
|
||||
## [1.5.3] - 2022-02-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Error throwing not finding config even it's not being used.
|
||||
|
||||
## [1.5.2] - 2022-02-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Config loading @jjromannet
|
||||
- Making a backup of the file @jjromannet
|
||||
|
||||
## [1.5.1] - 2021-12-06
|
||||
|
||||
### Changed
|
||||
|
||||
- use official docker image instead of installing rclone every time docker is used.
|
||||
- docker docs
|
||||
|
||||
### Fixed
|
||||
|
||||
- lock file not always next to the config file.
|
||||
- update / install bugs.
|
||||
- lock docker image tag to the current autorestic version
|
||||
- better error logging
|
||||
|
||||
## [1.5.0] - 2021-11-20
|
||||
|
||||
### Added
|
||||
|
||||
- Support for multiple paths.
|
||||
- Improved error handling.
|
||||
- Allow for specific snapshot to be restored.
|
||||
- Docker image.
|
||||
|
||||
### Fixed
|
||||
|
||||
- rclone in docker volumes.
|
||||
|
||||
### Changed
|
||||
|
||||
- [Breaking Change] Declaration of docker volumes. See: https://autorestic.vercel.app/migration/1.4_1.5.
|
||||
- [Breaking Change] Hooks default executing directory now defaults to the config file directory. See: https://autorestic.vercel.app/migration/1.4_1.5.
|
||||
|
||||
## [1.4.1] - 2021-10-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Numeric values from config files not being passed to env.
|
||||
|
||||
## [1.4.0] - 2021-10-30
|
||||
|
||||
### Added
|
||||
|
||||
- Allow specify to specify a backend for location backup.
|
||||
- Global restic flags.
|
||||
- Generic ENV support for backends.
|
||||
|
||||
### Changed
|
||||
|
||||
- Install now only requires `wget`.
|
||||
- Env variable for the `KEY` has been renamed from `AUTORESTIC_[BACKEND NAME]_KEY` -> `AUTORESTIC_[BACKEND NAME]_RESTIC_PASSWORD`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Error handling during upgrade & uninstall.
|
||||
|
||||
## [1.3.0] - 2021-10-26
|
||||
|
||||
### Added
|
||||
|
||||
- Pass restic backup metadata as ENV to hooks.
|
||||
- Support for `XDG_CONFIG_HOME` and `${HOME}/.config` as default locations for `.autorestic.yaml` file.
|
||||
- Binary restic flags are now supported.
|
||||
- Pass encryption keys from env variables or files.
|
||||
|
||||
## [1.2.0] - 2021-08-05
|
||||
|
||||
### Added
|
||||
|
||||
- Community page
|
||||
- Support for yaml references and aliases.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Better verbose output for hooks.
|
||||
- Better error message for bad formatted configs.
|
||||
|
||||
## [1.1.2] - 2021-07-11
|
||||
|
||||
### Fixes
|
||||
|
||||
Don't check all backend when running `forget` or `exec` commands.
|
||||
|
||||
## [1.1.1] - 2021-05-17
|
||||
|
||||
### Added
|
||||
|
||||
- Options for backends.
|
||||
|
||||
## [1.1.0] - 2021-05-06
|
||||
|
||||
### Added
|
||||
|
||||
- use custom restic binary.
|
||||
- success & failure hooks.
|
||||
|
||||
### Fixed
|
||||
|
||||
- don't skip other locations on failure.
|
||||
|
||||
## [1.0.9] - 2021-05-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Validation for docker volumes.
|
||||
|
||||
## [1.0.8] - 2021-04-28
|
||||
|
||||
### Added
|
||||
|
||||
- `--lean` flag to cron command for less output about skipping backups.
|
||||
|
||||
### Fixed
|
||||
|
||||
- consistent lower casing in usage descriptions.
|
||||
|
||||
## [1.0.7] - 2021-04-26
|
||||
|
||||
### Added
|
||||
|
||||
- Support for `darwin/arm64` aka Apple Silicon.
|
||||
- Added support for `arm64` and `aarch64` in install scripts.
|
||||
|
||||
## [1.0.6] - 2021-04-24
|
||||
|
||||
### Added
|
||||
|
||||
- Support for rclone.
|
||||
|
||||
## [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
30
DEVELOPMENT.md
Normal 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 pre-release.
|
||||
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
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM golang:1.24-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.* .
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN go build
|
||||
|
||||
FROM restic/restic:0.17.3
|
||||
RUN apk add --no-cache rclone bash curl docker-cli
|
||||
COPY --from=builder /app/autorestic /usr/bin/autorestic
|
||||
ENTRYPOINT []
|
||||
CMD [ "autorestic" ]
|
215
LICENSE
215
LICENSE
@ -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.
|
||||
|
52
README.md
52
README.md
@ -1,2 +1,50 @@
|
||||
# autorestic
|
||||
High level CLI utility for restic
|
||||
<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>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
### 💭 Why / What?
|
||||
|
||||
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 locations that you want to backup to multiple locations. This utility is aimed at making this easier 🙂.
|
||||
|
||||
### 🌈 Features
|
||||
|
||||
- YAML config files, no CLI
|
||||
- Incremental -> Minimal space is used
|
||||
- Backup locations to multiple backends
|
||||
- Snapshot policies and pruning
|
||||
- Fully encrypted
|
||||
- Before/after backup hooks
|
||||
- Exclude pattern/files
|
||||
- Cron jobs for automatic backup
|
||||
- Backup & Restore docker volume
|
||||
- Generated completions for `[bash|zsh|fish|powershell]`
|
||||
|
||||
### ❓ Questions / Support
|
||||
|
||||
Check the [discussions page](https://github.com/cupcakearmy/autorestic/discussions) or [join on discord](https://discord.gg/wS7RpYTYd2)
|
||||
|
||||
## Contributing / Developing
|
||||
|
||||
PRs, feature requests, etc. are welcomed :)
|
||||
Have a look at [the dev docs](./DEVELOPMENT.md)
|
||||
|
132
build/build.go
Normal file
132
build/build.go
Normal file
@ -0,0 +1,132 @@
|
||||
// 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 (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
)
|
||||
|
||||
var DIR, _ = filepath.Abs("./dist")
|
||||
|
||||
var targets = map[string][]string{
|
||||
// "aix": {"ppc64"}, // Not supported by fsnotify
|
||||
"darwin": {"amd64", "arm64"},
|
||||
"freebsd": {"386", "amd64", "arm"},
|
||||
"linux": {"386", "amd64", "arm", "arm64", "ppc64le", "mips", "mipsle", "mips64", "mips64le", "s390x"},
|
||||
"netbsd": {"386", "amd64"},
|
||||
"openbsd": {"386", "amd64"},
|
||||
// "windows": {"386", "amd64"}, // Not supported by autorestic
|
||||
"solaris": {"amd64"},
|
||||
}
|
||||
|
||||
type buildOptions struct {
|
||||
Target, Arch, Version string
|
||||
}
|
||||
|
||||
const (
|
||||
CHECKSUM_MD5 = "MD5SUMS"
|
||||
CHECKSUM_SHA_1 = "SHA1SUMS"
|
||||
CHECKSUM_SHA_256 = "SHA256SUMS"
|
||||
)
|
||||
|
||||
type Checksums struct {
|
||||
filename, md5, sha1, sha256 string
|
||||
}
|
||||
|
||||
func writeChecksums(checksums *[]Checksums) {
|
||||
FILE_MD5, _ := os.OpenFile(path.Join(DIR, CHECKSUM_MD5), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
defer FILE_MD5.Close()
|
||||
FILE_SHA1, _ := os.OpenFile(path.Join(DIR, CHECKSUM_SHA_1), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
defer FILE_SHA1.Close()
|
||||
FILE_SHA256, _ := os.OpenFile(path.Join(DIR, CHECKSUM_SHA_256), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
defer FILE_SHA256.Close()
|
||||
|
||||
for _, checksum := range *checksums {
|
||||
fmt.Fprintf(FILE_MD5, "%s %s\n", checksum.md5, checksum.filename)
|
||||
fmt.Fprintf(FILE_SHA1, "%s %s\n", checksum.sha1, checksum.filename)
|
||||
fmt.Fprintf(FILE_SHA256, "%s %s\n", checksum.sha256, checksum.filename)
|
||||
}
|
||||
}
|
||||
|
||||
func build(options buildOptions, wg *sync.WaitGroup, checksums *[]Checksums) {
|
||||
defer wg.Done()
|
||||
|
||||
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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Checksum
|
||||
{
|
||||
file := out + ".bz2"
|
||||
content, _ := ioutil.ReadFile(file)
|
||||
*checksums = append(*checksums, Checksums{
|
||||
filename: path.Base(file),
|
||||
md5: fmt.Sprintf("%x", md5.Sum(content)),
|
||||
sha1: fmt.Sprintf("%x", sha1.Sum(content)),
|
||||
sha256: fmt.Sprintf("%x", sha256.Sum256(content)),
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("Built: %s\n", path.Base(out))
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.RemoveAll(DIR)
|
||||
v := internal.VERSION
|
||||
checksums := []Checksums{}
|
||||
|
||||
// Build async
|
||||
var wg sync.WaitGroup
|
||||
for target, archs := range targets {
|
||||
for _, arch := range archs {
|
||||
wg.Add(1)
|
||||
go build(buildOptions{
|
||||
Target: target,
|
||||
Arch: arch,
|
||||
Version: v,
|
||||
}, &wg, &checksums)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
writeChecksums(&checksums)
|
||||
}
|
50
cmd/backup.go
Normal file
50
cmd/backup.go
Normal file
@ -0,0 +1,50 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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) {
|
||||
internal.GetConfig()
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
dry, _ := cmd.Flags().GetBool("dry-run")
|
||||
|
||||
selected, err := internal.GetAllOrSelected(cmd, false)
|
||||
CheckErr(err)
|
||||
|
||||
errors := 0
|
||||
for _, name := range selected {
|
||||
var splitted = strings.Split(name, "@")
|
||||
var specificBackend = ""
|
||||
if len(splitted) > 1 {
|
||||
specificBackend = splitted[1]
|
||||
}
|
||||
location, _ := internal.GetLocation(splitted[0])
|
||||
errs := location.Backup(false, dry, specificBackend)
|
||||
for _, err := range errs {
|
||||
colors.Error.Printf("%s\n\n", err)
|
||||
errors++
|
||||
}
|
||||
}
|
||||
if errors > 0 {
|
||||
CheckErr(fmt.Errorf("%d errors were found", errors))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(backupCmd)
|
||||
internal.AddFlagsToCommand(backupCmd, false)
|
||||
backupCmd.Flags().Bool("dry-run", false, "do not write changes, show what would be affected")
|
||||
}
|
27
cmd/check.go
Normal file
27
cmd/check.go
Normal file
@ -0,0 +1,27 @@
|
||||
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) {
|
||||
internal.GetConfig()
|
||||
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
70
cmd/completion.go
Normal 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)
|
||||
}
|
28
cmd/cron.go
Normal file
28
cmd/cron.go
Normal file
@ -0,0 +1,28 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||
"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.GetConfig()
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
|
||||
err = internal.RunCron()
|
||||
CheckErr(err)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(cronCmd)
|
||||
cronCmd.Flags().BoolVar(&flags.CRON_LEAN, "lean", false, "only output information about actual backups")
|
||||
}
|
47
cmd/exec.go
Normal file
47
cmd/exec.go
Normal file
@ -0,0 +1,47 @@
|
||||
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 execCmd = &cobra.Command{
|
||||
Use: "exec",
|
||||
Short: "Execute arbitrary native restic commands for given backends",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
internal.GetConfig()
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
|
||||
selected, err := internal.GetAllOrSelected(cmd, true)
|
||||
CheckErr(err)
|
||||
|
||||
var errors []error
|
||||
for _, name := range selected {
|
||||
colors.PrimaryPrint(" Executing on \"%s\" ", name)
|
||||
backend, _ := internal.GetBackend(name)
|
||||
err := backend.Exec(args)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
for _, err := range errors {
|
||||
colors.Error.Printf("%s\n\n", err)
|
||||
}
|
||||
|
||||
CheckErr(fmt.Errorf("%d errors were found", len(errors)))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(execCmd)
|
||||
internal.AddFlagsToCommand(execCmd, true)
|
||||
}
|
35
cmd/forget.go
Normal file
35
cmd/forget.go
Normal file
@ -0,0 +1,35 @@
|
||||
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) {
|
||||
internal.GetConfig()
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
|
||||
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
18
cmd/info.go
Normal 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
19
cmd/install.go
Normal 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)
|
||||
}
|
63
cmd/restore.go
Normal file
63
cmd/restore.go
Normal file
@ -0,0 +1,63 @@
|
||||
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 [snapshot id]",
|
||||
Short: "Restore backup for location",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
internal.GetConfig()
|
||||
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")
|
||||
snapshot := ""
|
||||
if len(args) > 0 {
|
||||
snapshot = args[0]
|
||||
}
|
||||
|
||||
// Get optional flags
|
||||
optional := []string{}
|
||||
for _, flag := range []string{"include", "exclude", "iinclude", "iexclude"} {
|
||||
values, err := cmd.Flags().GetStringSlice(flag)
|
||||
if err == nil {
|
||||
for _, value := range values {
|
||||
optional = append(optional, "--"+flag, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = l.Restore(target, from, force, snapshot, optional)
|
||||
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")
|
||||
|
||||
// Passed on flags
|
||||
restoreCmd.Flags().StringSliceP("include", "i", []string{}, "Include a pattern")
|
||||
restoreCmd.Flags().StringSliceP("exclude", "e", []string{}, "Exclude a pattern")
|
||||
restoreCmd.Flags().StringSlice("iinclude", []string{}, "Include a pattern, case insensitive")
|
||||
restoreCmd.Flags().StringSlice("iexclude", []string{}, "Exclude a pattern, case insensitive")
|
||||
}
|
92
cmd/root.go
Normal file
92
cmd/root.go
Normal file
@ -0,0 +1,92 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||
"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:\thttps://autorestic.vercel.app\nSupport:\thttps://discord.gg/wS7RpYTYd2",
|
||||
}
|
||||
|
||||
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(&flags.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
|
||||
rootCmd.PersistentFlags().BoolVarP(&flags.VERBOSE, "verbose", "v", false, "verbose mode")
|
||||
rootCmd.PersistentFlags().StringVar(&flags.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
|
||||
rootCmd.PersistentFlags().StringVar(&flags.DOCKER_IMAGE, "docker-image", "cupcakearmy/autorestic:"+internal.VERSION, "specify a custom docker image")
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if ci, _ := rootCmd.Flags().GetBool("ci"); ci {
|
||||
colors.DisableColors(true)
|
||||
flags.VERBOSE = true
|
||||
}
|
||||
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
viper.AutomaticEnv()
|
||||
if viper.ConfigFileUsed() == "" {
|
||||
colors.Error.Printf("cannot read config file %s\n", cfgFile)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
configPaths := getConfigPaths()
|
||||
for _, cfgPath := range configPaths {
|
||||
viper.AddConfigPath(cfgPath)
|
||||
}
|
||||
if flags.VERBOSE {
|
||||
colors.Faint.Printf("Using config paths: %s\n", strings.Join(configPaths, " "))
|
||||
}
|
||||
cfgFileName := ".autorestic"
|
||||
viper.SetConfigName(cfgFileName)
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigPaths() []string {
|
||||
result := []string{"."}
|
||||
if home, err := homedir.Dir(); err == nil {
|
||||
result = append(result, home)
|
||||
}
|
||||
|
||||
{
|
||||
xdgConfigHome, found := os.LookupEnv("XDG_CONFIG_HOME")
|
||||
if !found {
|
||||
if home, err := homedir.Dir(); err == nil {
|
||||
xdgConfigHome = filepath.Join(home, ".config")
|
||||
}
|
||||
}
|
||||
xdgConfig := filepath.Join(xdgConfigHome, "autorestic")
|
||||
result = append(result, xdgConfig)
|
||||
}
|
||||
return result
|
||||
}
|
36
cmd/root_test.go
Normal file
36
cmd/root_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
|
||||
func assertContains(t *testing.T, array []string, element string) {
|
||||
if !slices.Contains(array, element) {
|
||||
t.Errorf("Expected %s to be contained in %s", element, array)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigResolving(t *testing.T) {
|
||||
t.Run("~/.config/autorestic is used if XDG_CONFIG_HOME is not set", func(t *testing.T) {
|
||||
// Override env using testing so that env gets restored after test
|
||||
t.Setenv(xdgConfigHome, "")
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
configPaths := getConfigPaths()
|
||||
homeDir, _ := homedir.Dir()
|
||||
expectedConfigPath := filepath.Join(homeDir, ".config/autorestic")
|
||||
assertContains(t, configPaths, expectedConfigPath)
|
||||
})
|
||||
|
||||
t.Run("XDG_CONFIG_HOME is respected if set", func(t *testing.T) {
|
||||
t.Setenv(xdgConfigHome, "/foo/bar")
|
||||
|
||||
configPaths := getConfigPaths()
|
||||
assertContains(t, configPaths, filepath.Join("/", "foo", "bar", "autorestic"))
|
||||
})
|
||||
}
|
20
cmd/uninstall.go
Normal file
20
cmd/uninstall.go
Normal 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) {
|
||||
restic, _ := cmd.Flags().GetBool("restic")
|
||||
bins.Uninstall(restic)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(uninstallCmd)
|
||||
uninstallCmd.Flags().Bool("restic", false, "also uninstall restic.")
|
||||
}
|
81
cmd/unlock.go
Normal file
81
cmd/unlock.go
Normal file
@ -0,0 +1,81 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var unlockCmd = &cobra.Command{
|
||||
Use: "unlock",
|
||||
Short: "Unlock autorestic only if you are sure that no other instance is running",
|
||||
Long: `Unlock autorestic only if you are sure that no other instance is running.
|
||||
To check you can run "ps aux | grep autorestic".`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
internal.GetConfig()
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
if !force && isAutoresticRunning() {
|
||||
colors.Error.Print("Another autorestic instance is running. Are you sure you want to unlock? (yes/no): ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if strings.ToLower(response) != "yes" {
|
||||
colors.Primary.Println("Unlocking aborted.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := lock.Unlock()
|
||||
if err != nil {
|
||||
colors.Error.Println("Could not unlock:", err)
|
||||
return
|
||||
}
|
||||
|
||||
colors.Success.Println("Unlock successful")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(unlockCmd)
|
||||
unlockCmd.Flags().Bool("force", false, "force unlock")
|
||||
}
|
||||
|
||||
// isAutoresticRunning checks if autorestic is running
|
||||
// and returns true if it is.
|
||||
// It also prints the processes to stdout.
|
||||
func isAutoresticRunning() bool {
|
||||
cmd := exec.Command("sh", "-c", "ps aux | grep autorestic")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lines := strings.Split(out.String(), "\n")
|
||||
autoresticProcesses := []string{}
|
||||
currentPid := fmt.Sprint(os.Getpid())
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "autorestic") && !strings.Contains(line, "grep autorestic") && !strings.Contains(line, currentPid) {
|
||||
autoresticProcesses = append(autoresticProcesses, line)
|
||||
}
|
||||
}
|
||||
|
||||
if len(autoresticProcesses) > 0 {
|
||||
colors.Faint.Println("Found autorestic processes:")
|
||||
for _, proc := range autoresticProcesses {
|
||||
colors.Faint.Println(proc)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
21
cmd/upgrade.go
Normal file
21
cmd/upgrade.go
Normal 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) {
|
||||
restic, _ := cmd.Flags().GetBool("restic")
|
||||
err := bins.Upgrade(restic)
|
||||
CheckErr(err)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(upgradeCmd)
|
||||
upgradeCmd.Flags().Bool("restic", true, "also update restic")
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
locations:
|
||||
home:
|
||||
from: /home/myUser
|
||||
to: remote
|
||||
important:
|
||||
from: /path/to/important/stuff
|
||||
to:
|
||||
- remote
|
||||
- hdd
|
||||
backends:
|
||||
remote:
|
||||
type: b2
|
||||
path: 'myBucket:backup/home'
|
||||
B2_ACCOUNT_ID: account_id
|
||||
B2_ACCOUNT_KEY: account_key
|
||||
hdd:
|
||||
type: local
|
||||
path: /mnt/my_external_storage
|
2
docs/.gitignore
vendored
Normal file
2
docs/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.next
|
1
docs/.nvmrc
Normal file
1
docs/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v22.7.0
|
6
docs/next.config.js
Normal file
6
docs/next.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
const withNextra = require('nextra')({
|
||||
theme: 'nextra-theme-docs',
|
||||
themeConfig: './theme.config.jsx',
|
||||
})
|
||||
|
||||
module.exports = withNextra()
|
15
docs/package.json
Normal file
15
docs/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "NEXT_TELEMETRY_DISABLED=1 next build",
|
||||
"dev": "NEXT_TELEMETRY_DISABLED=1 next",
|
||||
"start": "NEXT_TELEMETRY_DISABLED=1 next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^14.2.7",
|
||||
"nextra": "^2.13.4",
|
||||
"nextra-theme-docs": "^2.13.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"packageManager": "pnpm@9.9.0"
|
||||
}
|
10
docs/pages/_meta.json
Normal file
10
docs/pages/_meta.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"index": "Home",
|
||||
"quick": "Quick Start",
|
||||
"installation": "Installation",
|
||||
"config": "Configuration",
|
||||
"location": "Locations",
|
||||
"backend": "Backend",
|
||||
"cli": "CLI",
|
||||
"migration": "Migration"
|
||||
}
|
6
docs/pages/backend/_meta.json
Normal file
6
docs/pages/backend/_meta.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"index": "Overview",
|
||||
"available": "Available backends",
|
||||
"options": "Options",
|
||||
"env": "Environment"
|
||||
}
|
85
docs/pages/backend/available.md
Normal file
85
docs/pages/backend/available.md
Normal 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:
|
||||
|
||||
> ℹ️ You can also [specify the `env` variables in a config file](/backend/env) to separate them from the config file.
|
||||
|
||||
## Local
|
||||
|
||||
```yaml
|
||||
backends:
|
||||
name-of-backend:
|
||||
type: local
|
||||
path: /data/my/backups
|
||||
```
|
||||
|
||||
## Backblaze
|
||||
|
||||
```yaml
|
||||
backends:
|
||||
name-of-backend:
|
||||
type: b2
|
||||
path: 'bucket_name'
|
||||
# Or With a path
|
||||
# path: 'bucket_name:/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: 'bucket_name: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
|
||||
```
|
65
docs/pages/backend/env.md
Normal file
65
docs/pages/backend/env.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Environment
|
||||
|
||||
> ⚠ Available since version `v1.4.0`
|
||||
|
||||
Sometimes it's favorable not having the encryption keys in the config files.
|
||||
For that `autorestic` allows passing the env variables to backend password as `ENV` variables, or through an env file.
|
||||
You can also pass whatever `env` variable to restic by prefixing it with `AUTORESTIC_[BACKEND NAME]_`.
|
||||
|
||||
> ℹ️ Env variables and file overwrite the config file in the following order:
|
||||
>
|
||||
> Env Variables > Env File (`.autorestic.env`) > Config file (`.autorestic.yaml`)
|
||||
|
||||
## Env file
|
||||
|
||||
Alternatively `autorestic` can load an env file, located next to `.autorestic.yml` called `.autorestic.env`.
|
||||
|
||||
```
|
||||
AUTORESTIC_FOO_RESTIC_PASSWORD=secret123
|
||||
```
|
||||
|
||||
### Example with repository password
|
||||
|
||||
The syntax for the `ENV` variables is as follows: `AUTORESTIC_[BACKEND NAME]_RESTIC_PASSWORD`.
|
||||
|
||||
```yaml | autorestic.yaml
|
||||
backend:
|
||||
foo:
|
||||
type: ...
|
||||
path: ...
|
||||
key: secret123 # => AUTORESTIC_FOO_RESTIC_PASSWORD=secret123
|
||||
```
|
||||
|
||||
This means we could remove `key: secret123` from `.autorestic.yaml` and execute as follows:
|
||||
|
||||
```bash
|
||||
AUTORESTIC_FOO_RESTIC_PASSWORD=secret123 autorestic backup ...
|
||||
```
|
||||
|
||||
### Example with Backblaze B2
|
||||
|
||||
```yaml | autorestic.yaml
|
||||
backends:
|
||||
bb:
|
||||
type: b2
|
||||
path: myBucket
|
||||
key: myPassword
|
||||
env:
|
||||
B2_ACCOUNT_ID: 123
|
||||
B2_ACCOUNT_KEY: 456
|
||||
```
|
||||
|
||||
You could create an `.autorestic.env` or pass the following `ENV` variables to autorestic:
|
||||
|
||||
```
|
||||
AUTORESTIC_BB_RESTIC_PASSWORD=myPassword
|
||||
AUTORESTIC_BB_B2_ACCOUNT_ID=123
|
||||
AUTORESTIC_BB_B2_ACCOUNT_KEY=456
|
||||
```
|
||||
|
||||
```yaml | autorestic.yaml
|
||||
backends:
|
||||
bb:
|
||||
type: b2
|
||||
path: myBucket
|
||||
```
|
40
docs/pages/backend/index.md
Normal file
40
docs/pages/backend/index.md
Normal file
@ -0,0 +1,40 @@
|
||||
# 💽 Backends
|
||||
|
||||
Backends are the outputs of the backup process. Each location needs at least one.
|
||||
|
||||
Note: names of backends MUST be lower case!
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
version: 2
|
||||
|
||||
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.
|
||||
|
||||
## Avoid Generating Keys
|
||||
|
||||
By default, `autorestic` will generate a key for every backend if none is defined. This is done by updating your config file with the key.
|
||||
|
||||
In cases where you want to provide the key yourself, you can ensure that `autorestic` doesn't accidentally generate one for you by setting `requireKey: true`.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
version: 2
|
||||
|
||||
backends:
|
||||
foo:
|
||||
type: local
|
||||
path: /data/my/backups
|
||||
# Alternatively, you can set the key through the `AUTORESTIC_FOO_RESTIC_PASSWORD` environment variable.
|
||||
key: ... your key here ...
|
||||
requireKey: true
|
||||
```
|
||||
|
||||
With this setting, if a key is missing, `autorestic` will crash instead of generating a new key and updating your config file.
|
17
docs/pages/backend/options.md
Normal file
17
docs/pages/backend/options.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Options
|
||||
|
||||
> ℹ️ For more detail see the [location docs](/location/options) for options, as they are the same.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
foo:
|
||||
type: ...
|
||||
path: ...
|
||||
options:
|
||||
backup:
|
||||
tag:
|
||||
- foo
|
||||
- bar
|
||||
```
|
||||
|
||||
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag abc --tag` to the native command.
|
3
docs/pages/cli/_meta.json
Normal file
3
docs/pages/cli/_meta.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"general": "General"
|
||||
}
|
26
docs/pages/cli/backup.md
Normal file
26
docs/pages/cli/backup.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Backup
|
||||
|
||||
```bash
|
||||
autorestic backup [-l, --location] [-a, --all] [--dry-run]
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.
|
||||
|
||||
|
||||
```bash
|
||||
# All
|
||||
autorestic backup -a
|
||||
|
||||
# Some
|
||||
autorestic backup -l foo -l bar
|
||||
```
|
||||
|
||||
## Specific location
|
||||
|
||||
`autorestic` also allows selecting specific backends for a location with the `location@backend` syntax.
|
||||
|
||||
```bash
|
||||
autorestic backup -l location@backend
|
||||
```
|
9
docs/pages/cli/check.md
Normal file
9
docs/pages/cli/check.md
Normal file
@ -0,0 +1,9 @@
|
||||
# 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.
|
15
docs/pages/cli/completion.md
Normal file
15
docs/pages/cli/completion.md
Normal file
@ -0,0 +1,15 @@
|
||||
# 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
|
11
docs/pages/cli/cron.md
Normal file
11
docs/pages/cli/cron.md
Normal file
@ -0,0 +1,11 @@
|
||||
# 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.
|
13
docs/pages/cli/exec.md
Normal file
13
docs/pages/cli/exec.md
Normal file
@ -0,0 +1,13 @@
|
||||
# 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.
|
11
docs/pages/cli/forget.md
Normal file
11
docs/pages/cli/forget.md
Normal file
@ -0,0 +1,11 @@
|
||||
# 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/options/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.
|
36
docs/pages/cli/general.md
Normal file
36
docs/pages/cli/general.md
Normal file
@ -0,0 +1,36 @@
|
||||
# General
|
||||
|
||||
## `-c, --config`
|
||||
|
||||
Specify the config file to be used (must use .yml as an extension).
|
||||
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
|
||||
```
|
16
docs/pages/cli/info.md
Normal file
16
docs/pages/cli/info.md
Normal file
@ -0,0 +1,16 @@
|
||||
# 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
|
||||
```
|
7
docs/pages/cli/install.md
Normal file
7
docs/pages/cli/install.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Install
|
||||
|
||||
Installs both restic and autorestic to `/usr/local/bin`.
|
||||
|
||||
```bash
|
||||
autorestic install
|
||||
```
|
17
docs/pages/cli/restore.md
Normal file
17
docs/pages/cli/restore.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Restore
|
||||
|
||||
```bash
|
||||
autorestic restore [-l, --location] [--from backend] [--to <out dir>] [-f, --force] [snapshot]
|
||||
```
|
||||
|
||||
This will restore the location to the selected target. If for one location there are more than one backends specified autorestic will take the first one. If no specific snapshot is specified `autorestic` will use `latest`.
|
||||
|
||||
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`
|
7
docs/pages/cli/uninstall.md
Normal file
7
docs/pages/cli/uninstall.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Uninstall
|
||||
|
||||
Uninstalls both restic and autorestic from `/usr/local/bin`.
|
||||
|
||||
```bash
|
||||
autorestic uninstall
|
||||
```
|
32
docs/pages/cli/unlock.md
Normal file
32
docs/pages/cli/unlock.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Unlock
|
||||
|
||||
In case autorestic throws the error message `an instance is already running. exiting`, but there is no instance running you can unlock the lock.
|
||||
|
||||
To verify that there is no instance running you can use `ps aux | grep autorestic`.
|
||||
|
||||
Example with no instance running:
|
||||
|
||||
```bash
|
||||
> ps aux | grep autorestic
|
||||
root 39260 0.0 0.0 6976 2696 pts/11 S+ 19:41 0:00 grep autorestic
|
||||
```
|
||||
|
||||
Example with an instance running:
|
||||
|
||||
```bash
|
||||
> ps aux | grep autorestic
|
||||
root 29465 0.0 0.0 1162068 7380 pts/7 Sl+ 19:28 0:00 autorestic --ci backup -a
|
||||
root 39260 0.0 0.0 6976 2696 pts/11 S+ 19:41 0:00 grep autorestic
|
||||
```
|
||||
|
||||
**If an instance is running you should not unlock as it could lead to data loss!**
|
||||
|
||||
```bash
|
||||
autorestic unlock
|
||||
```
|
||||
|
||||
Use the `--force` to prevent the confirmation prompt if an instance is running.
|
||||
|
||||
```bash
|
||||
autorestic unlock --force
|
||||
```
|
9
docs/pages/cli/upgrade.md
Normal file
9
docs/pages/cli/upgrade.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Upgrade
|
||||
|
||||
Autorestic can upgrade itself! Super handy right? Simply run autorestic upgrade and we will check for you if there are updates for restic and autorestic and install them if necessary.
|
||||
|
||||
```bash
|
||||
autorestic upgrade
|
||||
```
|
||||
|
||||
Updates both restic and autorestic automagically.
|
17
docs/pages/community.md
Normal file
17
docs/pages/community.md
Normal file
@ -0,0 +1,17 @@
|
||||
# 🏘 Community
|
||||
|
||||
## Software
|
||||
|
||||
A list of community driven projects. (No official affiliation)
|
||||
|
||||
- SystemD Units: <https://gitlab.com/py_crash/autorestic-systemd-units>
|
||||
- Docker image: <https://github.com/pascaliske/docker-autorestic>
|
||||
- Ansible Role: <https://github.com/adsanz/ansible-restic-role>
|
||||
- Ansible Role: <https://github.com/ItsNotGoodName/ansible-role-autorestic>
|
||||
- Ansible Role: <https://github.com/FuzzyMistborn/ansible-role-autorestic>
|
||||
- Ansible Role: <https://0xacab.org/varac-projects/ansible-role-autorestic>
|
||||
- Ansible Role: <https://github.com/dbrennand/ansible-role-autorestic>
|
||||
|
||||
## Writing
|
||||
|
||||
- [restic: excellent resource for local and cloud backup](https://notes.nicfab.eu/en/posts/restic/)
|
88
docs/pages/config.md
Normal file
88
docs/pages/config.md
Normal file
@ -0,0 +1,88 @@
|
||||
# 🎛 Config File
|
||||
|
||||
## Path
|
||||
|
||||
By default autorestic searches for a `.autorestic.yml` file in the current directory, your home folder and your XDG config folder (`~/.config/` by default):
|
||||
|
||||
- `./.autorestic.yml`
|
||||
- `~/.autorestic.yml`
|
||||
- `~/.config/autorestic/.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
|
||||
version: 2
|
||||
|
||||
locations:
|
||||
home:
|
||||
from: /home/me
|
||||
to: remote
|
||||
|
||||
important:
|
||||
from: /path/to/important/stuff
|
||||
to:
|
||||
- remote
|
||||
- hdd
|
||||
|
||||
backends:
|
||||
remote:
|
||||
type: b2
|
||||
path: 'myBucket:backup/home'
|
||||
env:
|
||||
B2_ACCOUNT_ID: account_id
|
||||
B2_ACCOUNT_KEY: account_key
|
||||
|
||||
hdd:
|
||||
type: local
|
||||
path: /mnt/my_external_storage
|
||||
```
|
||||
|
||||
## Aliases
|
||||
|
||||
A handy tool for more advanced configurations is to use yaml aliases.
|
||||
These must be specified under the global `extras` key in the `.autorestic.yml` config file.
|
||||
Aliases allow to reuse snippets of config throughout the same file.
|
||||
|
||||
The following example shows how the locations `a` and `b` share the same hooks and forget policies.
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
version: 2
|
||||
|
||||
extras:
|
||||
hooks: &foo
|
||||
prevalidate:
|
||||
- echo "Wake up!"
|
||||
before:
|
||||
- echo "Hello"
|
||||
after:
|
||||
- echo "kthxbye"
|
||||
policies: &bar
|
||||
keep-daily: 14
|
||||
keep-weekly: 52
|
||||
|
||||
backends:
|
||||
# ...
|
||||
locations:
|
||||
a:
|
||||
from: /data/a
|
||||
to: some
|
||||
hooks:
|
||||
<<: *foo
|
||||
options:
|
||||
forget:
|
||||
<<: *bar
|
||||
b:
|
||||
from: data/b
|
||||
to: some
|
||||
hooks:
|
||||
<<: *foo
|
||||
options:
|
||||
forget:
|
||||
<<: *bar
|
||||
```
|
19
docs/pages/contributors.md
Normal file
19
docs/pages/contributors.md
Normal file
@ -0,0 +1,19 @@
|
||||
# 🙋♀️🙋♂️ Contributors
|
||||
|
||||
This amazing people helped the project!
|
||||
|
||||
- @agateblue - Docs, Pruning, S3.
|
||||
- @g-a-c - Update/Install bugs.
|
||||
- @jjromannet - Bug fixes.
|
||||
- @fariszr - Docker image improvements.
|
||||
- @david-boles - Docs.
|
||||
- @SebDanielsson - Brew.
|
||||
- @n194 - AUR Package.
|
||||
- @olofvndrhr - Healthchecks example.
|
||||
- @jin-park-dev - Typos.
|
||||
- @sumnerboy12 - Typos.
|
||||
- @FuzzyMistborn - Typos.
|
||||
- @ChanceM - Typos.
|
||||
- @TheForcer - Typos.
|
||||
- @themorlan - Typos.
|
||||
- @somebox - Typos.
|
28
docs/pages/docker.md
Normal file
28
docs/pages/docker.md
Normal file
@ -0,0 +1,28 @@
|
||||
# 🐳 Docker
|
||||
|
||||
The docker image is build with rclone and restic already included. It's ment more as a utility image.
|
||||
|
||||
## Remote hosts
|
||||
|
||||
For remote backups (S3, B2, GCS, etc.) it's quite easy, as you only need to mount the config file and the data to backup.
|
||||
|
||||
```bash
|
||||
docker run --rm \\
|
||||
-v $(pwd):/data \\
|
||||
cupcakearmy/autorestic \\
|
||||
autorestic backup -va -c /data/.autorestic.yaml
|
||||
```
|
||||
|
||||
## Rclone
|
||||
|
||||
For rclone you will have to also mount the rclone config file to `/root/.config/rclone/rclone.conf`.
|
||||
|
||||
To check where it is located you can run the following command: `rclone config file`.
|
||||
|
||||
**Example**
|
||||
|
||||
```bash
|
||||
docker run \\
|
||||
-v /home/user/.config/rclone/rclone.conf:/root/.config/rclone/rclone.conf:ro \\
|
||||
...
|
||||
```
|
38
docs/pages/examples.md
Normal file
38
docs/pages/examples.md
Normal file
@ -0,0 +1,38 @@
|
||||
# 🐣 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
|
||||
```
|
||||
|
||||
## Use hooks to integrate with [healthchecks](https://healthchecks.io/)
|
||||
|
||||
> Thanks to @olofvndrhr for providing it ❤️
|
||||
|
||||
```yaml
|
||||
extras:
|
||||
healthchecks: &healthchecks
|
||||
hooks:
|
||||
before:
|
||||
- 'curl -m 10 --retry 5 -X POST -H "Content-Type: text/plain" --data "Starting backup for location: ${AUTORESTIC_LOCATION}" https://<healthchecks-url>/ping/<uid>/start'
|
||||
failure:
|
||||
- 'curl -m 10 --retry 5 -X POST -H "Content-Type: text/plain" --data "Backup failed for location: ${AUTORESTIC_LOCATION}" https://<healthchecks-url>/ping/<uid>/fail'
|
||||
success:
|
||||
- 'curl -m 10 --retry 5 -X POST -H "Content-Type: text/plain" --data "Backup successful for location: ${AUTORESTIC_LOCATION}" https://<healthchecks-url>/ping/<uid>'
|
||||
|
||||
locations:
|
||||
something:
|
||||
<<: *healthchecks
|
||||
from: /somewhere
|
||||
to:
|
||||
- somewhere-else
|
||||
```
|
20
docs/pages/index.md
Normal file
20
docs/pages/index.md
Normal file
@ -0,0 +1,20 @@
|
||||
# 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
|
||||
- Before/after backup hooks
|
||||
- Exclude pattern/files
|
||||
- Cron jobs for automatic backup
|
||||
- Backup & Restore docker volumes
|
||||
- Generated completions for `[bash|zsh|fish|powershell]`
|
33
docs/pages/installation.md
Normal file
33
docs/pages/installation.md
Normal file
@ -0,0 +1,33 @@
|
||||
# 🛳 Installation
|
||||
|
||||
Linux & macOS. Windows is not supported. If you have problems installing please open an issue :)
|
||||
|
||||
Autorestic requires `bash`, `wget` and `bzip2` to be installed. For most systems these should be already installed.
|
||||
|
||||
```bash
|
||||
wget -qO - https://raw.githubusercontent.com/cupcakearmy/autorestic/master/install.sh | bash
|
||||
```
|
||||
|
||||
## Alternatives
|
||||
|
||||
### Docker
|
||||
|
||||
There is an official docker image over at [cupcakearmy/autorestic](https://hub.docker.com/r/cupcakearmy/autorestic).
|
||||
|
||||
For some examples see [here](/docker).
|
||||
|
||||
### 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`.
|
||||
|
||||
### Fedora
|
||||
|
||||
Fedora users can install the [autorestic](https://src.fedoraproject.org/rpms/autorestic/) package with `dnf install autorestic`.
|
||||
|
||||
### AUR
|
||||
|
||||
If you are on Arch there is an [AUR Package](https://aur.archlinux.org/packages/autorestic-bin/)
|
7
docs/pages/location/_meta.json
Normal file
7
docs/pages/location/_meta.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"index": "Overview",
|
||||
"hooks": "Hooks",
|
||||
"options": "Options",
|
||||
"cron": "Cronjobs",
|
||||
"docker": "Docker volumes"
|
||||
}
|
53
docs/pages/location/cron.md
Normal file
53
docs/pages/location/cron.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Cron
|
||||
|
||||
Often it is useful 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 an 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 how 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 advice 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 -c /path/to/my/.autorestic.yml --ci cron
|
||||
```
|
||||
|
||||
> The `--ci` option is not required, but recommended
|
||||
|
||||
To debug a cron job you can use
|
||||
|
||||
```bash
|
||||
*/5 * * * * autorestic -c /path/to/my/.autorestic.yml --ci cron > /tmp/autorestic.log 2>&1
|
||||
```
|
||||
|
||||
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 intentionally not linked.
|
37
docs/pages/location/docker.md
Normal file
37
docs/pages/location/docker.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Docker
|
||||
|
||||
autorestic supports docker volumes directly, without needing them to be mounted to the host filesystem.
|
||||
|
||||
```yaml | docker-compose.yml
|
||||
version: '3.8'
|
||||
|
||||
volumes:
|
||||
data:
|
||||
name: my-data
|
||||
|
||||
services:
|
||||
api:
|
||||
image: alpine
|
||||
volumes:
|
||||
- data:/foo/bar
|
||||
```
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
locations:
|
||||
hello:
|
||||
from: my-data
|
||||
type: volume
|
||||
# ...
|
||||
```
|
||||
|
||||
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.
|
85
docs/pages/location/hooks.md
Normal file
85
docs/pages/location/hooks.md
Normal file
@ -0,0 +1,85 @@
|
||||
# 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:
|
||||
|
||||
- `prevalidate`
|
||||
- `before`
|
||||
- `after`
|
||||
- `failure`
|
||||
- `success`
|
||||
|
||||
The difference between `prevalidate` and `before` hooks are that `prevalidate` is run before checking the backup location is valid, including checking that the `from` directories exist. This can be useful, for example, to mount the source filesystem that contains the directories listed in `from`.
|
||||
|
||||
```yml | .autorestic.yml
|
||||
locations:
|
||||
my-location:
|
||||
from: /data
|
||||
to: my-backend
|
||||
hooks:
|
||||
prevalidate:
|
||||
- echo "Checks"
|
||||
before:
|
||||
- echo "One"
|
||||
- echo "Two"
|
||||
- echo "Three"
|
||||
after:
|
||||
- echo "Bye"
|
||||
failure:
|
||||
- echo "Something went wrong"
|
||||
success:
|
||||
- echo "Well done!"
|
||||
```
|
||||
|
||||
## Flowchart
|
||||
|
||||
1. `prevalidate` hook
|
||||
2. Check backup location
|
||||
3. `before` hook
|
||||
4. Run backup
|
||||
5. `after` hook
|
||||
6. - `success` hook if no errors were found
|
||||
- `failure` hook if at least one error was encountered
|
||||
|
||||
If either the `prevalidate` or `before` hook encounters errors then the backup and `after` hooks will be skipped and only the `failed` hooks will run.
|
||||
|
||||
## Environment variables
|
||||
|
||||
All hooks are exposed to the `AUTORESTIC_LOCATION` environment variable, which contains the location name.
|
||||
|
||||
The `after` and `success` hooks have access to additional information with the following syntax:
|
||||
|
||||
```bash
|
||||
AUTORESTIC_[TYPE]_[I]
|
||||
AUTORESTIC_[TYPE]_[BACKEND_NAME]
|
||||
```
|
||||
|
||||
Every type of metadata is appended with both the name of the backend associated with and the number in which the backends where executed.
|
||||
|
||||
### Available Metadata Types
|
||||
|
||||
- `SNAPSHOT_ID`
|
||||
- `PARENT_SNAPSHOT_ID`
|
||||
- `FILES_ADDED`
|
||||
- `FILES_CHANGED`
|
||||
- `FILES_UNMODIFIED`
|
||||
- `DIRS_ADDED`
|
||||
- `DIRS_CHANGED`
|
||||
- `DIRS_UNMODIFIED`
|
||||
- `ADDED_SIZE`
|
||||
- `PROCESSED_FILES`
|
||||
- `PROCESSED_SIZE`
|
||||
- `PROCESSED_DURATION`
|
||||
|
||||
#### Example
|
||||
|
||||
Assuming you have a location `bar` that backs up to a single backend named `foo` you could expect the following env variables:
|
||||
|
||||
```bash
|
||||
AUTORESTIC_LOCATION=bar
|
||||
AUTORESTIC_FILES_ADDED_0=42
|
||||
AUTORESTIC_FILES_ADDED_FOO=42
|
||||
```
|
33
docs/pages/location/index.md
Normal file
33
docs/pages/location/index.md
Normal file
@ -0,0 +1,33 @@
|
||||
# 🗂 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.
|
||||
|
||||
Note: names of locations MUST be lower case!
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
version: 2
|
||||
|
||||
locations:
|
||||
my-location-name:
|
||||
from: path/to/backup
|
||||
# Or multiple
|
||||
# from:
|
||||
# - /a
|
||||
# - /b
|
||||
to:
|
||||
- name-of-backend
|
||||
- also-backup-to-this-backend
|
||||
```
|
||||
|
||||
## `from`
|
||||
|
||||
This is the source of the location. Can be an `array` for multiple sources.
|
||||
|
||||
#### 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.
|
3
docs/pages/location/options/_meta.json
Normal file
3
docs/pages/location/options/_meta.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"index": "Overview"
|
||||
}
|
31
docs/pages/location/options/copy.md
Normal file
31
docs/pages/location/options/copy.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Copy
|
||||
|
||||
Instead of specifying multiple `to` backends for a given `location` you can also use the `copy` option. Instead of recalculating the backup multiple times, you can copy the freshly copied snapshot from one backend to the other, avoiding recomputation.
|
||||
|
||||
###### Example
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
locations:
|
||||
my-location:
|
||||
from: /data
|
||||
to:
|
||||
- a #Fast
|
||||
- b #Fast
|
||||
- c #Slow
|
||||
```
|
||||
|
||||
Becomes
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
locations:
|
||||
my-location:
|
||||
from: /data
|
||||
to:
|
||||
- a
|
||||
- b
|
||||
copy:
|
||||
a:
|
||||
- c
|
||||
```
|
||||
|
||||
Instead of backing up to each backend separately, you can choose that the snapshot created to `a` will be copied over to `c`, avoiding heavy computation on `c`.
|
18
docs/pages/location/options/exclude.md
Normal file
18
docs/pages/location/options/exclude.md
Normal file
@ -0,0 +1,18 @@
|
||||
# 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
|
||||
```
|
55
docs/pages/location/options/forget.md
Normal file
55
docs/pages/location/options/forget.md
Normal file
@ -0,0 +1,55 @@
|
||||
# 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
|
||||
version: 2
|
||||
|
||||
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: '14d' # keep snapshots from the last 14 days
|
||||
```
|
||||
|
||||
## Globally
|
||||
|
||||
You can specify global forget policies that would be applied to all locations:
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
version: 2
|
||||
|
||||
global:
|
||||
forget:
|
||||
keep-daily: 30
|
||||
keep-weekly: 52
|
||||
```
|
||||
|
||||
## Automatically forget after backup
|
||||
|
||||
You can also configure `autorestic` to automatically run the forget command for you after every backup. You can do that by specifying the `forget` option.
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
version: 2
|
||||
|
||||
locations:
|
||||
etc:
|
||||
from: /etc
|
||||
to: local
|
||||
forget: prune # Or only "yes" if you don't want to prune
|
||||
options:
|
||||
forget:
|
||||
keep-last: 5
|
||||
```
|
65
docs/pages/location/options/index.md
Normal file
65
docs/pages/location/options/index.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Options
|
||||
|
||||
For the `backup` and `forget` commands you can pass any native flags to `restic`. In addition you can specify flags for every command with `all`.
|
||||
|
||||
If flags don't start with `-` they will get prefixed with `--`.
|
||||
|
||||
Flags without arguments can be set to `true`. They will be handled accordingly.
|
||||
|
||||
> ℹ️ It is also possible to set options for an [entire backend](/backend/options) or globally (see below).
|
||||
|
||||
```yaml
|
||||
locations:
|
||||
foo:
|
||||
# ...
|
||||
options:
|
||||
all:
|
||||
some-flag: 123
|
||||
# Equivalent to
|
||||
--some-flag: 123
|
||||
backup:
|
||||
boolean-flag: true
|
||||
tag:
|
||||
- foo
|
||||
- bar
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
In this example, whenever `autorestic` runs `restic backup` it will append a `--tag foo --tag bar` to the native command.
|
||||
|
||||
```yaml
|
||||
locations:
|
||||
foo:
|
||||
path: ...
|
||||
to: ...
|
||||
options:
|
||||
backup:
|
||||
tag:
|
||||
- foo
|
||||
- bar
|
||||
```
|
||||
|
||||
## Priority
|
||||
|
||||
Options can be set globally, on the backends or on the locations.
|
||||
|
||||
The priority is as follows: `location > backend > global`.
|
||||
|
||||
## Global Options
|
||||
|
||||
It is possible to specify global flags that will be run every time restic is invoked. To do so specify them under `global` in your config file.
|
||||
|
||||
```yaml
|
||||
global:
|
||||
all:
|
||||
cache-dir: ~/restic
|
||||
backup:
|
||||
tag:
|
||||
- foo
|
||||
|
||||
backends:
|
||||
# ...
|
||||
locations:
|
||||
# ...
|
||||
```
|
4
docs/pages/migration.md
Normal file
4
docs/pages/migration.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Migration
|
||||
|
||||
- [From 0.x to 1.0](/migration/0.x_1.0)
|
||||
- [From 1.4 to 1.5](/migration/1.4_1.5)
|
24
docs/pages/migration/0.x_1.0.md
Normal file
24
docs/pages/migration/0.x_1.0.md
Normal file
@ -0,0 +1,24 @@
|
||||
# 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.
|
66
docs/pages/migration/1.4_1.5.md
Normal file
66
docs/pages/migration/1.4_1.5.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Migration from `1.4` to `1.5`
|
||||
|
||||
## ⚠️ Important notes
|
||||
|
||||
The way snapshots are referenced in the `restore` and `prune` commands has been changed. Before they were referenced by the path. Now every backup is tagged and those tags are then referenced in the cli. This means that when running restore and forget commands old backups are not taken into account anymore.
|
||||
|
||||
## Config files
|
||||
|
||||
- The config file now required to have a version number. This has to be added with `version: 2` at the root.
|
||||
- Hooks now optionally support `dir: /some/dir` in the [options object](https://pkg.go.dev/github.com/cupcakearmy/autorestic/internal#Hooks).
|
||||
- Docker volumes don't get prefixed with `volume:` anymore, rather you have to set the `type: volume` in the [location config](https://pkg.go.dev/github.com/cupcakearmy/autorestic/internal#Hooks).
|
||||
|
||||
See detailed instructions below.
|
||||
|
||||
## Config Version
|
||||
|
||||
```yaml
|
||||
version: 2 # Added
|
||||
|
||||
backends:
|
||||
# ...
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
Since `1.5` multiple sources for a location are possible.
|
||||
For this reason, while before hooks where executed in the folder of the source, now they are executed in the directory of the config `.autorestic.yaml`.
|
||||
|
||||
You can overwrite this behavior with the new `dir` option in the hook section of the config.
|
||||
|
||||
```yaml
|
||||
locations:
|
||||
l1:
|
||||
# ...
|
||||
from: /foo/bar
|
||||
hooks:
|
||||
dir: /foo/bar
|
||||
before: pwd
|
||||
```
|
||||
|
||||
## Docker volumes
|
||||
|
||||
The syntax with docker volumes has changed and needs to be adjusted.
|
||||
|
||||
```yaml
|
||||
# Before
|
||||
locations:
|
||||
foo:
|
||||
from: volume:my-data
|
||||
```
|
||||
|
||||
```yaml
|
||||
# After
|
||||
locations:
|
||||
foo:
|
||||
from: my-data
|
||||
type: volume
|
||||
```
|
||||
|
||||
## Tagging
|
||||
|
||||
Autorestic changed the way backups are referenced. Before we took the paths as the identifying information. Now autorestic uses native restic tags to reference them. This means that old backups are not referenced. You can the old snapshots manually. An example can be shown below.
|
||||
|
||||
```bash
|
||||
autorestic exec -va -- tag --add ar:location:LOCATION_NAME # Only if you have only one location
|
||||
```
|
4
docs/pages/migration/_meta.json
Normal file
4
docs/pages/migration/_meta.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"0.x_1.0": "0.x → 1.0",
|
||||
"1.4_1.5": "1.4 → 1.5"
|
||||
}
|
8
docs/pages/qa.md
Normal file
8
docs/pages/qa.md
Normal file
@ -0,0 +1,8 @@
|
||||
# ❓ 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.
|
84
docs/pages/quick.md
Normal file
84
docs/pages/quick.md
Normal file
@ -0,0 +1,84 @@
|
||||
# 🚀 Quickstart
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
wget -qO - 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
|
||||
version: 2
|
||||
|
||||
locations:
|
||||
home:
|
||||
from: /home
|
||||
# Or multiple
|
||||
# from:
|
||||
# - /foo
|
||||
# - /bar
|
||||
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.
|
3190
docs/pnpm-lock.yaml
generated
Normal file
3190
docs/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
docs/theme.config.jsx
Normal file
24
docs/theme.config.jsx
Normal file
@ -0,0 +1,24 @@
|
||||
export default {
|
||||
logo: <span>Autorestic</span>,
|
||||
docsRepositoryBase: 'https://github.com/cupcakearmy/autorestic/tree/master/docs',
|
||||
project: {
|
||||
link: 'https://github.com/cupcakearmy/autorestic',
|
||||
},
|
||||
sidebar: {
|
||||
defaultMenuCollapseLevel: 1,
|
||||
},
|
||||
feedback: {
|
||||
content: 'Question? An error? Give feedback →',
|
||||
},
|
||||
footer: {
|
||||
text: (
|
||||
<span>
|
||||
MIT {new Date().getFullYear()} ©{' '}
|
||||
<a href="https://github.com/cupcakearmy" target="_blank">
|
||||
cupcakearmy
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
}
|
39
go.mod
Normal file
39
go.mod
Normal file
@ -0,0 +1,39 @@
|
||||
module github.com/cupcakearmy/autorestic
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/buger/goterm v1.0.0
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/robfig/cron v1.2.0
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/spf13/viper v1.11.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
502
go.sum
Normal file
502
go.sum
Normal file
@ -0,0 +1,502 @@
|
||||
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.44.3/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 v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
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/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
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/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
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/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
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/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
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.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
|
||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
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/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
|
||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
|
||||
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
|
||||
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
|
||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
|
||||
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
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.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44=
|
||||
github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
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/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
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/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
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/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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-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/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
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/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-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-20190524140312-2c0ae7006135/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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
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/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
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/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
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=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
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.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
|
||||
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/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=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
50
install.sh
Executable file
50
install.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
shopt -s nocaseglob
|
||||
|
||||
OUT_FILE=/usr/local/bin/autorestic
|
||||
|
||||
# Type
|
||||
NATIVE_OS=$(uname | tr '[:upper:]' '[:lower:]')
|
||||
if [[ $NATIVE_OS == *"linux"* ]]; then
|
||||
OS=linux
|
||||
elif [[ $NATIVE_OS == *"darwin"* ]]; then
|
||||
OS=darwin
|
||||
elif [[ $NATIVE_OS == *"freebsd"* ]]; then
|
||||
OS=freebsd
|
||||
else
|
||||
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"* || $NATIVE_ARCH == *"amd64"* ]]; then
|
||||
ARCH=amd64
|
||||
elif [[ $NATIVE_ARCH == *"arm64"* || $NATIVE_ARCH == *"aarch64"* ]]; then
|
||||
ARCH=arm64
|
||||
elif [[ $NATIVE_ARCH == *"x86"* ]]; then
|
||||
ARCH=386
|
||||
elif [[ $NATIVE_ARCH == *"armv7"* ]]; then
|
||||
ARCH=arm
|
||||
else
|
||||
echo "Could not determine Architecure automatically, please check the release page manually: https://github.com/cupcakearmy/autorestic/releases"
|
||||
exit 1
|
||||
fi
|
||||
echo $ARCH
|
||||
|
||||
if ! command -v bzip2 &>/dev/null; then
|
||||
echo "Missing bzip2 command. Please install the bzip2 package for your system."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wget -qO - https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
|
||||
| grep "browser_download_url.*_${OS}_${ARCH}" \
|
||||
| cut -d : -f 2,3 \
|
||||
| tr -d \" \
|
||||
| wget -O "${OUT_FILE}.bz2" -i -
|
||||
bzip2 -fd "${OUT_FILE}.bz2"
|
||||
chmod +x ${OUT_FILE}
|
||||
|
||||
autorestic install
|
||||
echo "Successfully installed autorestic"
|
215
internal/backend.go
Normal file
215
internal/backend.go
Normal file
@ -0,0 +1,215 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||
)
|
||||
|
||||
type BackendRest struct {
|
||||
User string `mapstructure:"user,omitempty" yaml:"user,omitempty"`
|
||||
Password string `mapstructure:"password,omitempty" yaml:"password,omitempty"`
|
||||
}
|
||||
|
||||
type Backend struct {
|
||||
name string
|
||||
Type string `mapstructure:"type,omitempty" yaml:"type,omitempty"`
|
||||
Path string `mapstructure:"path,omitempty" yaml:"path,omitempty"`
|
||||
Key string `mapstructure:"key,omitempty" yaml:"key,omitempty"`
|
||||
RequireKey bool `mapstructure:"requireKey,omitempty" yaml:"requireKey,omitempty"`
|
||||
Env map[string]string `mapstructure:"env,omitempty" yaml:"env,omitempty"`
|
||||
Rest BackendRest `mapstructure:"rest,omitempty" yaml:"rest,omitempty"`
|
||||
Options Options `mapstructure:"options,omitempty" yaml:"options,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(os.ExpandEnv(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)
|
||||
}
|
||||
}
|
||||
|
||||
var nonAlphaRegex = regexp.MustCompile("[^A-Za-z0-9]")
|
||||
|
||||
func (b Backend) getEnv() (map[string]string, error) {
|
||||
env := make(map[string]string)
|
||||
// Key
|
||||
if b.Key != "" {
|
||||
env["RESTIC_PASSWORD"] = b.Key
|
||||
}
|
||||
|
||||
// From config file
|
||||
repo, err := b.generateRepo()
|
||||
env["RESTIC_REPOSITORY"] = repo
|
||||
for key, value := range b.Env {
|
||||
env[strings.ToUpper(key)] = value
|
||||
}
|
||||
|
||||
// From Envfile and passed as env
|
||||
nameForEnv := strings.ToUpper(b.name)
|
||||
nameForEnv = nonAlphaRegex.ReplaceAllString(nameForEnv, "_")
|
||||
var prefix = "AUTORESTIC_" + nameForEnv + "_"
|
||||
for _, variable := range os.Environ() {
|
||||
var splitted = strings.SplitN(variable, "=", 2)
|
||||
if strings.HasPrefix(splitted[0], prefix) {
|
||||
env[strings.TrimPrefix(splitted[0], prefix)] = splitted[1]
|
||||
}
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
// Check if key is set in environment
|
||||
env, _ := b.getEnv()
|
||||
if _, found := env["RESTIC_PASSWORD"]; !found {
|
||||
if b.RequireKey {
|
||||
return fmt.Errorf("backend %s requires a key but none was provided", b.name)
|
||||
}
|
||||
// No key set in config file or env => generate random key and save file
|
||||
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, Silent: true}
|
||||
// Check if already initialized
|
||||
cmd := []string{"check"}
|
||||
cmd = append(cmd, combineBackendOptions("check", b)...)
|
||||
_, _, err = ExecuteResticCommand(options, cmd...)
|
||||
if err == nil {
|
||||
return nil
|
||||
} else {
|
||||
// If not initialize
|
||||
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
|
||||
cmd := []string{"init"}
|
||||
cmd = append(cmd, combineBackendOptions("init", b)...)
|
||||
_, _, err := ExecuteResticCommand(options, cmd...)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (b Backend) Exec(args []string) error {
|
||||
env, err := b.getEnv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options := ExecuteOptions{Envs: env}
|
||||
args = append(args, combineBackendOptions("exec", b)...)
|
||||
_, out, err := ExecuteResticCommand(options, args...)
|
||||
if err != nil {
|
||||
colors.Error.Println(out)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Backend) ExecDocker(l Location, args []string) (int, string, error) {
|
||||
env, err := b.getEnv()
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
volume := l.From[0]
|
||||
options := ExecuteOptions{
|
||||
Command: "docker",
|
||||
Envs: env,
|
||||
}
|
||||
dir := "/data"
|
||||
args = append([]string{"restic"}, args...)
|
||||
docker := []string{
|
||||
"run", "--rm",
|
||||
"--entrypoint", "ash",
|
||||
"--workdir", dir,
|
||||
"--volume", volume + ":" + dir,
|
||||
}
|
||||
// Use of docker host, not the container host
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
docker = append(docker, "--hostname", hostname)
|
||||
}
|
||||
switch b.Type {
|
||||
case "local":
|
||||
actual := env["RESTIC_REPOSITORY"]
|
||||
docker = append(docker, "--volume", actual+":"+"/repo")
|
||||
env["RESTIC_REPOSITORY"] = "/repo"
|
||||
case "b2":
|
||||
case "s3":
|
||||
case "azure":
|
||||
case "gs":
|
||||
case "rest":
|
||||
// No additional setup needed
|
||||
case "rclone":
|
||||
// Read host rclone config and mount it into the container
|
||||
code, configFile, err := ExecuteCommand(ExecuteOptions{Command: "rclone"}, "config", "file")
|
||||
if err != nil {
|
||||
return code, "", err
|
||||
}
|
||||
splitted := strings.Split(strings.TrimSpace(configFile), "\n")
|
||||
configFilePath := splitted[len(splitted)-1]
|
||||
docker = append(docker, "--volume", configFilePath+":"+"/root/.config/rclone/rclone.conf:ro")
|
||||
default:
|
||||
return -1, "", fmt.Errorf("Backend type \"%s\" is not supported as volume endpoint", b.Type)
|
||||
}
|
||||
for key, value := range env {
|
||||
docker = append(docker, "--env", key+"="+value)
|
||||
}
|
||||
|
||||
docker = append(docker, flags.DOCKER_IMAGE, "-c", strings.Join(args, " "))
|
||||
return ExecuteCommand(options, docker...)
|
||||
}
|
265
internal/backend_test.go
Normal file
265
internal/backend_test.go
Normal file
@ -0,0 +1,265 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerateRepo(t *testing.T) {
|
||||
|
||||
t.Run("empty backend", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "empty backend",
|
||||
Type: "",
|
||||
}
|
||||
_, err := b.generateRepo()
|
||||
if err == nil {
|
||||
t.Errorf("Error expected for empty backend type")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("local backend", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "local backend",
|
||||
Type: "local",
|
||||
Path: "/foo/bar",
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "/foo/bar")
|
||||
})
|
||||
|
||||
t.Run("local backend with homedir prefix", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "local backend",
|
||||
Type: "local",
|
||||
Path: "~/foo/bar",
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, fmt.Sprintf("%s/foo/bar", os.Getenv("HOME")))
|
||||
})
|
||||
|
||||
t.Run("local backend with config file", func(t *testing.T) {
|
||||
// config file path should always be present from initConfig
|
||||
viper.SetConfigFile("/tmp/.autorestic.yml")
|
||||
defer viper.Reset()
|
||||
|
||||
b := Backend{
|
||||
name: "local backend",
|
||||
Type: "local",
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "/tmp")
|
||||
})
|
||||
|
||||
t.Run("rest backend with valid path", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "rest backend",
|
||||
Type: "rest",
|
||||
Path: "http://localhost:8000/foo",
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "rest:http://localhost:8000/foo")
|
||||
})
|
||||
|
||||
t.Run("rest backend with user", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "rest backend",
|
||||
Type: "rest",
|
||||
Path: "http://localhost:8000/foo",
|
||||
Rest: BackendRest{
|
||||
User: "user",
|
||||
Password: "",
|
||||
},
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "rest:http://user@localhost:8000/foo")
|
||||
})
|
||||
|
||||
t.Run("rest backend with user and password", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "rest backend",
|
||||
Type: "rest",
|
||||
Path: "http://localhost:8000/foo",
|
||||
Rest: BackendRest{
|
||||
User: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
}
|
||||
result, err := b.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result, "rest:http://user:pass@localhost:8000/foo")
|
||||
})
|
||||
|
||||
backendTests := []struct {
|
||||
name string
|
||||
backend Backend
|
||||
want string
|
||||
}{
|
||||
{name: "b2 backend", backend: Backend{name: "b2", Type: "b2", Path: "foo"}, want: "b2:foo"},
|
||||
{name: "azure backend", backend: Backend{name: "azure", Type: "azure", Path: "foo"}, want: "azure:foo"},
|
||||
{name: "gs backend", backend: Backend{name: "gs", Type: "gs", Path: "foo"}, want: "gs:foo"},
|
||||
{name: "s3 backend", backend: Backend{name: "s3", Type: "s3", Path: "foo"}, want: "s3:foo"},
|
||||
{name: "sftp backend", backend: Backend{name: "sftp", Type: "sftp", Path: "foo"}, want: "sftp:foo"},
|
||||
{name: "rclone backend", backend: Backend{name: "rclone", Type: "rclone", Path: "foo"}, want: "rclone:foo"},
|
||||
}
|
||||
|
||||
for _, tt := range backendTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.backend.generateRepo()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnv(t *testing.T) {
|
||||
t.Run("env in key field", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "",
|
||||
Type: "local",
|
||||
Path: "/foo/bar",
|
||||
Key: "secret123",
|
||||
}
|
||||
result, err := b.getEnv()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
|
||||
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
|
||||
})
|
||||
|
||||
t.Run("env in config file", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "",
|
||||
Type: "local",
|
||||
Path: "/foo/bar",
|
||||
Env: map[string]string{
|
||||
"B2_ACCOUNT_ID": "foo123",
|
||||
"B2_ACCOUNT_KEY": "foo456",
|
||||
},
|
||||
}
|
||||
result, err := b.getEnv()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
|
||||
assertEqual(t, result["RESTIC_PASSWORD"], "")
|
||||
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
|
||||
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
|
||||
})
|
||||
|
||||
t.Run("env in Envfile or env vars", func(t *testing.T) {
|
||||
// generate env variables
|
||||
// TODO better way to teardown
|
||||
defer os.Unsetenv("AUTORESTIC_FOO_RESTIC_PASSWORD")
|
||||
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_ID")
|
||||
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY")
|
||||
os.Setenv("AUTORESTIC_FOO_RESTIC_PASSWORD", "secret123")
|
||||
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_ID", "foo123")
|
||||
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY", "foo456")
|
||||
|
||||
b := Backend{
|
||||
name: "foo",
|
||||
Type: "local",
|
||||
Path: "/foo/bar",
|
||||
}
|
||||
result, err := b.getEnv()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
|
||||
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
|
||||
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
|
||||
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
|
||||
})
|
||||
|
||||
for _, char := range "@-_:/" {
|
||||
t.Run(fmt.Sprintf("env var with special char (%c)", char), func(t *testing.T) {
|
||||
// generate env variables
|
||||
// TODO better way to teardown
|
||||
defer os.Unsetenv("AUTORESTIC_FOO_BAR_RESTIC_PASSWORD")
|
||||
defer os.Unsetenv("AUTORESTIC_FOO_BAR_B2_ACCOUNT_ID")
|
||||
defer os.Unsetenv("AUTORESTIC_FOO_BAR_B2_ACCOUNT_KEY")
|
||||
os.Setenv("AUTORESTIC_FOO_BAR_RESTIC_PASSWORD", "secret123")
|
||||
os.Setenv("AUTORESTIC_FOO_BAR_B2_ACCOUNT_ID", "foo123")
|
||||
os.Setenv("AUTORESTIC_FOO_BAR_B2_ACCOUNT_KEY", "foo456")
|
||||
|
||||
b := Backend{
|
||||
name: fmt.Sprintf("foo%cbar", char),
|
||||
Type: "local",
|
||||
Path: "/foo/bar",
|
||||
}
|
||||
result, err := b.getEnv()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
|
||||
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
|
||||
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
|
||||
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
t.Run("no type given", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "foo",
|
||||
Type: "",
|
||||
Path: "/foo/bar",
|
||||
}
|
||||
err := b.validate()
|
||||
if err == nil {
|
||||
t.Error("expected to get error")
|
||||
}
|
||||
assertEqual(t, err.Error(), "Backend \"foo\" has no \"type\"")
|
||||
})
|
||||
|
||||
t.Run("no path given", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "foo",
|
||||
Type: "local",
|
||||
Path: "",
|
||||
}
|
||||
err := b.validate()
|
||||
if err == nil {
|
||||
t.Error("expected to get error")
|
||||
}
|
||||
assertEqual(t, err.Error(), "Backend \"foo\" has no \"path\"")
|
||||
})
|
||||
|
||||
t.Run("require key with no key", func(t *testing.T) {
|
||||
b := Backend{
|
||||
name: "foo",
|
||||
Type: "local",
|
||||
Path: "~/foo/bar",
|
||||
RequireKey: true,
|
||||
}
|
||||
err := b.validate()
|
||||
fmt.Printf("error: %v\n", err)
|
||||
assert.EqualError(t, err, "backend foo requires a key but none was provided")
|
||||
})
|
||||
}
|
173
internal/bins/bins.go
Normal file
173
internal/bins/bins.go
Normal file
@ -0,0 +1,173 @@
|
||||
package bins
|
||||
|
||||
import (
|
||||
"compress/bzip2"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||
)
|
||||
|
||||
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 := io.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 {
|
||||
return err
|
||||
}
|
||||
if restic {
|
||||
if err := os.Remove(path.Join(INSTALL_PATH, "restic")); err != nil {
|
||||
return 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 file in the same directory as the install directory
|
||||
// Linux does not support overwriting the file that is currently being running
|
||||
// But it can be delete the old one and a new one moved in its place.
|
||||
tmp, err := os.CreateTemp(INSTALL_PATH, "autorestic-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tmp.Close()
|
||||
if err := tmp.Chmod(0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(tmp, bz); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
to := path.Join(INSTALL_PATH, name)
|
||||
defer os.Remove(tmp.Name()) // Cleanup temporary file after thread exits
|
||||
|
||||
mode := os.FileMode(0755)
|
||||
if originalBin, err := os.Lstat(to); err == nil {
|
||||
mode = originalBin.Mode()
|
||||
err := os.Remove(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Rename(tmp.Name(), to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Chmod(to, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
_, _, err := internal.ExecuteCommand(internal.ExecuteOptions{
|
||||
Command: flags.RESTIC_BIN,
|
||||
}, "self-update")
|
||||
return err
|
||||
}
|
||||
|
||||
func Upgrade(restic bool) error {
|
||||
// Upgrade restic
|
||||
if restic {
|
||||
if err := InstallRestic(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := upgradeRestic(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if err := downloadAndInstallAsset(body, "autorestic"); err != nil {
|
||||
return err
|
||||
}
|
||||
colors.Success.Println("Updated autorestic")
|
||||
} else {
|
||||
colors.Body.Println("Already up to date")
|
||||
}
|
||||
return nil
|
||||
}
|
29
internal/colors/colors.go
Normal file
29
internal/colors/colors.go
Normal 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)
|
||||
}
|
343
internal/config.go
Normal file
343
internal/config.go
Normal file
@ -0,0 +1,343 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const VERSION = "1.8.3"
|
||||
|
||||
type OptionMap map[string][]interface{}
|
||||
type Options map[string]OptionMap
|
||||
|
||||
type Config struct {
|
||||
Version string `mapstructure:"version" yaml:"version"`
|
||||
Extras interface{} `mapstructure:"extras" yaml:"extras"`
|
||||
Locations map[string]Location `mapstructure:"locations" yaml:"locations"`
|
||||
Backends map[string]Backend `mapstructure:"backends" yaml:"backends"`
|
||||
Global Options `mapstructure:"global" yaml:"global"`
|
||||
}
|
||||
|
||||
var once sync.Once
|
||||
var config *Config
|
||||
|
||||
func exitConfig(err error, msg string) {
|
||||
if err != nil {
|
||||
colors.Error.Println(err)
|
||||
}
|
||||
if msg != "" {
|
||||
colors.Error.Println(msg)
|
||||
}
|
||||
lock.Unlock()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func GetConfig() *Config {
|
||||
|
||||
if config == nil {
|
||||
once.Do(func() {
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
absConfig, _ := filepath.Abs(viper.ConfigFileUsed())
|
||||
if !flags.CRON_LEAN {
|
||||
colors.Faint.Println("Using config: \t", absConfig)
|
||||
}
|
||||
// Load env file
|
||||
envFile := filepath.Join(filepath.Dir(absConfig), ".autorestic.env")
|
||||
err = godotenv.Load(envFile)
|
||||
if err == nil && !flags.CRON_LEAN {
|
||||
colors.Faint.Println("Using env:\t", envFile)
|
||||
}
|
||||
} else {
|
||||
text := err.Error()
|
||||
if strings.Contains(text, "no such file or directory") {
|
||||
cfgFileName := ".autorestic"
|
||||
colors.Error.Println(
|
||||
fmt.Sprintf(
|
||||
"cannot find configuration file '%s.yml' or '%s.yaml'.",
|
||||
cfgFileName, cfgFileName))
|
||||
} else {
|
||||
colors.Error.Println("could not load config file\n" + text)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var versionConfig interface{}
|
||||
viper.UnmarshalKey("version", &versionConfig)
|
||||
if versionConfig == nil {
|
||||
exitConfig(nil, "no version specified in config file. please see docs on how to migrate")
|
||||
}
|
||||
version, ok := versionConfig.(int)
|
||||
if !ok {
|
||||
exitConfig(nil, "version specified in config file is not an int")
|
||||
} else {
|
||||
// Check for version
|
||||
if version != 2 {
|
||||
exitConfig(nil, "unsupported config version number. please check the docs for migration\nhttps://autorestic.vercel.app/migration/")
|
||||
}
|
||||
}
|
||||
|
||||
config = &Config{}
|
||||
if err := viper.UnmarshalExact(config); err != nil {
|
||||
exitConfig(err, "Could not parse config file!")
|
||||
}
|
||||
})
|
||||
}
|
||||
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)
|
||||
|
||||
tmp = ""
|
||||
for _, path := range l.From {
|
||||
tmp += fmt.Sprintf("\t%s %s\n", colors.Success.Sprint("←"), path)
|
||||
}
|
||||
colors.PrintDescription("From", tmp)
|
||||
|
||||
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{
|
||||
"PreValidate": l.Hooks.PreValidate,
|
||||
"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`, flags.RESTIC_BIN)
|
||||
}
|
||||
|
||||
cwd, _ := GetPathRelativeToConfig(".")
|
||||
for name, location := range c.Locations {
|
||||
location.name = name
|
||||
|
||||
// Hooks before location validation
|
||||
options := ExecuteOptions{
|
||||
Command: "bash",
|
||||
Dir: cwd,
|
||||
Envs: map[string]string{
|
||||
"AUTORESTIC_LOCATION": location.name,
|
||||
},
|
||||
}
|
||||
if err := location.ExecuteHooks(location.Hooks.PreValidate, options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := location.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for name, backend := range c.Backends {
|
||||
backend.name = name
|
||||
if err := backend.validate(); 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 {
|
||||
var splitted = strings.Split(s, "@")
|
||||
for _, l := range list {
|
||||
if l == splitted[0] {
|
||||
goto found
|
||||
}
|
||||
}
|
||||
if backends {
|
||||
return nil, fmt.Errorf("invalid backend \"%s\"", s)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid location \"%s\"", s)
|
||||
}
|
||||
found:
|
||||
}
|
||||
|
||||
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 to the original.")
|
||||
|
||||
viper.Set("backends", c.Backends)
|
||||
viper.Set("locations", c.Locations)
|
||||
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
|
||||
func optionToString(option string) string {
|
||||
if !strings.HasPrefix(option, "-") {
|
||||
return "--" + option
|
||||
}
|
||||
return option
|
||||
}
|
||||
|
||||
func appendOptionsToSlice(str *[]string, options OptionMap) {
|
||||
for key, values := range options {
|
||||
for _, value := range values {
|
||||
// Bool
|
||||
asBool, ok := value.(bool)
|
||||
if ok && asBool {
|
||||
*str = append(*str, optionToString(key))
|
||||
continue
|
||||
}
|
||||
*str = append(*str, optionToString(key), fmt.Sprint(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getOptions(options Options, keys []string) []string {
|
||||
var selected []string
|
||||
for _, key := range keys {
|
||||
appendOptionsToSlice(&selected, options[key])
|
||||
}
|
||||
return selected
|
||||
}
|
||||
|
||||
func combineBackendOptions(key string, b Backend) []string {
|
||||
// Priority: backend > global
|
||||
var options []string
|
||||
gFlags := getOptions(GetConfig().Global, []string{key})
|
||||
bFlags := getOptions(b.Options, []string{"all", key})
|
||||
options = append(options, gFlags...)
|
||||
options = append(options, bFlags...)
|
||||
return options
|
||||
}
|
||||
|
||||
func combineAllOptions(key string, l Location, b Backend) []string {
|
||||
// Priority: location > backend > global
|
||||
var options []string
|
||||
gFlags := getOptions(GetConfig().Global, []string{key})
|
||||
bFlags := getOptions(b.Options, []string{"all", key})
|
||||
lFlags := getOptions(l.Options, []string{"all", key})
|
||||
options = append(options, gFlags...)
|
||||
options = append(options, bFlags...)
|
||||
options = append(options, lFlags...)
|
||||
return options
|
||||
}
|
211
internal/config_test.go
Normal file
211
internal/config_test.go
Normal file
@ -0,0 +1,211 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionToString(t *testing.T) {
|
||||
t.Run("no prefix", func(t *testing.T) {
|
||||
opt := "test"
|
||||
result := optionToString(opt)
|
||||
assertEqual(t, result, "--test")
|
||||
})
|
||||
|
||||
t.Run("single prefix", func(t *testing.T) {
|
||||
opt := "-test"
|
||||
result := optionToString(opt)
|
||||
assertEqual(t, result, "-test")
|
||||
})
|
||||
|
||||
t.Run("double prefix", func(t *testing.T) {
|
||||
opt := "--test"
|
||||
result := optionToString(opt)
|
||||
assertEqual(t, result, "--test")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppendOneOptionToSlice(t *testing.T) {
|
||||
t.Run("string flag", func(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{"string-flag": []interface{}{"/root"}}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
})
|
||||
|
||||
t.Run("bool flag", func(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{"boolean-flag": []interface{}{true}}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--boolean-flag",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
})
|
||||
|
||||
t.Run("int flag", func(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{"int-flag": []interface{}{123}}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--int-flag", "123",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppendMultipleOptionsToSlice(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{
|
||||
"string-flag": []interface{}{"/root"},
|
||||
"int-flag": []interface{}{123},
|
||||
}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
"--int-flag", "123",
|
||||
}
|
||||
if len(result) != len(expected) {
|
||||
t.Errorf("got length %d, want length %d", len(result), len(expected))
|
||||
}
|
||||
|
||||
// checks that expected option comes after flag, regardless of key order in map
|
||||
for i, v := range expected {
|
||||
v = strings.TrimPrefix(v, "--")
|
||||
|
||||
if value, ok := optionMap[v]; ok {
|
||||
if val, ok := value[0].(int); ok {
|
||||
if expected[i+1] != strconv.Itoa(val) {
|
||||
t.Errorf("Flags and options order are mismatched. got %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendOptionWithMultipleValuesToSlice(t *testing.T) {
|
||||
result := []string{}
|
||||
optionMap := OptionMap{
|
||||
"string-flag": []interface{}{"/root", "/bin"},
|
||||
}
|
||||
|
||||
appendOptionsToSlice(&result, optionMap)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
"--string-flag", "/bin",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
}
|
||||
|
||||
func TestGetOptionsOneKey(t *testing.T) {
|
||||
optionMap := OptionMap{
|
||||
"string-flag": []interface{}{"/root"},
|
||||
}
|
||||
options := Options{"backend": optionMap}
|
||||
keys := []string{"backend"}
|
||||
|
||||
result := getOptions(options, keys)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
}
|
||||
assertSliceEqual(t, result, expected)
|
||||
}
|
||||
|
||||
func TestGetOptionsMultipleKeys(t *testing.T) {
|
||||
firstOptionMap := OptionMap{
|
||||
"string-flag": []interface{}{"/root"},
|
||||
}
|
||||
secondOptionMap := OptionMap{
|
||||
"boolean-flag": []interface{}{true},
|
||||
"int-flag": []interface{}{123},
|
||||
}
|
||||
options := Options{
|
||||
"all": firstOptionMap,
|
||||
"forget": secondOptionMap,
|
||||
}
|
||||
keys := []string{"all", "forget"}
|
||||
|
||||
result := getOptions(options, keys)
|
||||
expected := []string{
|
||||
"--string-flag", "/root",
|
||||
"--boolean-flag",
|
||||
"--int-flag", "123",
|
||||
}
|
||||
reflect.DeepEqual(result, expected)
|
||||
}
|
||||
|
||||
func TestSaveConfigProducesReadableConfig(t *testing.T) {
|
||||
workDir := t.TempDir()
|
||||
viper.SetConfigFile(path.Join(workDir, ".autorestic.yml"))
|
||||
|
||||
// Required to appease the config reader
|
||||
viper.Set("version", 2)
|
||||
|
||||
c := Config{
|
||||
Version: "2",
|
||||
Locations: map[string]Location{
|
||||
"test": {
|
||||
Type: "local",
|
||||
name: "test",
|
||||
From: []string{"in-dir"},
|
||||
To: []string{"test"},
|
||||
// ForgetOption & ConfigOption have previously marshalled in a way that
|
||||
// can't get read correctly
|
||||
ForgetOption: "foo",
|
||||
CopyOption: map[string][]string{"foo": {"bar"}},
|
||||
},
|
||||
},
|
||||
Backends: map[string]Backend{
|
||||
"test": {
|
||||
name: "test",
|
||||
Type: "local",
|
||||
Path: "backup-target",
|
||||
Key: "supersecret",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := c.SaveConfig()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Ensure we the config reading logic actually runs
|
||||
config = nil
|
||||
once = sync.Once{}
|
||||
readConfig := GetConfig()
|
||||
assert.NotNil(t, readConfig)
|
||||
assert.Equal(t, c, *readConfig)
|
||||
}
|
||||
|
||||
func assertEqual[T comparable](t testing.TB, result, expected T) {
|
||||
t.Helper()
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("got %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSliceEqual(t testing.TB, result, expected []string) {
|
||||
t.Helper()
|
||||
|
||||
if len(result) != len(expected) {
|
||||
t.Errorf("got length %d, want length %d", len(result), len(expected))
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
assertEqual(t, result[i], expected[i])
|
||||
}
|
||||
}
|
22
internal/cron.go
Normal file
22
internal/cron.go
Normal file
@ -0,0 +1,22 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func RunCron() error {
|
||||
c := GetConfig()
|
||||
var errs []error
|
||||
for name, l := range c.Locations {
|
||||
l.name = name
|
||||
if err := l.RunCron(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("Encountered errors during cron process:\n%w", errors.Join(errs...))
|
||||
}
|
||||
return nil
|
||||
}
|
9
internal/flags/flags.go
Normal file
9
internal/flags/flags.go
Normal file
@ -0,0 +1,9 @@
|
||||
package flags
|
||||
|
||||
var (
|
||||
CI bool = false
|
||||
VERBOSE bool = false
|
||||
CRON_LEAN bool = false
|
||||
RESTIC_BIN string
|
||||
DOCKER_IMAGE string
|
||||
)
|
463
internal/location.go
Normal file
463
internal/location.go
Normal file
@ -0,0 +1,463 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/cupcakearmy/autorestic/internal/metadata"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
type LocationType string
|
||||
|
||||
const (
|
||||
TypeLocal LocationType = "local"
|
||||
TypeVolume LocationType = "volume"
|
||||
)
|
||||
|
||||
type HookArray = []string
|
||||
|
||||
type LocationForgetOption string
|
||||
|
||||
const (
|
||||
LocationForgetYes LocationForgetOption = "yes"
|
||||
LocationForgetNo LocationForgetOption = "no"
|
||||
LocationForgetPrune LocationForgetOption = "prune"
|
||||
)
|
||||
|
||||
type Hooks struct {
|
||||
Dir string `mapstructure:"dir" yaml:"dir"`
|
||||
PreValidate HookArray `mapstructure:"prevalidate,omitempty" yaml:"prevalidate,omitempty"`
|
||||
Before HookArray `mapstructure:"before,omitempty" yaml:"before,omitempty"`
|
||||
After HookArray `mapstructure:"after,omitempty" yaml:"after,omitempty"`
|
||||
Success HookArray `mapstructure:"success,omitempty" yaml:"success,omitempty"`
|
||||
Failure HookArray `mapstructure:"failure,omitempty" yaml:"failure,omitempty"`
|
||||
}
|
||||
|
||||
type LocationCopy = map[string][]string
|
||||
|
||||
type Location struct {
|
||||
name string `mapstructure:",omitempty" yaml:",omitempty"`
|
||||
From []string `mapstructure:"from,omitempty" yaml:"from,omitempty"`
|
||||
Type string `mapstructure:"type,omitempty" yaml:"type,omitempty"`
|
||||
To []string `mapstructure:"to,omitempty" yaml:"to,omitempty"`
|
||||
Hooks Hooks `mapstructure:"hooks,omitempty" yaml:"hooks,omitempty"`
|
||||
Cron string `mapstructure:"cron,omitempty" yaml:"cron,omitempty"`
|
||||
Options Options `mapstructure:"options,omitempty" yaml:"options,omitempty"`
|
||||
ForgetOption LocationForgetOption `mapstructure:"forget,omitempty" yaml:"forget,omitempty"`
|
||||
CopyOption LocationCopy `mapstructure:"copy,omitempty" yaml:"copy,omitempty"`
|
||||
}
|
||||
|
||||
func GetLocation(name string) (Location, bool) {
|
||||
l, ok := GetConfig().Locations[name]
|
||||
l.name = name
|
||||
return l, ok
|
||||
}
|
||||
|
||||
func (l Location) validate() error {
|
||||
if len(l.From) == 0 {
|
||||
return fmt.Errorf(`Location "%s" is missing "from" key`, l.name)
|
||||
}
|
||||
t, err := l.getType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t {
|
||||
case TypeLocal:
|
||||
for _, path := range l.From {
|
||||
if from, err := GetPathRelativeToConfig(path); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if _, err := os.Stat(from); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case TypeVolume:
|
||||
if len(l.From) > 1 {
|
||||
return fmt.Errorf(`location "%s" has more than one docker volume`, 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(`location "%s" has an invalid backend "%s"`, l.name, to)
|
||||
}
|
||||
}
|
||||
|
||||
// Check copy option
|
||||
for copyFrom, copyTo := range l.CopyOption {
|
||||
if _, ok := GetBackend(copyFrom); !ok {
|
||||
return fmt.Errorf(`location "%s" has an invalid backend "%s" in copy option`, l.name, copyFrom)
|
||||
}
|
||||
if !ArrayContains(l.To, copyFrom) {
|
||||
return fmt.Errorf(`location "%s" has an invalid copy from "%s"`, l.name, copyFrom)
|
||||
}
|
||||
for _, copyToTarget := range copyTo {
|
||||
if _, ok := GetBackend(copyToTarget); !ok {
|
||||
return fmt.Errorf(`location "%s" has an invalid backend "%s" in copy option`, l.name, copyToTarget)
|
||||
}
|
||||
if ArrayContains(l.To, copyToTarget) {
|
||||
return fmt.Errorf(`location "%s" cannot copy to "%s" as it's already a target`, l.name, copyToTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if forget type is correct
|
||||
if l.ForgetOption != "" {
|
||||
if l.ForgetOption != LocationForgetYes && l.ForgetOption != LocationForgetNo && l.ForgetOption != LocationForgetPrune {
|
||||
return fmt.Errorf("invalid value for forget option: %s", l.ForgetOption)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Location) ExecuteHooks(commands []string, options ExecuteOptions) error {
|
||||
if len(commands) == 0 {
|
||||
return nil
|
||||
}
|
||||
if l.Hooks.Dir != "" {
|
||||
if dir, err := GetPathRelativeToConfig(l.Hooks.Dir); err != nil {
|
||||
return err
|
||||
} else {
|
||||
options.Dir = dir
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
colors.Body.Println("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Location) getType() (LocationType, error) {
|
||||
t := strings.ToLower(l.Type)
|
||||
if t == "" || t == "local" {
|
||||
return TypeLocal, nil
|
||||
} else if t == "volume" {
|
||||
return TypeVolume, nil
|
||||
}
|
||||
return "", fmt.Errorf("invalid location type \"%s\"", l.Type)
|
||||
}
|
||||
|
||||
func buildTag(parts ...string) string {
|
||||
parts = append([]string{"ar"}, parts...)
|
||||
return strings.Join(parts, ":")
|
||||
}
|
||||
|
||||
func (l Location) getLocationTags() string {
|
||||
return buildTag("location", l.name)
|
||||
}
|
||||
|
||||
func (l Location) Backup(cron bool, dry bool, specificBackend string) []error {
|
||||
var errors []error
|
||||
var backends []string
|
||||
colors.PrimaryPrint(" Backing up location \"%s\" ", l.name)
|
||||
t, err := l.getType()
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
return errors
|
||||
}
|
||||
cwd, _ := GetPathRelativeToConfig(".")
|
||||
options := ExecuteOptions{
|
||||
Command: "bash",
|
||||
Dir: cwd,
|
||||
Envs: map[string]string{
|
||||
"AUTORESTIC_LOCATION": l.name,
|
||||
},
|
||||
}
|
||||
|
||||
// Hooks before location validation
|
||||
if err := l.ExecuteHooks(l.Hooks.PreValidate, options); err != nil {
|
||||
errors = append(errors, err)
|
||||
goto after
|
||||
}
|
||||
|
||||
if err := l.validate(); err != nil {
|
||||
errors = append(errors, err)
|
||||
goto after
|
||||
}
|
||||
|
||||
// Hooks after location validation
|
||||
if err := l.ExecuteHooks(l.Hooks.Before, options); err != nil {
|
||||
errors = append(errors, err)
|
||||
goto after
|
||||
}
|
||||
|
||||
// Backup
|
||||
if specificBackend == "" {
|
||||
backends = l.To
|
||||
} else {
|
||||
if l.hasBackend(specificBackend) {
|
||||
backends = []string{specificBackend}
|
||||
} else {
|
||||
errors = append(errors, fmt.Errorf("backup location \"%s\" has no backend \"%s\"", l.name, specificBackend))
|
||||
return errors
|
||||
}
|
||||
}
|
||||
for i, to := range backends {
|
||||
backend, _ := GetBackend(to)
|
||||
colors.Secondary.Printf("Backend: %s\n", backend.name)
|
||||
env, err := backend.getEnv()
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
|
||||
cmd := []string{"backup"}
|
||||
cmd = append(cmd, combineAllOptions("backup", l, backend)...)
|
||||
if cron {
|
||||
cmd = append(cmd, "--tag", buildTag("cron"))
|
||||
}
|
||||
if dry {
|
||||
cmd = append(cmd, "--dry-run")
|
||||
}
|
||||
cmd = append(cmd, "--tag", l.getLocationTags())
|
||||
backupOptions := ExecuteOptions{
|
||||
Envs: env,
|
||||
}
|
||||
|
||||
var code int = 0
|
||||
var out string
|
||||
switch t {
|
||||
case TypeLocal:
|
||||
for _, from := range l.From {
|
||||
path, err := GetPathRelativeToConfig(from)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
goto after
|
||||
}
|
||||
cmd = append(cmd, path)
|
||||
}
|
||||
code, out, err = ExecuteResticCommand(backupOptions, cmd...)
|
||||
case TypeVolume:
|
||||
ok := CheckIfVolumeExists(l.From[0])
|
||||
if !ok {
|
||||
errors = append(errors, fmt.Errorf("volume \"%s\" does not exist", l.From[0]))
|
||||
continue
|
||||
}
|
||||
cmd = append(cmd, "/data")
|
||||
code, out, err = backend.ExecDocker(l, cmd)
|
||||
}
|
||||
|
||||
// Extract metadata
|
||||
md := metadata.ExtractMetadataFromBackupLog(out)
|
||||
md.ExitCode = fmt.Sprint(code)
|
||||
mdEnv := metadata.MakeEnvFromMetadata(&md)
|
||||
for k, v := range mdEnv {
|
||||
options.Envs[k+"_"+fmt.Sprint(i)] = v
|
||||
options.Envs[k+"_"+strings.ToUpper(backend.name)] = v
|
||||
}
|
||||
|
||||
// If error save it and continue
|
||||
if err != nil {
|
||||
colors.Error.Println(out)
|
||||
errors = append(errors, fmt.Errorf("%s@%s:\n%s%s", l.name, backend.name, out, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Copy
|
||||
if md.SnapshotID != "" {
|
||||
for copyFrom, copyTo := range l.CopyOption {
|
||||
b1, _ := GetBackend(copyFrom)
|
||||
for _, copyToTarget := range copyTo {
|
||||
b2, _ := GetBackend(copyToTarget)
|
||||
colors.Secondary.Println("Copying " + copyFrom + " → " + copyToTarget)
|
||||
env, _ := b1.getEnv()
|
||||
env2, _ := b2.getEnv()
|
||||
// Add the second repo to the env with a "2" suffix
|
||||
for k, v := range env2 {
|
||||
env[k+"2"] = v
|
||||
}
|
||||
_, _, err := ExecuteResticCommand(ExecuteOptions{
|
||||
Envs: env,
|
||||
}, "copy", md.SnapshotID)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After backup hooks
|
||||
if err := l.ExecuteHooks(l.Hooks.After, options); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
after:
|
||||
// Success/failure hooks
|
||||
var commands []string
|
||||
var isSuccess = len(errors) == 0
|
||||
if isSuccess {
|
||||
commands = l.Hooks.Success
|
||||
} else {
|
||||
commands = l.Hooks.Failure
|
||||
}
|
||||
if err := l.ExecuteHooks(commands, options); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
// Forget and optionally prune
|
||||
if isSuccess && l.ForgetOption != "" && l.ForgetOption != LocationForgetNo {
|
||||
err := l.Forget(l.ForgetOption == LocationForgetPrune, false)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) == 0 {
|
||||
colors.Success.Println("Done")
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
func (l Location) Forget(prune bool, dry bool) error {
|
||||
colors.PrimaryPrint("Forgetting for location \"%s\"", l.name)
|
||||
|
||||
backendsToForget := l.To
|
||||
for _, copyBackends := range l.CopyOption {
|
||||
backendsToForget = append(backendsToForget, copyBackends...)
|
||||
}
|
||||
|
||||
for _, to := range backendsToForget {
|
||||
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,
|
||||
}
|
||||
cmd := []string{"forget", "--tag", l.getLocationTags()}
|
||||
if prune {
|
||||
cmd = append(cmd, "--prune")
|
||||
}
|
||||
if dry {
|
||||
cmd = append(cmd, "--dry-run")
|
||||
}
|
||||
cmd = append(cmd, combineAllOptions("forget", l, backend)...)
|
||||
_, _, err = ExecuteResticCommand(options, cmd...)
|
||||
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 buildRestoreCommand(l Location, to string, snapshot string, options []string) []string {
|
||||
base := []string{"restore", "--target", to, "--tag", l.getLocationTags(), snapshot}
|
||||
base = append(base, options...)
|
||||
return base
|
||||
}
|
||||
|
||||
func (l Location) Restore(to, from string, force bool, snapshot string, options []string) error {
|
||||
if from == "" {
|
||||
from = l.To[0]
|
||||
} else if !l.hasBackend(from) {
|
||||
return fmt.Errorf("invalid backend: \"%s\"", from)
|
||||
}
|
||||
|
||||
if snapshot == "" {
|
||||
snapshot = "latest"
|
||||
}
|
||||
|
||||
colors.PrimaryPrint("Restoring location \"%s\"", l.name)
|
||||
backend, _ := GetBackend(from)
|
||||
colors.Secondary.Printf("Restoring %s@%s → %s\n", snapshot, backend.name, to)
|
||||
|
||||
t, err := l.getType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t {
|
||||
case TypeLocal:
|
||||
to, err = filepath.Abs(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 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(buildRestoreCommand(l, to, snapshot, options))
|
||||
case TypeVolume:
|
||||
_, _, err = backend.ExecDocker(l, buildRestoreCommand(l, "/", snapshot, options))
|
||||
}
|
||||
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())
|
||||
errs := l.Backup(true, false, "")
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("Failed to backup location \"%s\":\n%w", l.name, errors.Join(errs...))
|
||||
}
|
||||
} else {
|
||||
if !flags.CRON_LEAN {
|
||||
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
93
internal/location_test.go
Normal file
93
internal/location_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
package internal
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetType(t *testing.T) {
|
||||
|
||||
t.Run("TypeLocal", func(t *testing.T) {
|
||||
l := Location{
|
||||
Type: "local",
|
||||
}
|
||||
result, err := l.getType()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
assertEqual(t, result, TypeLocal)
|
||||
})
|
||||
|
||||
t.Run("TypeVolume", func(t *testing.T) {
|
||||
l := Location{
|
||||
Type: "volume",
|
||||
}
|
||||
result, err := l.getType()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
assertEqual(t, result, TypeVolume)
|
||||
})
|
||||
|
||||
t.Run("Empty type", func(t *testing.T) {
|
||||
l := Location{
|
||||
Type: "",
|
||||
}
|
||||
result, err := l.getType()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
assertEqual(t, result, TypeLocal)
|
||||
})
|
||||
|
||||
t.Run("Invalid type", func(t *testing.T) {
|
||||
l := Location{
|
||||
Type: "foo",
|
||||
}
|
||||
_, err := l.getType()
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildTag(t *testing.T) {
|
||||
result := buildTag("foo", "bar")
|
||||
expected := "ar:foo:bar"
|
||||
assertEqual(t, result, expected)
|
||||
}
|
||||
|
||||
func TestGetLocationTags(t *testing.T) {
|
||||
l := Location{
|
||||
name: "foo",
|
||||
}
|
||||
result := l.getLocationTags()
|
||||
expected := "ar:location:foo"
|
||||
assertEqual(t, result, expected)
|
||||
}
|
||||
|
||||
func TestHasBackend(t *testing.T) {
|
||||
t.Run("backend present", func(t *testing.T) {
|
||||
l := Location{
|
||||
name: "foo",
|
||||
To: []string{"foo", "bar"},
|
||||
}
|
||||
result := l.hasBackend("foo")
|
||||
assertEqual(t, result, true)
|
||||
})
|
||||
|
||||
t.Run("backend absent", func(t *testing.T) {
|
||||
l := Location{
|
||||
name: "foo",
|
||||
To: []string{"bar", "baz"},
|
||||
}
|
||||
result := l.hasBackend("foo")
|
||||
assertEqual(t, result, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildRestoreCommand(t *testing.T) {
|
||||
l := Location{
|
||||
name: "foo",
|
||||
}
|
||||
result := buildRestoreCommand(l, "to", "snapshot", []string{"options"})
|
||||
expected := []string{"restore", "--target", "to", "--tag", "ar:location:foo", "snapshot", "options"}
|
||||
assertSliceEqual(t, result, expected)
|
||||
}
|
78
internal/lock/lock.go
Normal file
78
internal/lock/lock.go
Normal file
@ -0,0 +1,78 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var lock *viper.Viper
|
||||
var file string
|
||||
var once sync.Once
|
||||
|
||||
const (
|
||||
RUNNING = "running"
|
||||
)
|
||||
|
||||
func getLock() *viper.Viper {
|
||||
if lock == nil {
|
||||
|
||||
once.Do(func() {
|
||||
lock = viper.New()
|
||||
lock.SetDefault("running", false)
|
||||
p := viper.ConfigFileUsed()
|
||||
if p == "" {
|
||||
colors.Error.Println("cannot lock before reading config location")
|
||||
os.Exit(1)
|
||||
}
|
||||
file = path.Join(path.Dir(p), ".autorestic.lock.yml")
|
||||
if !flags.CRON_LEAN {
|
||||
colors.Faint.Println("Using lock:\t", file)
|
||||
}
|
||||
lock.SetConfigFile(file)
|
||||
lock.SetConfigType("yml")
|
||||
lock.ReadInConfig()
|
||||
})
|
||||
}
|
||||
return lock
|
||||
}
|
||||
|
||||
func setLockValue(key string, value interface{}) (*viper.Viper, error) {
|
||||
lock := getLock()
|
||||
|
||||
if key == RUNNING {
|
||||
value := value.(bool)
|
||||
if value && lock.GetBool(key) {
|
||||
colors.Error.Println("an instance is already running. exiting")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
lock.Set(key, value)
|
||||
if err := lock.WriteConfigAs(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
func GetCron(location string) int64 {
|
||||
return getLock().GetInt64("cron." + location)
|
||||
}
|
||||
|
||||
func SetCron(location string, value int64) {
|
||||
setLockValue("cron."+location, value)
|
||||
}
|
||||
|
||||
func Lock() error {
|
||||
_, err := setLockValue(RUNNING, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func Unlock() error {
|
||||
_, err := setLockValue(RUNNING, false)
|
||||
return err
|
||||
}
|
114
internal/lock/lock_test.go
Normal file
114
internal/lock/lock_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var testDirectory = "autorestic_test_tmp"
|
||||
|
||||
// All tests must share the same lock file as it is only initialized once
|
||||
func setup(t *testing.T) {
|
||||
d, err := os.MkdirTemp("", testDirectory)
|
||||
if err != nil {
|
||||
log.Fatalf("error creating temp dir: %v", err)
|
||||
return
|
||||
}
|
||||
// set config file location
|
||||
viper.SetConfigFile(d + "/.autorestic.yml")
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(d)
|
||||
viper.Reset()
|
||||
})
|
||||
}
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
setup(t)
|
||||
|
||||
t.Run("getLock", func(t *testing.T) {
|
||||
result := getLock().GetBool(RUNNING)
|
||||
|
||||
if result {
|
||||
t.Errorf("got %v, want %v", result, false)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("lock", func(t *testing.T) {
|
||||
lock, err := setLockValue(RUNNING, true)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := lock.GetBool(RUNNING)
|
||||
if !result {
|
||||
t.Errorf("got %v, want %v", result, true)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unlock", func(t *testing.T) {
|
||||
lock, err := setLockValue(RUNNING, false)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := lock.GetBool(RUNNING)
|
||||
if result {
|
||||
t.Errorf("got %v, want %v", result, false)
|
||||
}
|
||||
})
|
||||
|
||||
// locking a locked instance exits the instance
|
||||
// this trick to capture os.Exit(1) is discussed here:
|
||||
// https://talks.golang.org/2014/testing.slide#23
|
||||
t.Run("lock twice", func(t *testing.T) {
|
||||
if os.Getenv("CRASH") == "1" {
|
||||
err := Lock()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
// should fail
|
||||
Lock()
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestLock/lock_twice")
|
||||
cmd.Env = append(os.Environ(), "CRASH=1")
|
||||
err := cmd.Run()
|
||||
|
||||
err, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
t.Error("unexpected error")
|
||||
}
|
||||
expected := "exit status 1"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("got %q, want %q", err.Error(), expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("set cron", func(t *testing.T) {
|
||||
expected := int64(5)
|
||||
SetCron("foo", expected)
|
||||
|
||||
result, err := strconv.ParseInt(getLock().GetString("cron.foo"), 10, 64)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if result != expected {
|
||||
t.Errorf("got %d, want %d", result, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get cron", func(t *testing.T) {
|
||||
expected := int64(5)
|
||||
result := GetCron("foo")
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("got %d, want %d", result, expected)
|
||||
}
|
||||
})
|
||||
}
|
22
internal/metadata/extractor_added.go
Normal file
22
internal/metadata/extractor_added.go
Normal file
@ -0,0 +1,22 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type addedExtractor struct {
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
func (e addedExtractor) Matches(line string) bool {
|
||||
return e.re.MatchString(line)
|
||||
}
|
||||
func (e addedExtractor) Extract(metadata *BackupLogMetadata, line string) {
|
||||
// Sample line: "Added to the repo: 0 B"
|
||||
metadata.AddedSize = strings.TrimSpace(e.re.ReplaceAllString(line, "$2"))
|
||||
}
|
||||
|
||||
func NewAddedExtractor() MetadatExtractor {
|
||||
return addedExtractor{regexp.MustCompile(`(?i)^Added to the repo(sitory)?: ([\d\.]+ \w+)( \([\d\.]+[\w\s]+\))?`)}
|
||||
}
|
57
internal/metadata/extractor_changeset.go
Normal file
57
internal/metadata/extractor_changeset.go
Normal file
@ -0,0 +1,57 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ChangeSetExtractor struct {
|
||||
re *regexp.Regexp
|
||||
cleaner *regexp.Regexp
|
||||
saver changeSetSaver
|
||||
}
|
||||
|
||||
func (e ChangeSetExtractor) Matches(line string) bool {
|
||||
return e.re.MatchString(line)
|
||||
}
|
||||
func (e ChangeSetExtractor) Extract(metadata *BackupLogMetadata, line string) {
|
||||
// Sample line: "Files: 0 new, 0 changed, 2 unmodified"
|
||||
trimmed := strings.TrimSpace(e.re.ReplaceAllString(line, ""))
|
||||
splitted := strings.Split(trimmed, ",")
|
||||
var changeset BackupLogMetadataChangeset = BackupLogMetadataChangeset{}
|
||||
changeset.Added = e.cleaner.ReplaceAllString(splitted[0], "")
|
||||
changeset.Changed = e.cleaner.ReplaceAllString(splitted[1], "")
|
||||
changeset.Unmodified = e.cleaner.ReplaceAllString(splitted[2], "")
|
||||
e.saver.Save(metadata, changeset)
|
||||
}
|
||||
|
||||
type changeSetSaver interface {
|
||||
Save(metadata *BackupLogMetadata, changeset BackupLogMetadataChangeset)
|
||||
}
|
||||
|
||||
type fileSaver struct{}
|
||||
|
||||
func (f fileSaver) Save(metadata *BackupLogMetadata, changeset BackupLogMetadataChangeset) {
|
||||
metadata.Files = changeset
|
||||
}
|
||||
|
||||
type dirsSaver struct{}
|
||||
|
||||
func (d dirsSaver) Save(metadata *BackupLogMetadata, changeset BackupLogMetadataChangeset) {
|
||||
metadata.Dirs = changeset
|
||||
}
|
||||
|
||||
func NewFilesExtractor() MetadatExtractor {
|
||||
return ChangeSetExtractor{
|
||||
re: regexp.MustCompile(`(?i)^Files:`),
|
||||
cleaner: regexp.MustCompile(`[^\d]`),
|
||||
saver: fileSaver{},
|
||||
}
|
||||
}
|
||||
func NewDirsExtractor() MetadatExtractor {
|
||||
return ChangeSetExtractor{
|
||||
re: regexp.MustCompile(`(?i)^Dirs:`),
|
||||
cleaner: regexp.MustCompile(`[^\d]`),
|
||||
saver: dirsSaver{},
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user