mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2024-12-22 16:26:25 +00:00
commit
769ae4c566
4739
.codedoc/package-lock.json
generated
4739
.codedoc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
.github/workflows/build.yml
vendored
Normal file
23
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
name: Main
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16.3'
|
||||
- name: Build
|
||||
run: go run build/build.go
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: dist/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
23
.gitignore
vendored
23
.gitignore
vendored
@ -2,20 +2,11 @@
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# Build & Runtime
|
||||
bin
|
||||
lib
|
||||
data
|
||||
restore
|
||||
docker
|
||||
Dockerfile
|
||||
build
|
||||
dist
|
||||
|
||||
# Config
|
||||
.autorestic.yml
|
||||
.autorestic.lock
|
||||
.docker.yml
|
||||
.autorestic*
|
||||
|
||||
# Build & Dev
|
||||
test
|
||||
autorestic
|
||||
data
|
||||
dist
|
@ -1,4 +0,0 @@
|
||||
semi: false
|
||||
singleQuote: true
|
||||
trailingComma: 'es5'
|
||||
printWidth: 150
|
@ -1,3 +0,0 @@
|
||||
## 0.30
|
||||
|
||||
- prepare for new updates
|
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.
|
||||
|
83
build/build.go
Normal file
83
build/build.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Heavily inspired (copied) by the restic build file
|
||||
// https://github.com/restic/restic/blob/aa0faa8c7d7800b6ba7b11164fa2d3683f7f78aa/helpers/build-release-binaries/main.go#L225
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
)
|
||||
|
||||
var DIR, _ = filepath.Abs("./dist")
|
||||
|
||||
var targets = map[string][]string{
|
||||
"darwin": {"amd64"},
|
||||
"freebsd": {"386", "amd64", "arm"},
|
||||
"linux": {"386", "amd64", "arm", "arm64"},
|
||||
"netbsd": {"386", "amd64"},
|
||||
"openbsd": {"386", "amd64"},
|
||||
}
|
||||
|
||||
type buildOptions struct {
|
||||
Target, Arch, Version string
|
||||
}
|
||||
|
||||
func build(options buildOptions) error {
|
||||
fmt.Printf("Building %s %s\n", options.Target, options.Arch)
|
||||
out := fmt.Sprintf("autorestic_%s_%s_%s", options.Version, options.Target, options.Arch)
|
||||
out = path.Join(DIR, out)
|
||||
out, _ = filepath.Abs(out)
|
||||
fmt.Println(out)
|
||||
|
||||
// Build
|
||||
{
|
||||
c := exec.Command("go", "build", "-o", out, "./main.go")
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
c.Env = append(os.Environ(),
|
||||
"CGO_ENABLED=0",
|
||||
"GOOS="+options.Target,
|
||||
"GOARCH="+options.Arch,
|
||||
)
|
||||
err := c.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Compress
|
||||
{
|
||||
c := exec.Command("bzip2", out)
|
||||
c.Dir = DIR
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
err := c.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.RemoveAll(DIR)
|
||||
v := internal.VERSION
|
||||
for target, archs := range targets {
|
||||
for _, arch := range archs {
|
||||
err := build(buildOptions{
|
||||
Target: target,
|
||||
Arch: arch,
|
||||
Version: v,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
cmd/backup.go
Normal file
31
cmd/backup.go
Normal file
@ -0,0 +1,31 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var backupCmd = &cobra.Command{
|
||||
Use: "backup",
|
||||
Short: "Create backups for given locations",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
|
||||
CheckErr(internal.CheckConfig())
|
||||
|
||||
selected, err := internal.GetAllOrSelected(cmd, false)
|
||||
CheckErr(err)
|
||||
for _, name := range selected {
|
||||
location, _ := internal.GetLocation(name)
|
||||
location.Backup()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(backupCmd)
|
||||
internal.AddFlagsToCommand(backupCmd, false)
|
||||
}
|
26
cmd/check.go
Normal file
26
cmd/check.go
Normal file
@ -0,0 +1,26 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var checkCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check if everything is setup",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
|
||||
CheckErr(internal.CheckConfig())
|
||||
|
||||
colors.Success.Println("Everyting is fine.")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(checkCmd)
|
||||
}
|
25
cmd/cron.go
Normal file
25
cmd/cron.go
Normal file
@ -0,0 +1,25 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cronCmd = &cobra.Command{
|
||||
Use: "cron",
|
||||
Short: "Run cron job for automated backups",
|
||||
Long: `Intended to be mainly triggered by an automated system like systemd or crontab. For each location checks if a cron backup is due and runs it.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
|
||||
err = internal.RunCron()
|
||||
CheckErr(err)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(cronCmd)
|
||||
}
|
33
cmd/exec.go
Normal file
33
cmd/exec.go
Normal file
@ -0,0 +1,33 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var execCmd = &cobra.Command{
|
||||
Use: "exec",
|
||||
Short: "Execute arbitrary native restic commands for given backends",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
|
||||
CheckErr(internal.CheckConfig())
|
||||
|
||||
selected, err := internal.GetAllOrSelected(cmd, true)
|
||||
CheckErr(err)
|
||||
for _, name := range selected {
|
||||
colors.PrimaryPrint(" Executing on \"%s\" ", name)
|
||||
backend, _ := internal.GetBackend(name)
|
||||
backend.Exec(args)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(execCmd)
|
||||
internal.AddFlagsToCommand(execCmd, true)
|
||||
}
|
36
cmd/forget.go
Normal file
36
cmd/forget.go
Normal file
@ -0,0 +1,36 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var forgetCmd = &cobra.Command{
|
||||
Use: "forget",
|
||||
Short: "Forget and optionally prune snapshots according the specified policies",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
|
||||
CheckErr(internal.CheckConfig())
|
||||
|
||||
selected, err := internal.GetAllOrSelected(cmd, false)
|
||||
CheckErr(err)
|
||||
prune, _ := cmd.Flags().GetBool("prune")
|
||||
dry, _ := cmd.Flags().GetBool("dry-run")
|
||||
for _, name := range selected {
|
||||
location, _ := internal.GetLocation(name)
|
||||
err := location.Forget(prune, dry)
|
||||
CheckErr(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(forgetCmd)
|
||||
internal.AddFlagsToCommand(forgetCmd, false)
|
||||
forgetCmd.Flags().Bool("prune", false, "Also prune repository")
|
||||
forgetCmd.Flags().Bool("dry-run", false, "Do not write changes, show what would be affected")
|
||||
}
|
18
cmd/info.go
Normal file
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)
|
||||
}
|
39
cmd/restore.go
Normal file
39
cmd/restore.go
Normal file
@ -0,0 +1,39 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var restoreCmd = &cobra.Command{
|
||||
Use: "restore",
|
||||
Short: "Restore backup for location",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := lock.Lock()
|
||||
CheckErr(err)
|
||||
defer lock.Unlock()
|
||||
|
||||
location, _ := cmd.Flags().GetString("location")
|
||||
l, ok := internal.GetLocation(location)
|
||||
if !ok {
|
||||
CheckErr(fmt.Errorf("invalid location \"%s\"", location))
|
||||
}
|
||||
target, _ := cmd.Flags().GetString("to")
|
||||
from, _ := cmd.Flags().GetString("from")
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
err = l.Restore(target, from, force)
|
||||
CheckErr(err)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(restoreCmd)
|
||||
restoreCmd.Flags().BoolP("force", "f", false, "Force, target folder will be overwritten")
|
||||
restoreCmd.Flags().String("from", "", "Which backend to use")
|
||||
restoreCmd.Flags().String("to", "", "Where to restore the data")
|
||||
restoreCmd.Flags().StringP("location", "l", "", "Location to be restored")
|
||||
restoreCmd.MarkFlagRequired("location")
|
||||
}
|
61
cmd/root.go
Normal file
61
cmd/root.go
Normal file
@ -0,0 +1,61 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func CheckErr(err error) {
|
||||
if err != nil {
|
||||
colors.Error.Fprintln(os.Stderr, "Error:", err)
|
||||
lock.Unlock()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var cfgFile string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Version: internal.VERSION,
|
||||
Use: "autorestic",
|
||||
Short: "CLI Wrapper for restic",
|
||||
Long: "Documentation: https://autorestic.vercel.app",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
CheckErr(rootCmd.Execute())
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.autorestic.yml or ./.autorestic.yml)")
|
||||
rootCmd.PersistentFlags().BoolVar(&internal.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
|
||||
rootCmd.PersistentFlags().BoolVarP(&internal.VERBOSE, "verbose", "v", false, "verbose mode")
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if ci, _ := rootCmd.Flags().GetBool("ci"); ci {
|
||||
colors.DisableColors(true)
|
||||
internal.VERBOSE = true
|
||||
}
|
||||
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
home, err := homedir.Dir()
|
||||
CheckErr(err)
|
||||
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath(home)
|
||||
viper.SetConfigName(".autorestic")
|
||||
}
|
||||
viper.AutomaticEnv()
|
||||
internal.GetConfig()
|
||||
}
|
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) {
|
||||
noRestic, _ := cmd.Flags().GetBool("no-restic")
|
||||
bins.Uninstall(!noRestic)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(uninstallCmd)
|
||||
uninstallCmd.Flags().Bool("no-restic", false, "Do not uninstall restic.")
|
||||
}
|
21
cmd/upgrade.go
Normal file
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) {
|
||||
noRestic, _ := cmd.Flags().GetBool("no-restic")
|
||||
err := bins.Upgrade(!noRestic)
|
||||
CheckErr(err)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(upgradeCmd)
|
||||
upgradeCmd.Flags().Bool("no-restic", false, "Also update restic. Default: true")
|
||||
}
|
@ -2,7 +2,7 @@ import { configuration } from '@codedoc/core'
|
||||
|
||||
export const config = configuration({
|
||||
src: {
|
||||
base: 'docs',
|
||||
base: 'markdown',
|
||||
},
|
||||
dest: {
|
||||
html: './build',
|
10284
docs/.codedoc/package-lock.json
generated
Normal file
10284
docs/.codedoc/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
docs/.gitignore
vendored
Normal file
2
docs/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
build
|
@ -1,15 +0,0 @@
|
||||
# Check
|
||||
|
||||
```bash
|
||||
autorestic check [-b, --backend] [-a, --all]
|
||||
```
|
||||
|
||||
Cheks if one or more backend 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.
|
||||
|
||||
```bash
|
||||
autorestic check -b my-backend
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@ -1,11 +0,0 @@
|
||||
# Forget
|
||||
|
||||
```bash
|
||||
autorestic forget [-l, --location] [-a, --all] [--dry-run]
|
||||
```
|
||||
|
||||
This will prune and remove old data form the backends according to the [keep policy you have specified for the location](/locations/forget)
|
||||
|
||||
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.
|
||||
|
||||
> :ToCPrevNext
|
@ -1,21 +0,0 @@
|
||||
# 🐣 Examples
|
||||
|
||||
## Exec
|
||||
|
||||
### List all the snapshots for all the backends
|
||||
|
||||
```bash
|
||||
autorestic exec -a -- snapshots
|
||||
```
|
||||
|
||||
### Unlock a locked repository
|
||||
|
||||
If you accidentally cancelled a running operation this could be useful.
|
||||
|
||||
Only do this if you know what you are doing.
|
||||
|
||||
```bash
|
||||
autorestic exec -b my-backend -- unlock
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@ -1,58 +0,0 @@
|
||||
# Docker
|
||||
|
||||
autorestic supports docker volumes directly, without needing them to be mounted to the host filesystem.
|
||||
|
||||
> Available since version 0.13
|
||||
|
||||
Let see an example.
|
||||
|
||||
```yaml | docker-compose.yml
|
||||
version: '3.7'
|
||||
|
||||
volumes:
|
||||
data:
|
||||
name: my-data
|
||||
|
||||
services:
|
||||
api:
|
||||
image: alpine
|
||||
volumes:
|
||||
- data:/foo/bar
|
||||
```
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
locations:
|
||||
hello:
|
||||
from: 'volume:my-data'
|
||||
to:
|
||||
- remote
|
||||
options:
|
||||
forget:
|
||||
keep-last: 14 # Useful for limitations explained belowd
|
||||
|
||||
backends:
|
||||
remote: ...
|
||||
```
|
||||
|
||||
Now you can backup and restore as always.
|
||||
|
||||
```bash
|
||||
autorestic -l hello backup
|
||||
```
|
||||
|
||||
```bash
|
||||
autorestic -l hello restore
|
||||
```
|
||||
|
||||
If the volume does not exist on restore, autorestic will create it for you and then fill it with the data.
|
||||
|
||||
## Limitations
|
||||
|
||||
Unfortunately there are some limitations when backing up directly from a docker volume without mounting the volume to the host:
|
||||
|
||||
1. Incremental updates are not possible right now due to how the current docker mounting works. This means that it will take significantely more space.
|
||||
2. Exclude patterns and files also do not work as restic only sees a compressed tarball as source and not the actual data.
|
||||
|
||||
If you are curious or have ideas how to improve this, please [read more here](https://github.com/cupcakearmy/autorestic/issues/4#issuecomment-568771951). Any help is welcomed 🙂
|
||||
|
||||
> :ToCPrevNext
|
@ -1,18 +0,0 @@
|
||||
# Hooks
|
||||
|
||||
Sometimes you might want to stop an app/db before backing up data and start the service again after the backup has completed. This is what the hooks are made for. Simply add them to your location config. You can have as many commands as you wish.
|
||||
|
||||
```yml | .autorestic.yml
|
||||
locations:
|
||||
my-location:
|
||||
from: /data
|
||||
to: my-backend
|
||||
hooks:
|
||||
before:
|
||||
- echo "Hello"
|
||||
- echo "Human"
|
||||
after:
|
||||
- echo "kthxbye"
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@ -1,7 +1,7 @@
|
||||
[Home](/)
|
||||
[Quick Start](/quick)
|
||||
[Installation](/installation)
|
||||
[Configuration](/config)
|
||||
[Upgrade](/upgrade)
|
||||
|
||||
> :Collapse label=Locations
|
||||
>
|
@ -7,7 +7,11 @@ autorestic backup [-l, --location] [-a, --all]
|
||||
Performs a backup of all locations if the `-a` flag is passed. To only backup some locations pass one or more `-l` or `--location` flags.
|
||||
|
||||
```bash
|
||||
autorestic backup -l my-location
|
||||
# All
|
||||
autorestic backup -a
|
||||
|
||||
# Some
|
||||
autorestic backup -l foo -l bar
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
11
docs/markdown/cli/check.md
Normal file
11
docs/markdown/cli/check.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Check
|
||||
|
||||
```bash
|
||||
autorestic check
|
||||
```
|
||||
|
||||
Checks locations and backends are configured properly and initializes them if they are not already.
|
||||
|
||||
This is mostly an internal command, but useful to verify if a backend is configured correctly.
|
||||
|
||||
> :ToCPrevNext
|
13
docs/markdown/cli/forget.md
Normal file
13
docs/markdown/cli/forget.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Forget
|
||||
|
||||
```bash
|
||||
autorestic forget [-l, --location] [-a, --all] [--dry-run] [--prune]
|
||||
```
|
||||
|
||||
This will prune and remove old data form the backends according to the [keep policy you have specified for the location](/location/forget)
|
||||
|
||||
The `--dry-run` flag will do a dry run showing what would have been deleted, but won't touch the actual data.
|
||||
|
||||
The `--prune` flag will also [prune the data](https://restic.readthedocs.io/en/latest/060_forget.html#removing-backup-snapshots). This is a costly operation that can take longer, however it will free up the actual space.
|
||||
|
||||
> :ToCPrevNext
|
@ -1,14 +1,6 @@
|
||||
# General
|
||||
|
||||
## `--version`
|
||||
|
||||
Prints the current version
|
||||
|
||||
```bash
|
||||
autorestic --version
|
||||
```
|
||||
|
||||
## `--c, --config`
|
||||
## `-c, --config`
|
||||
|
||||
Specify the config file to be used.
|
||||
If omitted `autorestic` will search for for a `.autorestic.yml` in the current directory and your home directory.
|
||||
@ -19,11 +11,18 @@ autorestic -c /path/to/my/config.yml
|
||||
|
||||
## `--ci`
|
||||
|
||||
> Available since version 0.22
|
||||
Run the CLI in CI Mode, which means there will be no interactivity, no colors and automatically sets the `--verbose` flag.
|
||||
|
||||
Run the CLI in CI Mode, which means there will be no interactivity.
|
||||
This can be useful when you want to run cron e.g. as all the output will be saved.
|
||||
|
||||
```bash
|
||||
autorestic --ci
|
||||
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
|
||||
```
|
17
docs/markdown/examples.md
Normal file
17
docs/markdown/examples.md
Normal file
@ -0,0 +1,17 @@
|
||||
# 🐣 Examples
|
||||
|
||||
## List all the snapshots for all the backends
|
||||
|
||||
```bash
|
||||
autorestic exec -a -- 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 sure no other process is actually reading/writing to the repository of course.
|
||||
|
||||
```bash
|
||||
autorestic exec -b my-backend -- unlock
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
@ -1,16 +1,10 @@
|
||||
# `autorestic`
|
||||
# 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 🙂
|
||||
|
||||
<!-- ![Sketch](./docs/Sketch.png) -->
|
||||
|
||||
## ✈️ Roadmap
|
||||
|
||||
~~I would like to make the official `1.0` release in the coming months. Until then please feel free to file issues or feature requests so that the tool is as flexible as possible :)~~
|
||||
|
||||
As of version `0.18` crons are supported wich where the last feature missing for a `1.0`. Will test this for a few weeks and then it's time for the first "real" release! 🎉 Also we now have waaay better docs 📒
|
||||
> If you are coming from `0.x` see the [upgrade guide](/upgrade).
|
||||
|
||||
## 🌈 Features
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
Often it is usefull to trigger backups autmatically. For this we can specify a `cron` attribute to each location.
|
||||
|
||||
> Available since version 0.18
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
locations:
|
||||
my-location:
|
||||
@ -46,4 +44,6 @@ PATH="/usr/local/bin:/usr/bin:/bin"
|
||||
|
||||
Now you can add as many `cron` attributes as you wish in the config file ⏱
|
||||
|
||||
> Also note that manually triggered backups with `autorestic backup` will not influence the cron timeline, they are willingly not linked.
|
||||
|
||||
> :ToCPrevNext
|
43
docs/markdown/location/docker.md
Normal file
43
docs/markdown/location/docker.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Docker
|
||||
|
||||
autorestic supports docker volumes directly, without needing them to be mounted to the host filesystem.
|
||||
|
||||
```yaml | docker-compose.yml
|
||||
version: '3.7'
|
||||
|
||||
volumes:
|
||||
data:
|
||||
name: my-data
|
||||
|
||||
services:
|
||||
api:
|
||||
image: alpine
|
||||
volumes:
|
||||
- data:/foo/bar
|
||||
```
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
locations:
|
||||
- name: hello
|
||||
from: volume:my-data
|
||||
to:
|
||||
- remote
|
||||
|
||||
backends:
|
||||
- name: remote
|
||||
# ...
|
||||
```
|
||||
|
||||
Now you can backup and restore as always.
|
||||
|
||||
```bash
|
||||
autorestic backup -l hello
|
||||
```
|
||||
|
||||
```bash
|
||||
autorestic restore -l hello
|
||||
```
|
||||
|
||||
The volume has to exists whenever backing up or restoring.
|
||||
|
||||
> :ToCPrevNext
|
20
docs/markdown/location/hooks.md
Normal file
20
docs/markdown/location/hooks.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Hooks
|
||||
|
||||
If you want to perform some commands before and/or after a backup, you can use hooks.
|
||||
|
||||
They consist of a list of `before`/`after` commands that will be executed in the same directory as the target `from`.
|
||||
|
||||
```yml | .autorestic.yml
|
||||
locations:
|
||||
my-location:
|
||||
from: /data
|
||||
to: my-backend
|
||||
hooks:
|
||||
before:
|
||||
- echo "Hello"
|
||||
- echo "Human"
|
||||
after:
|
||||
- echo "kthxbye"
|
||||
```
|
||||
|
||||
> :ToCPrevNext
|
10
docs/markdown/qa.md
Normal file
10
docs/markdown/qa.md
Normal file
@ -0,0 +1,10 @@
|
||||
# ❓ QA
|
||||
|
||||
## My config file was moved?
|
||||
|
||||
This happens when autorestic needs to write to the config file: e.g. when we are generating a key for you.
|
||||
Unfortunately during this process formatting and comments are lost because the `yaml` library used is not comment and/or format aware.
|
||||
|
||||
That is why autorestic will place a copy of your old config next to the one we are writing to.
|
||||
|
||||
> :ToCPrevNext
|
@ -17,30 +17,35 @@ 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 redudancy.
|
||||
- One `backend` can also be the target for multiple `locations`.
|
||||
- **Backup the config file as it will contain the generated keys**. If you don't have a copy of that keys, the backups are useless as they are encrypted and data will be not recoverable.
|
||||
|
||||
> **⚠️ WARNING ⚠️**
|
||||
>
|
||||
> Note that the data is automatically encrypted on the server. The key will be generated and added to your config file. Every backend will have a separate key. **You should keep a copy of the keys or config file somewhere in case your server dies**. Otherwise DATA IS LOST!
|
||||
|
||||
```yaml | .autorestic.yml
|
||||
locations:
|
||||
home:
|
||||
- name: home
|
||||
from: /home/me
|
||||
to: remote
|
||||
|
||||
important:
|
||||
- name: important
|
||||
from: /path/to/important/stuff
|
||||
to:
|
||||
- remote
|
||||
- hdd
|
||||
|
||||
backends:
|
||||
remote:
|
||||
- name: remote
|
||||
type: s3
|
||||
path: 's3.amazonaws.com/bucket_name'
|
||||
key: some-random-password-198rc79r8y1029c8yfewj8f1u0ef87yh198uoieufy
|
||||
AWS_ACCESS_KEY_ID: account_id
|
||||
AWS_SECRET_ACCESS_KEY: account_key
|
||||
|
||||
hdd:
|
||||
- name: hdd
|
||||
type: local
|
||||
path: /mnt/my_external_storage
|
||||
key: 'if not key is set it will be generated for you'
|
||||
```
|
||||
|
||||
## Check
|
28
docs/markdown/upgrade.md
Normal file
28
docs/markdown/upgrade.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Update
|
||||
|
||||
## From `0.x` to `1.0`
|
||||
|
||||
Most of the config file is remained compatible, however to clean up the backends custom environment variables were moved from the root object to an `env` object.
|
||||
|
||||
```yaml
|
||||
# Before
|
||||
remote:
|
||||
type: b2
|
||||
path: bucket:path/to/backup
|
||||
key: some random encryption key
|
||||
B2_ACCOUNT_ID: id
|
||||
B2_ACCOUNT_KEY: key
|
||||
|
||||
# After
|
||||
remote:
|
||||
type: b2
|
||||
path: bucket:path/to/backup
|
||||
key: some random encryption key
|
||||
env:
|
||||
B2_ACCOUNT_ID: id
|
||||
B2_ACCOUNT_KEY: key
|
||||
```
|
||||
|
||||
Other than the config file there is a new `-v, --verbose` flag which shows the output of native commands, which are now hidden by default.
|
||||
|
||||
> :ToCPrevNext
|
1828
docs/package-lock.json
generated
Normal file
1828
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
docs/package.json
Normal file
10
docs/package.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "codedoc install && codedoc build",
|
||||
"dev": "codedoc serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codedoc/cli": "^0.2.8"
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
# ❓ QA
|
||||
|
||||
## My config file was moved?
|
||||
|
||||
This happens when autorestic needs to write to the config file. This happend e.g. when we are generating a key for you.
|
||||
Unfortunately during this process formatting and comments are lost. That is why autorestic will place a copy of your old config next to the one we are writing to.
|
||||
|
||||
> :ToCPrevNext
|
13
go.mod
Normal file
13
go.mod
Normal file
@ -0,0 +1,13 @@
|
||||
module github.com/cupcakearmy/autorestic
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/buger/goterm v1.0.0
|
||||
github.com/fatih/color v1.10.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/robfig/cron v1.2.0
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/spf13/viper v1.7.1
|
||||
)
|
328
go.sum
Normal file
328
go.sum
Normal file
@ -0,0 +1,328 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/buger/goterm v1.0.0 h1:ZB6uUlY8+sjJyFGzz2WpRqX2XYPeXVgtZAOJMwOsTWM=
|
||||
github.com/buger/goterm v1.0.0/go.mod h1:16STi3LquiscTIHA8SXUNKEa/Cnu4ZHBH8NsCaWgso0=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
|
||||
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
|
||||
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
35
install.sh
35
install.sh
@ -1,22 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
shopt -s nocaseglob
|
||||
|
||||
OUT_FILE=/usr/local/bin/autorestic
|
||||
|
||||
if [[ "$OSTYPE" == "linux-gnu" ]]; then
|
||||
TYPE=linux
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
TYPE=macos
|
||||
# Type
|
||||
NATIVE_OS=$(uname | tr '[:upper:]' '[:lower:]')
|
||||
if [[ $NATIVE_OS == *"linux"* ]]; then
|
||||
OS=linux
|
||||
elif [[ $NATIVE_OS == *"darwin"* ]]; then
|
||||
OS=darwin
|
||||
else
|
||||
echo "Unsupported OS"
|
||||
exit
|
||||
echo "Could not determine OS automatically, please check the release page manually: https://github.com/cupcakearmy/autorestic/releases"
|
||||
exit 1
|
||||
fi
|
||||
echo $OS
|
||||
|
||||
NATIVE_ARCH=$(uname -m)
|
||||
if [[ $NATIVE_ARCH == *"x86_64"* ]]; then
|
||||
ARCH=amd64
|
||||
elif [[ $NATIVE_ARCH == *"x86"* ]]; then
|
||||
ARCH=386
|
||||
else
|
||||
echo "Could not determine Architecure automatically, please check the release page manually: https://github.com/cupcakearmy/autorestic/releases"
|
||||
exit 1
|
||||
fi
|
||||
echo $ARCH
|
||||
|
||||
curl -s https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
|
||||
| grep "browser_download_url.*_${TYPE}" \
|
||||
| grep "browser_download_url.*_${OS}_${ARCH}" \
|
||||
| cut -d : -f 2,3 \
|
||||
| tr -d \" \
|
||||
| wget -O ${OUT_FILE} -i -
|
||||
| wget -O "${OUT_FILE}.bz2" -i -
|
||||
bzip2 -fd "${OUT_FILE}.bz2"
|
||||
chmod +x ${OUT_FILE}
|
||||
|
||||
autorestic install
|
||||
autorestic --help
|
||||
echo "Succefsully installed autorestic"
|
150
internal/backend.go
Normal file
150
internal/backend.go
Normal file
@ -0,0 +1,150 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Backend struct {
|
||||
name string
|
||||
Type string `mapstructure:"type,omitempty"`
|
||||
Path string `mapstructure:"path,omitempty"`
|
||||
Key string `mapstructure:"key,omitempty"`
|
||||
Env map[string]string `mapstructure:"env,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 "b2", "azure", "gs", "s3", "sftp", "rest":
|
||||
return fmt.Sprintf("%s:%s", b.Type, b.Path), nil
|
||||
default:
|
||||
return "", fmt.Errorf("backend type \"%s\" is invalid", b.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (b Backend) getEnv() (map[string]string, error) {
|
||||
env := make(map[string]string)
|
||||
env["RESTIC_PASSWORD"] = b.Key
|
||||
repo, err := b.generateRepo()
|
||||
env["RESTIC_REPOSITORY"] = repo
|
||||
for key, value := range b.Env {
|
||||
env[strings.ToUpper(key)] = value
|
||||
}
|
||||
return env, err
|
||||
}
|
||||
|
||||
func generateRandomKey() string {
|
||||
b := make([]byte, 64)
|
||||
rand.Read(b)
|
||||
key := base64.StdEncoding.EncodeToString(b)
|
||||
key = strings.ReplaceAll(key, "=", "")
|
||||
key = strings.ReplaceAll(key, "+", "")
|
||||
key = strings.ReplaceAll(key, "/", "")
|
||||
return key
|
||||
}
|
||||
|
||||
func (b Backend) validate() error {
|
||||
if b.Type == "" {
|
||||
return fmt.Errorf(`Backend "%s" has no "type"`, b.name)
|
||||
}
|
||||
if b.Path == "" {
|
||||
return fmt.Errorf(`Backend "%s" has no "path"`, b.name)
|
||||
}
|
||||
if b.Key == "" {
|
||||
key := generateRandomKey()
|
||||
b.Key = key
|
||||
c := GetConfig()
|
||||
tmp := c.Backends[b.name]
|
||||
tmp.Key = key
|
||||
tmp.name = ""
|
||||
c.Backends[b.name] = tmp
|
||||
file := viper.ConfigFileUsed()
|
||||
if err := CopyFile(file, file+".old"); err != nil {
|
||||
return err
|
||||
}
|
||||
colors.Secondary.Println("Saved a backup copy of your file next the the original.")
|
||||
viper.Set("backends", c.Backends)
|
||||
viper.WriteConfig()
|
||||
}
|
||||
env, err := b.getEnv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options := ExecuteOptions{Envs: env}
|
||||
// Check if already initialized
|
||||
_, err = ExecuteResticCommand(options, "snapshots")
|
||||
if err == nil {
|
||||
return nil
|
||||
} else {
|
||||
// If not initialize
|
||||
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
|
||||
out, err := ExecuteResticCommand(options, "init")
|
||||
if VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (b Backend) Exec(args []string) error {
|
||||
env, err := b.getEnv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options := ExecuteOptions{Envs: env}
|
||||
out, err := ExecuteResticCommand(options, args...)
|
||||
if VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b Backend) ExecDocker(l Location, args []string) error {
|
||||
env, err := b.getEnv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
volume := l.getVolumeName()
|
||||
path, _ := l.getPath()
|
||||
options := ExecuteOptions{
|
||||
Command: "docker",
|
||||
Envs: env,
|
||||
}
|
||||
docker := []string{
|
||||
"run", "--rm",
|
||||
"--entrypoint", "ash",
|
||||
"--workdir", path,
|
||||
"--volume", volume + ":" + path,
|
||||
}
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
docker = append(docker, "--hostname", hostname)
|
||||
}
|
||||
if b.Type == "local" {
|
||||
actual := env["RESTIC_REPOSITORY"]
|
||||
docker = append(docker, "--volume", actual+":"+"/repo")
|
||||
env["RESTIC_REPOSITORY"] = "/repo"
|
||||
}
|
||||
for key, value := range env {
|
||||
docker = append(docker, "--env", key+"="+value)
|
||||
}
|
||||
docker = append(docker, "restic/restic", "-c", "restic "+strings.Join(args, " "))
|
||||
out, err := ExecuteCommand(options, docker...)
|
||||
if VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
return err
|
||||
}
|
140
internal/bins/bins.go
Normal file
140
internal/bins/bins.go
Normal file
@ -0,0 +1,140 @@
|
||||
package bins
|
||||
|
||||
import (
|
||||
"compress/bzip2"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/cupcakearmy/autorestic/internal"
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
)
|
||||
|
||||
const INSTALL_PATH = "/usr/local/bin"
|
||||
|
||||
type GithubReleaseAsset struct {
|
||||
Name string `json:"name"`
|
||||
Link string `json:"browser_download_url"`
|
||||
}
|
||||
type GithubRelease struct {
|
||||
Tag string `json:"tag_name"`
|
||||
Assets []GithubReleaseAsset `json:"assets"`
|
||||
}
|
||||
|
||||
func dlJSON(url string) (GithubRelease, error) {
|
||||
var parsed GithubRelease
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return parsed, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return parsed, err
|
||||
|
||||
}
|
||||
json.Unmarshal(body, &parsed)
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func Uninstall(restic bool) error {
|
||||
if err := os.Remove(path.Join(INSTALL_PATH, "autorestic")); err != nil {
|
||||
colors.Error.Println(err)
|
||||
}
|
||||
if restic {
|
||||
if err := os.Remove(path.Join(INSTALL_PATH, "restic")); err != nil {
|
||||
colors.Error.Println(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadAndInstallAsset(body GithubRelease, name string) error {
|
||||
ending := fmt.Sprintf("_%s_%s.bz2", runtime.GOOS, runtime.GOARCH)
|
||||
for _, asset := range body.Assets {
|
||||
if strings.HasSuffix(asset.Name, ending) {
|
||||
// Download archive
|
||||
colors.Faint.Println("Downloading:", asset.Link)
|
||||
resp, err := http.Get(asset.Link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Uncompress
|
||||
bz := bzip2.NewReader(resp.Body)
|
||||
|
||||
// Save binary
|
||||
file, err := os.Create(path.Join(INSTALL_PATH, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Chmod(0755)
|
||||
defer file.Close()
|
||||
io.Copy(file, bz)
|
||||
|
||||
colors.Success.Printf("Successfully installed '%s' under %s\n", name, INSTALL_PATH)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("could not find right binary for your system, please install restic manually. https://bit.ly/2Y1Rzai")
|
||||
}
|
||||
|
||||
func InstallRestic() error {
|
||||
installed := internal.CheckIfCommandIsCallable("restic")
|
||||
if installed {
|
||||
colors.Body.Println("restic already installed")
|
||||
return nil
|
||||
} else {
|
||||
if body, err := dlJSON("https://api.github.com/repos/restic/restic/releases/latest"); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return downloadAndInstallAsset(body, "restic")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func upgradeRestic() error {
|
||||
out, err := internal.ExecuteCommand(internal.ExecuteOptions{
|
||||
Command: "restic",
|
||||
}, "self-update")
|
||||
colors.Faint.Println(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func Upgrade(restic bool) error {
|
||||
// Upgrade restic
|
||||
if restic {
|
||||
InstallRestic()
|
||||
upgradeRestic()
|
||||
}
|
||||
|
||||
// Upgrade self
|
||||
current, err := semver.ParseTolerant(internal.VERSION)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := dlJSON("https://api.github.com/repos/cupcakearmy/autorestic/releases/latest")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
latest, err := semver.ParseTolerant(body.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current.GT(latest) {
|
||||
downloadAndInstallAsset(body, "autorestic")
|
||||
colors.Success.Println("Updated autorestic")
|
||||
} else {
|
||||
colors.Body.Println("Already up to date")
|
||||
}
|
||||
return nil
|
||||
}
|
29
internal/colors/colors.go
Normal file
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)
|
||||
}
|
199
internal/config.go
Normal file
199
internal/config.go
Normal file
@ -0,0 +1,199 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const VERSION = "1.0.0"
|
||||
|
||||
var CI bool = false
|
||||
var VERBOSE bool = false
|
||||
|
||||
type Config struct {
|
||||
Locations map[string]Location `mapstructure:"locations"`
|
||||
Backends map[string]Backend `mapstructure:"backends"`
|
||||
}
|
||||
|
||||
var once sync.Once
|
||||
var config *Config
|
||||
|
||||
func GetConfig() *Config {
|
||||
if config == nil {
|
||||
once.Do(func() {
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
colors.Faint.Println("Using config file:", viper.ConfigFileUsed())
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
config = &Config{}
|
||||
if err := viper.UnmarshalExact(config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func GetPathRelativeToConfig(p string) (string, error) {
|
||||
if path.IsAbs(p) {
|
||||
return p, nil
|
||||
} else if strings.HasPrefix(p, "~") {
|
||||
home, err := homedir.Dir()
|
||||
return path.Join(home, strings.TrimPrefix(p, "~")), err
|
||||
} else {
|
||||
return path.Join(path.Dir(viper.ConfigFileUsed()), p), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) Describe() {
|
||||
// Locations
|
||||
for name, l := range c.Locations {
|
||||
var tmp string
|
||||
colors.PrimaryPrint(`Location: "%s"`, name)
|
||||
|
||||
colors.PrintDescription("From", l.From)
|
||||
|
||||
tmp = ""
|
||||
for _, to := range l.To {
|
||||
tmp += fmt.Sprintf("\t%s %s\n", colors.Success.Sprint("→"), to)
|
||||
}
|
||||
colors.PrintDescription("To", tmp)
|
||||
|
||||
if l.Cron != "" {
|
||||
colors.PrintDescription("Cron", l.Cron)
|
||||
}
|
||||
|
||||
after, before := len(l.Hooks.After), len(l.Hooks.Before)
|
||||
if after+before > 0 {
|
||||
tmp = ""
|
||||
if before > 0 {
|
||||
tmp += "\tBefore"
|
||||
for _, cmd := range l.Hooks.Before {
|
||||
tmp += colors.Faint.Sprintf("\n\t ▶ %s", cmd)
|
||||
}
|
||||
}
|
||||
if after > 0 {
|
||||
tmp += "\n\tAfter"
|
||||
for _, cmd := range l.Hooks.After {
|
||||
tmp += colors.Faint.Sprintf("\n\t ▶ %s", cmd)
|
||||
}
|
||||
}
|
||||
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(`restic was not found. Install either with "autorestic install" or manually`)
|
||||
}
|
||||
for name, backend := range c.Backends {
|
||||
backend.name = name
|
||||
if err := backend.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for name, location := range c.Locations {
|
||||
location.name = name
|
||||
if err := location.validate(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAllOrSelected(cmd *cobra.Command, backends bool) ([]string, error) {
|
||||
var list []string
|
||||
if backends {
|
||||
for name := range config.Backends {
|
||||
list = append(list, name)
|
||||
}
|
||||
} else {
|
||||
for name := range config.Locations {
|
||||
list = append(list, name)
|
||||
}
|
||||
}
|
||||
|
||||
all, _ := cmd.Flags().GetBool("all")
|
||||
if all {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
var selected []string
|
||||
if backends {
|
||||
selected, _ = cmd.Flags().GetStringSlice("backend")
|
||||
} else {
|
||||
selected, _ = cmd.Flags().GetStringSlice("location")
|
||||
}
|
||||
for _, s := range selected {
|
||||
found := false
|
||||
for _, l := range list {
|
||||
if l == s {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if backends {
|
||||
return nil, fmt.Errorf("invalid backend \"%s\"", s)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid location \"%s\"", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(selected) == 0 {
|
||||
return selected, fmt.Errorf("nothing selected, aborting")
|
||||
}
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
func AddFlagsToCommand(cmd *cobra.Command, backend bool) {
|
||||
cmd.PersistentFlags().BoolP("all", "a", false, "Backup all locations")
|
||||
if backend {
|
||||
cmd.PersistentFlags().StringSliceP("backend", "b", []string{}, "backends")
|
||||
} else {
|
||||
cmd.PersistentFlags().StringSliceP("location", "l", []string{}, "Locations")
|
||||
}
|
||||
}
|
12
internal/cron.go
Normal file
12
internal/cron.go
Normal file
@ -0,0 +1,12 @@
|
||||
package internal
|
||||
|
||||
func RunCron() error {
|
||||
c := GetConfig()
|
||||
for name, l := range c.Locations {
|
||||
l.name = name
|
||||
if err := l.RunCron(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
299
internal/location.go
Normal file
299
internal/location.go
Normal file
@ -0,0 +1,299 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
type LocationType string
|
||||
|
||||
const (
|
||||
TypeLocal LocationType = "local"
|
||||
TypeVolume LocationType = "volume"
|
||||
VolumePrefix string = "volume:"
|
||||
)
|
||||
|
||||
type HookArray = []string
|
||||
|
||||
type Hooks struct {
|
||||
Before HookArray `mapstructure:"before"`
|
||||
After HookArray `mapstructure:"after"`
|
||||
}
|
||||
|
||||
type Options map[string]map[string][]string
|
||||
|
||||
type Location struct {
|
||||
name string `mapstructure:",omitempty"`
|
||||
From string `mapstructure:"from,omitempty"`
|
||||
To []string `mapstructure:"to,omitempty"`
|
||||
Hooks Hooks `mapstructure:"hooks,omitempty"`
|
||||
Cron string `mapstructure:"cron,omitempty"`
|
||||
Options Options `mapstructure:"options,omitempty"`
|
||||
}
|
||||
|
||||
func GetLocation(name string) (Location, bool) {
|
||||
l, ok := GetConfig().Locations[name]
|
||||
l.name = name
|
||||
return l, ok
|
||||
}
|
||||
|
||||
func (l Location) validate(c *Config) error {
|
||||
if l.From == "" {
|
||||
return fmt.Errorf(`Location "%s" is missing "from" key`, l.name)
|
||||
}
|
||||
if len(l.To) == 0 {
|
||||
return fmt.Errorf(`Location "%s" has no "to" targets`, l.name)
|
||||
}
|
||||
// Check if backends are all valid
|
||||
for _, to := range l.To {
|
||||
_, ok := GetBackend(to)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid backend `%s`", to)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Location) getOptions(key string) []string {
|
||||
var options []string
|
||||
saved := l.Options[key]
|
||||
for k, values := range saved {
|
||||
for _, value := range values {
|
||||
options = append(options, fmt.Sprintf("--%s", k), value)
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func ExecuteHooks(commands []string, options ExecuteOptions) error {
|
||||
if len(commands) == 0 {
|
||||
return nil
|
||||
}
|
||||
colors.Secondary.Println("\nRunning hooks")
|
||||
for _, command := range commands {
|
||||
colors.Body.Println("> " + command)
|
||||
out, err := ExecuteCommand(options, "-c", command)
|
||||
if VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
colors.Body.Println("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Location) getType() LocationType {
|
||||
if strings.HasPrefix(l.From, VolumePrefix) {
|
||||
return TypeVolume
|
||||
}
|
||||
return TypeLocal
|
||||
}
|
||||
|
||||
func (l Location) getVolumeName() string {
|
||||
return strings.TrimPrefix(l.From, VolumePrefix)
|
||||
}
|
||||
|
||||
func (l Location) getPath() (string, error) {
|
||||
t := l.getType()
|
||||
switch t {
|
||||
case TypeLocal:
|
||||
if path, err := GetPathRelativeToConfig(l.From); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return path, nil
|
||||
}
|
||||
case TypeVolume:
|
||||
return "/volume/" + l.name + "/" + l.getVolumeName(), nil
|
||||
}
|
||||
return "", fmt.Errorf("could not get path for location \"%s\"", l.name)
|
||||
}
|
||||
|
||||
func (l Location) Backup() error {
|
||||
colors.PrimaryPrint(" Backing up location \"%s\" ", l.name)
|
||||
t := l.getType()
|
||||
options := ExecuteOptions{
|
||||
Command: "bash",
|
||||
}
|
||||
|
||||
if t == TypeLocal {
|
||||
dir, _ := GetPathRelativeToConfig(l.From)
|
||||
options.Dir = dir
|
||||
}
|
||||
|
||||
// Hooks
|
||||
if err := ExecuteHooks(l.Hooks.Before, options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Backup
|
||||
for _, to := range l.To {
|
||||
backend, _ := GetBackend(to)
|
||||
colors.Secondary.Printf("Backend: %s\n", backend.name)
|
||||
env, err := backend.getEnv()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
flags := l.getOptions("backup")
|
||||
cmd := []string{"backup"}
|
||||
cmd = append(cmd, flags...)
|
||||
cmd = append(cmd, ".")
|
||||
backupOptions := ExecuteOptions{
|
||||
Dir: options.Dir,
|
||||
Envs: env,
|
||||
}
|
||||
|
||||
var out string
|
||||
|
||||
switch t {
|
||||
case TypeLocal:
|
||||
out, err = ExecuteResticCommand(backupOptions, cmd...)
|
||||
if VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
case TypeVolume:
|
||||
err = backend.ExecDocker(l, cmd)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// After hooks
|
||||
if err := ExecuteHooks(l.Hooks.After, options); err != nil {
|
||||
return err
|
||||
}
|
||||
colors.Success.Println("Done")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Location) Forget(prune bool, dry bool) error {
|
||||
colors.PrimaryPrint("Forgetting for location \"%s\"", l.name)
|
||||
|
||||
path, err := l.getPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, to := range l.To {
|
||||
backend, _ := GetBackend(to)
|
||||
colors.Secondary.Printf("For backend \"%s\"\n", backend.name)
|
||||
env, err := backend.getEnv()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
options := ExecuteOptions{
|
||||
Envs: env,
|
||||
}
|
||||
flags := l.getOptions("forget")
|
||||
cmd := []string{"forget", "--path", path}
|
||||
if prune {
|
||||
cmd = append(cmd, "--prune")
|
||||
}
|
||||
if dry {
|
||||
cmd = append(cmd, "--dry-run")
|
||||
}
|
||||
cmd = append(cmd, flags...)
|
||||
out, err := ExecuteResticCommand(options, cmd...)
|
||||
if VERBOSE {
|
||||
colors.Faint.Println(out)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
colors.Success.Println("Done")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Location) hasBackend(backend string) bool {
|
||||
for _, b := range l.To {
|
||||
if b == backend {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (l Location) Restore(to, from string, force bool) error {
|
||||
if from == "" {
|
||||
from = l.To[0]
|
||||
} else if !l.hasBackend(from) {
|
||||
return fmt.Errorf("invalid backend: \"%s\"", from)
|
||||
}
|
||||
|
||||
to, err := filepath.Abs(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
colors.PrimaryPrint("Restoring location \"%s\"", l.name)
|
||||
|
||||
backend, _ := GetBackend(from)
|
||||
path, err := l.getPath()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
colors.Secondary.Println("Restoring lastest snapshot")
|
||||
colors.Body.Printf("%s → %s.\n", from, path)
|
||||
switch l.getType() {
|
||||
case TypeLocal:
|
||||
// Check if target is empty
|
||||
if !force {
|
||||
notEmptyError := fmt.Errorf("target %s is not empty", to)
|
||||
_, err = os.Stat(to)
|
||||
if err == nil {
|
||||
files, err := ioutil.ReadDir(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(files) > 0 {
|
||||
return notEmptyError
|
||||
}
|
||||
} else {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
err = backend.Exec([]string{"restore", "--target", to, "--path", path, "latest"})
|
||||
case TypeVolume:
|
||||
err = backend.ExecDocker(l, []string{"restore", "--target", ".", "--path", path, "latest"})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
colors.Success.Println("Done")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Location) RunCron() error {
|
||||
if l.Cron == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
schedule, err := cron.ParseStandard(l.Cron)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
last := time.Unix(lock.GetCron(l.name), 0)
|
||||
next := schedule.Next(last)
|
||||
now := time.Now()
|
||||
if now.After(next) {
|
||||
lock.SetCron(l.name, now.Unix())
|
||||
l.Backup()
|
||||
} else {
|
||||
colors.Body.Printf("Skipping \"%s\", not due yet.\n", l.name)
|
||||
}
|
||||
return nil
|
||||
}
|
62
internal/lock/lock.go
Normal file
62
internal/lock/lock.go
Normal file
@ -0,0 +1,62 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var lock *viper.Viper
|
||||
var file string
|
||||
var once sync.Once
|
||||
|
||||
func getLock() *viper.Viper {
|
||||
if lock == nil {
|
||||
|
||||
once.Do(func() {
|
||||
lock = viper.New()
|
||||
lock.SetDefault("running", false)
|
||||
p := path.Dir(viper.ConfigFileUsed())
|
||||
file = path.Join(p, ".autorestic.lock.yml")
|
||||
lock.SetConfigFile(file)
|
||||
lock.SetConfigType("yml")
|
||||
lock.ReadInConfig()
|
||||
})
|
||||
}
|
||||
return lock
|
||||
}
|
||||
|
||||
func setLock(locked bool) error {
|
||||
lock := getLock()
|
||||
if locked {
|
||||
running := lock.GetBool("running")
|
||||
if running {
|
||||
return errors.New("an instance is already running")
|
||||
}
|
||||
}
|
||||
lock.Set("running", locked)
|
||||
if err := lock.WriteConfigAs(file); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetCron(location string) int64 {
|
||||
lock := getLock()
|
||||
return lock.GetInt64("cron." + location)
|
||||
}
|
||||
|
||||
func SetCron(location string, value int64) {
|
||||
lock.Set("cron."+location, value)
|
||||
lock.WriteConfigAs(file)
|
||||
}
|
||||
|
||||
func Lock() error {
|
||||
return setLock(true)
|
||||
}
|
||||
|
||||
func Unlock() error {
|
||||
return setLock(false)
|
||||
}
|
20
internal/terminal/main.go
Normal file
20
internal/terminal/main.go
Normal file
@ -0,0 +1,20 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
tm "github.com/buger/goterm"
|
||||
)
|
||||
|
||||
func Clear() {
|
||||
tm.Clear()
|
||||
}
|
||||
|
||||
func Append(line string) {
|
||||
tm.Println(line)
|
||||
tm.Flush()
|
||||
}
|
||||
|
||||
func Replace(line string) {
|
||||
tm.MoveCursorUp(1)
|
||||
tm.Print("\033[K")
|
||||
Append(line)
|
||||
}
|
74
internal/utils.go
Normal file
74
internal/utils.go
Normal file
@ -0,0 +1,74 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||
)
|
||||
|
||||
func CheckIfCommandIsCallable(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func CheckIfResticIsCallable() bool {
|
||||
return CheckIfCommandIsCallable("restic")
|
||||
}
|
||||
|
||||
type ExecuteOptions struct {
|
||||
Command string
|
||||
Envs map[string]string
|
||||
Dir string
|
||||
}
|
||||
|
||||
func ExecuteCommand(options ExecuteOptions, args ...string) (string, error) {
|
||||
cmd := exec.Command(options.Command, args...)
|
||||
env := os.Environ()
|
||||
for k, v := range options.Envs {
|
||||
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
cmd.Env = env
|
||||
cmd.Dir = options.Dir
|
||||
|
||||
if VERBOSE {
|
||||
colors.Faint.Printf("> Executing: %s\n", cmd)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
var error bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &error
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return error.String(), err
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func ExecuteResticCommand(options ExecuteOptions, args ...string) (string, error) {
|
||||
options.Command = "restic"
|
||||
return ExecuteCommand(options, args...)
|
||||
}
|
||||
|
||||
func CopyFile(from, to string) error {
|
||||
original, err := os.Open("original.txt")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer original.Close()
|
||||
|
||||
new, err := os.Create("new.txt")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer new.Close()
|
||||
|
||||
if _, err := io.Copy(new, original); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
42
main.go
Normal file
42
main.go
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright © 2021 NAME HERE <EMAIL ADDRESS>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/cupcakearmy/autorestic/cmd"
|
||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||
)
|
||||
|
||||
func handleCtrlC() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-c
|
||||
fmt.Println("Signal:", sig)
|
||||
lock.Unlock()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {
|
||||
handleCtrlC()
|
||||
cmd.Execute()
|
||||
}
|
30
package.json
30
package.json
@ -1,30 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc -w",
|
||||
"move": "mv bin/index-linux bin/autorestic_linux_x64 && mv bin/index-macos bin/autorestic_macos_x64",
|
||||
"bin": "yarn run build && pkg dist/index.js --targets latest-macos-x64,latest-linux-x64 --out-path bin && yarn run move",
|
||||
"docs:build": "codedoc install && codedoc build",
|
||||
"docs:dev": "codedoc serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codedoc/cli": "^0.2",
|
||||
"@types/js-yaml": "^3",
|
||||
"@types/node": "^14",
|
||||
"pkg": "^4.4",
|
||||
"ts-node-dev": "^1",
|
||||
"typescript": "^3.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/semver": "^7.3.4",
|
||||
"axios": "^0.19",
|
||||
"clitastic": "^0.1.2",
|
||||
"colors": "^1",
|
||||
"commander": "^6.2",
|
||||
"cron-parser": "2.x.x",
|
||||
"js-yaml": "3.x.x",
|
||||
"semver": "^7.3.5",
|
||||
"uhrwerk": "1.x.x"
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { config, VERBOSE } from './'
|
||||
import { Backend, Backends, Locations } from './types'
|
||||
import { exec, pathRelativeToConfigFile, filterObjectByKey } from './utils'
|
||||
|
||||
const ALREADY_EXISTS = /(?=.*already)(?=.*config).*/
|
||||
|
||||
export const getPathFromBackend = (backend: Backend): string => {
|
||||
switch (backend.type) {
|
||||
case 'local':
|
||||
return pathRelativeToConfigFile(backend.path)
|
||||
case 'b2':
|
||||
case 'azure':
|
||||
case 'gs':
|
||||
case 's3':
|
||||
case 'sftp':
|
||||
case 'rest':
|
||||
return `${backend.type}:${backend.path}`
|
||||
default:
|
||||
throw new Error(`Unknown backend type.`)
|
||||
}
|
||||
}
|
||||
|
||||
export const getEnvFromBackend = (backend: Backend) => {
|
||||
const { type, path, key, ...rest } = backend
|
||||
return {
|
||||
RESTIC_PASSWORD: key,
|
||||
RESTIC_REPOSITORY: getPathFromBackend(backend),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export const getBackendsFromLocations = (locations: Locations): string[] => {
|
||||
const backends = new Set<string>()
|
||||
for (const to of Object.values(locations).map((location) => location.to)) Array.isArray(to) ? to.forEach((t) => backends.add(t)) : backends.add(to)
|
||||
return Array.from(backends)
|
||||
}
|
||||
|
||||
export const checkAndConfigureBackend = (name: string, backend: Backend) => {
|
||||
const writer = new Writer(name.blue + ' : ' + 'Configuring... ⏳')
|
||||
try {
|
||||
const env = getEnvFromBackend(backend)
|
||||
|
||||
const { out, err } = exec('restic', ['init'], { env })
|
||||
|
||||
if (err.length > 0 && !ALREADY_EXISTS.test(err)) throw new Error(`Could not load the backend "${name}": ${err}`)
|
||||
|
||||
if (VERBOSE && out.length > 0) console.log(out)
|
||||
|
||||
writer.done(name.blue + ' : ' + 'Done ✓'.green)
|
||||
} catch (e) {
|
||||
writer.done(name.blue + ' : ' + 'Error ⚠️ ' + e.message.red)
|
||||
}
|
||||
}
|
||||
|
||||
export const checkAndConfigureBackends = (backends?: Backends) => {
|
||||
if (!backends) backends = config.backends
|
||||
|
||||
console.log('\nConfiguring Backends'.grey.underline)
|
||||
for (const [name, backend] of Object.entries(backends)) checkAndConfigureBackend(name, backend)
|
||||
}
|
||||
|
||||
export const checkAndConfigureBackendsForLocations = (locations: Locations) => {
|
||||
checkAndConfigureBackends(filterObjectByKey(config.backends, getBackendsFromLocations(locations)))
|
||||
}
|
104
src/backup.ts
104
src/backup.ts
@ -1,104 +0,0 @@
|
||||
import { Writer } from 'clitastic'
|
||||
import { mkdirSync } from 'fs'
|
||||
|
||||
import { config, hasError, VERBOSE } from './'
|
||||
import { getEnvFromBackend } from './backend'
|
||||
import { LocationFromPrefixes } from './config'
|
||||
import { Locations, Location, Backend } from './types'
|
||||
import {
|
||||
exec,
|
||||
pathRelativeToConfigFile,
|
||||
getFlagsFromLocation,
|
||||
makeArrayIfIsNot,
|
||||
execPlain,
|
||||
MeasureDuration,
|
||||
fill,
|
||||
decodeLocationFromPrefix,
|
||||
checkIfDockerVolumeExistsOrFail,
|
||||
getPathFromVolume,
|
||||
} from './utils'
|
||||
|
||||
export const backupFromFilesystem = (from: string, location: Location, backend: Backend, tags?: string[]) => {
|
||||
const path = pathRelativeToConfigFile(from)
|
||||
|
||||
const { out, err, status } = exec('restic', ['backup', '.', ...getFlagsFromLocation(location, 'backup')], {
|
||||
env: getEnvFromBackend(backend),
|
||||
cwd: path,
|
||||
})
|
||||
|
||||
if (VERBOSE) console.log(out, err)
|
||||
if (status != 0 || err.length > 0) throw new Error(err)
|
||||
}
|
||||
|
||||
export const backupFromVolume = (volume: string, location: Location, backend: Backend) => {
|
||||
const tmp = getPathFromVolume(volume)
|
||||
try {
|
||||
mkdirSync(tmp)
|
||||
checkIfDockerVolumeExistsOrFail(volume)
|
||||
|
||||
// For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost.
|
||||
// execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /data /backup`)
|
||||
execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar cf /backup/archive.tar -C /data .`)
|
||||
|
||||
backupFromFilesystem(tmp, location, backend)
|
||||
} catch (e) {
|
||||
throw e
|
||||
} finally {
|
||||
execPlain(`rm -rf ${tmp}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const backupSingle = (name: string, to: string, location: Location) => {
|
||||
const delta = new MeasureDuration()
|
||||
const writer = new Writer(name + to.blue + ' : ' + 'Backing up... ⏳')
|
||||
|
||||
try {
|
||||
const backend = config.backends[to]
|
||||
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||
|
||||
switch (type) {
|
||||
case LocationFromPrefixes.Filesystem:
|
||||
backupFromFilesystem(value, location, backend)
|
||||
break
|
||||
|
||||
case LocationFromPrefixes.DockerVolume:
|
||||
backupFromVolume(value, location, backend)
|
||||
break
|
||||
}
|
||||
|
||||
writer.done(`${name}${to.blue} : ${'Done ✓'.green} (${delta.finished(true)})`)
|
||||
} catch (e) {
|
||||
hasError()
|
||||
writer.done(`${name}${to.blue} : ${'Failed!'.red} (${delta.finished(true)}) ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const backupLocation = (name: string, location: Location) => {
|
||||
const display = name.yellow + ' ▶ '
|
||||
const filler = fill(name.length + 3)
|
||||
let first = true
|
||||
|
||||
if (location.hooks && location.hooks.before)
|
||||
for (const command of makeArrayIfIsNot(location.hooks.before)) {
|
||||
const cmd = execPlain(command, {})
|
||||
console.log(cmd.out, cmd.err)
|
||||
}
|
||||
|
||||
for (const t of makeArrayIfIsNot(location.to)) {
|
||||
backupSingle(first ? display : filler, t, location)
|
||||
if (first) first = false
|
||||
}
|
||||
|
||||
if (location.hooks && location.hooks.after)
|
||||
for (const command of makeArrayIfIsNot(location.hooks.after)) {
|
||||
const cmd = execPlain(command)
|
||||
console.log(cmd.out, cmd.err)
|
||||
}
|
||||
}
|
||||
|
||||
export const backupAll = (locations?: Locations) => {
|
||||
if (!locations) locations = config.locations
|
||||
|
||||
console.log('\nBacking Up'.underline.grey)
|
||||
for (const [name, location] of Object.entries(locations)) backupLocation(name, location)
|
||||
}
|
104
src/config.ts
104
src/config.ts
@ -1,104 +0,0 @@
|
||||
import { readFileSync, writeFileSync, statSync, copyFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
||||
import yaml from 'js-yaml'
|
||||
import CronParser from 'cron-parser'
|
||||
|
||||
import { Backend, Config } from './types'
|
||||
import { makeArrayIfIsNot, makeObjectKeysLowercase, rand } from './utils'
|
||||
|
||||
export enum LocationFromPrefixes {
|
||||
Filesystem,
|
||||
DockerVolume,
|
||||
}
|
||||
|
||||
export const normalizeAndCheckBackends = (config: Config) => {
|
||||
config.backends = makeObjectKeysLowercase(config.backends)
|
||||
|
||||
for (const [name, { type, path, key, ...rest }] of Object.entries(config.backends)) {
|
||||
if (!type || !path) throw new Error(`The backend "${name}" is missing some required attributes`)
|
||||
|
||||
const tmp: any = {
|
||||
type,
|
||||
path,
|
||||
key: key || rand(128),
|
||||
}
|
||||
for (const [key, value] of Object.entries(rest)) tmp[key.toUpperCase()] = value
|
||||
|
||||
config.backends[name] = tmp as Backend
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeAndCheckLocations = (config: Config) => {
|
||||
config.locations = makeObjectKeysLowercase(config.locations)
|
||||
const backends = Object.keys(config.backends)
|
||||
|
||||
const checkDestination = (backend: string, location: string) => {
|
||||
if (!backends.includes(backend)) throw new Error(`Cannot find the backend "${backend}" for "${location}"`)
|
||||
}
|
||||
|
||||
for (const [name, { from, to, cron, ...rest }] of Object.entries(config.locations)) {
|
||||
if (!from) throw new Error(`The location "${name.blue}" is missing the "${'from'.underline.red}" source folder. See https://git.io/Jf0xw`)
|
||||
if (!to || (Array.isArray(to) && !to.length))
|
||||
throw new Error(`The location "${name.blue}" has no backend "${'to'.underline.red}" to save the backups. See https://git.io/Jf0xw`)
|
||||
|
||||
for (const t of makeArrayIfIsNot(to)) checkDestination(t, name)
|
||||
|
||||
if (cron) {
|
||||
try {
|
||||
CronParser.parseExpression(cron)
|
||||
} catch {
|
||||
throw new Error(`The location "${name.blue}" has an invalid ${'cron'.underline.red} entry. See https://git.io/Jf0xP`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const findConfigFile = (custom: string): string => {
|
||||
const config = '.autorestic.yml'
|
||||
const paths = [resolve(custom || ''), resolve('./' + config), homedir() + '/' + config]
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const file = statSync(path)
|
||||
if (file.isFile()) return path
|
||||
} catch (e) {}
|
||||
}
|
||||
throw new Error('Config file not found')
|
||||
}
|
||||
|
||||
export let CONFIG_FILE: string = ''
|
||||
|
||||
export const init = (custom: string): Config => {
|
||||
const file = findConfigFile(custom)
|
||||
CONFIG_FILE = file
|
||||
|
||||
const parsed = yaml.safeLoad(readFileSync(CONFIG_FILE).toString())
|
||||
if (!parsed || typeof parsed === 'string') throw new Error('Could not parse the config file')
|
||||
const raw: Config = makeObjectKeysLowercase(parsed)
|
||||
|
||||
const current = JSON.stringify(raw)
|
||||
|
||||
normalizeAndCheckBackends(raw)
|
||||
normalizeAndCheckLocations(raw)
|
||||
|
||||
const changed = JSON.stringify(raw) !== current
|
||||
|
||||
if (changed) {
|
||||
const OLD_CONFIG_FILE = CONFIG_FILE + '.old'
|
||||
copyFileSync(CONFIG_FILE, OLD_CONFIG_FILE)
|
||||
writeFileSync(CONFIG_FILE, yaml.safeDump(raw))
|
||||
console.log(
|
||||
'\n' +
|
||||
'⚠️ MOVED OLD CONFIG FILE TO: ⚠️'.red.underline.bold +
|
||||
'\n' +
|
||||
OLD_CONFIG_FILE +
|
||||
'\n' +
|
||||
'What? Why? '.grey +
|
||||
'https://git.io/Jf0xK'.underline.grey +
|
||||
'\n'
|
||||
)
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
31
src/cron.ts
31
src/cron.ts
@ -1,31 +0,0 @@
|
||||
import CronParser from 'cron-parser'
|
||||
|
||||
import { config } from './'
|
||||
import { checkAndConfigureBackendsForLocations } from './backend'
|
||||
import { Location } from './types'
|
||||
import { backupLocation } from './backup'
|
||||
import { readLock, writeLock } from './lock'
|
||||
|
||||
const runCronForLocation = (name: string, location: Location) => {
|
||||
const lock = readLock()
|
||||
const parsed = CronParser.parseExpression(location.cron || '')
|
||||
const last = parsed.prev()
|
||||
|
||||
if (!lock.crons[name] || last.toDate().getTime() > lock.crons[name].lastRun) {
|
||||
backupLocation(name, location)
|
||||
lock.crons[name] = { lastRun: Date.now() }
|
||||
writeLock(lock)
|
||||
} else {
|
||||
console.log(`${name.yellow} ▶ Skipping. Scheduled for: ${parsed.next().toString().underline.blue}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const runCron = () => {
|
||||
const locationsWithCron = Object.entries(config.locations).filter(([name, { cron }]) => !!cron)
|
||||
checkAndConfigureBackendsForLocations(Object.fromEntries(locationsWithCron))
|
||||
|
||||
console.log('\nRunning cron jobs'.underline.gray)
|
||||
for (const [name, location] of locationsWithCron) runCronForLocation(name, location)
|
||||
|
||||
console.log('\nFinished!'.underline + ' 🎉')
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { config, VERBOSE } from './'
|
||||
import { getEnvFromBackend } from './backend'
|
||||
import { LocationFromPrefixes } from './config'
|
||||
import { Locations, Location, Flags } from './types'
|
||||
import { exec, pathRelativeToConfigFile, getFlagsFromLocation, makeArrayIfIsNot, fill, decodeLocationFromPrefix, getPathFromVolume } from './utils'
|
||||
|
||||
export const forgetSingle = (name: string, to: string, location: Location, dryRun: boolean) => {
|
||||
const base = name + to.blue + ' : '
|
||||
const writer = new Writer(base + 'Removing old snapshots… ⏳')
|
||||
|
||||
const backend = config.backends[to]
|
||||
const flags = getFlagsFromLocation(location, 'forget')
|
||||
|
||||
const [type, value] = decodeLocationFromPrefix(location.from)
|
||||
let path: string
|
||||
switch (type) {
|
||||
case LocationFromPrefixes.Filesystem:
|
||||
path = pathRelativeToConfigFile(value)
|
||||
break
|
||||
case LocationFromPrefixes.DockerVolume:
|
||||
path = getPathFromVolume(value)
|
||||
break
|
||||
}
|
||||
|
||||
if (flags.length == 0) {
|
||||
writer.done(base + 'Skipping, no policy declared')
|
||||
return
|
||||
}
|
||||
if (dryRun) flags.push('--dry-run')
|
||||
|
||||
writer.replaceLn(base + 'Forgetting old snapshots… ⏳')
|
||||
const cmd = exec('restic', ['forget', '--path', path, '--prune', ...flags], { env: getEnvFromBackend(backend) })
|
||||
|
||||
if (VERBOSE) console.log(cmd.out, cmd.err)
|
||||
writer.done(base + 'Done ✓'.green)
|
||||
}
|
||||
|
||||
export const forgetLocation = (name: string, backup: Location, dryRun: boolean) => {
|
||||
const display = name.yellow + ' ▶ '
|
||||
const filler = fill(name.length + 3)
|
||||
let first = true
|
||||
|
||||
for (const t of makeArrayIfIsNot(backup.to)) {
|
||||
const nameOrBlankSpaces: string = first ? display : filler
|
||||
forgetSingle(nameOrBlankSpaces, t, backup, dryRun)
|
||||
if (first) first = false
|
||||
}
|
||||
}
|
||||
|
||||
export const forgetAll = (backups?: Locations, dryRun = false) => {
|
||||
if (!backups) {
|
||||
backups = config.locations
|
||||
}
|
||||
|
||||
console.log('\nRemoving old snapshots according to policy'.underline.grey)
|
||||
if (dryRun) console.log('Running in dry-run mode, not touching data\n'.yellow)
|
||||
|
||||
for (const [name, backup] of Object.entries(backups)) forgetLocation(name, backup, dryRun)
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { checkAndConfigureBackendsForLocations } from '../backend'
|
||||
import { backupAll } from '../backup'
|
||||
import { Flags, Locations } from '../types'
|
||||
import { checkIfResticIsAvailable, parseLocations } from '../utils'
|
||||
|
||||
export default function backup({ location, all }: Flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const locations: Locations = parseLocations(location, all)
|
||||
checkAndConfigureBackendsForLocations(locations)
|
||||
backupAll(locations)
|
||||
|
||||
console.log('\nFinished!'.underline + ' 🎉')
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { checkAndConfigureBackends } from '../backend'
|
||||
import { Flags } from '../types'
|
||||
import { checkIfResticIsAvailable, parseBackend } from '../utils'
|
||||
|
||||
export default function check({ backend, all }: Flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const backends = parseBackend(backend, all)
|
||||
checkAndConfigureBackends(backends)
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { runCron } from '../cron'
|
||||
import { checkIfResticIsAvailable } from '../utils'
|
||||
|
||||
export function cron() {
|
||||
checkIfResticIsAvailable()
|
||||
runCron()
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { getEnvFromBackend } from '../backend'
|
||||
import { Flags } from '../types'
|
||||
import { checkIfResticIsAvailable, exec as execCLI, parseBackend } from '../utils'
|
||||
|
||||
export default function exec({ backend, all }: Flags, args: string[]) {
|
||||
checkIfResticIsAvailable()
|
||||
const backends = parseBackend(backend, all)
|
||||
for (const [name, backend] of Object.entries(backends)) {
|
||||
console.log(`\n${name}:\n`.grey.underline)
|
||||
const env = getEnvFromBackend(backend)
|
||||
const { out, err } = execCLI('restic', args, { env })
|
||||
console.log(out, err)
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { checkAndConfigureBackendsForLocations } from '../backend'
|
||||
import { forgetAll } from '../forget'
|
||||
import { Flags, Locations } from '../types'
|
||||
import { checkIfResticIsAvailable, parseLocations } from '../utils'
|
||||
|
||||
export default function forget({ location, all, dryRun }: Flags) {
|
||||
checkIfResticIsAvailable()
|
||||
const locations: Locations = parseLocations(location, all)
|
||||
checkAndConfigureBackendsForLocations(locations)
|
||||
forgetAll(locations, dryRun)
|
||||
|
||||
console.log('\nFinished!'.underline + ' 🎉')
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { config } from '../'
|
||||
import { fill, treeToString } from '../utils'
|
||||
|
||||
const showAll = () => {
|
||||
console.log('\n\n' + fill(32, '_') + 'LOCATIONS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.locations)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(data, ['to:', 'from:', 'hooks:', 'options:', 'cron:']))
|
||||
}
|
||||
|
||||
console.log('\n\n' + fill(32, '_') + 'BACKENDS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.backends)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(data, ['type:', 'path:', 'key:']))
|
||||
}
|
||||
}
|
||||
|
||||
export default showAll
|
@ -1,50 +0,0 @@
|
||||
import { join } from 'path'
|
||||
import { chmodSync, renameSync, unlinkSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
import axios from 'axios'
|
||||
import { Writer } from 'clitastic'
|
||||
|
||||
import { INSTALL_DIR } from '..'
|
||||
import { checkIfCommandIsAvailable, checkIfResticIsAvailable, downloadFile, exec } from '../utils'
|
||||
|
||||
export default async function install() {
|
||||
try {
|
||||
checkIfResticIsAvailable()
|
||||
console.log('Restic is already installed')
|
||||
return
|
||||
} catch {}
|
||||
|
||||
const w = new Writer('Checking latest version... ⏳')
|
||||
checkIfCommandIsAvailable('bzip2')
|
||||
const { data: json } = await axios({
|
||||
method: 'get',
|
||||
url: 'https://api.github.com/repos/restic/restic/releases/latest',
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
const archMap: { [a: string]: string } = {
|
||||
x32: '386',
|
||||
x64: 'amd64',
|
||||
}
|
||||
|
||||
w.replaceLn('Downloading binary... 🌎')
|
||||
const name = `${json.name.replace(' ', '_')}_${process.platform}_${archMap[process.arch]}.bz2`
|
||||
const dl = json.assets.find((asset: any) => asset.name === name)
|
||||
if (!dl) return console.log('Cannot get the right binary.'.red, 'Please see https://bit.ly/2Y1Rzai')
|
||||
|
||||
const tmp = join(tmpdir(), name)
|
||||
const extracted = tmp.slice(0, -4) //without the .bz2
|
||||
|
||||
await downloadFile(dl.browser_download_url, tmp)
|
||||
|
||||
w.replaceLn('Decompressing binary... 📦')
|
||||
exec('bzip2', ['-dk', tmp])
|
||||
unlinkSync(tmp)
|
||||
|
||||
w.replaceLn(`Moving to ${INSTALL_DIR} 🚙`)
|
||||
chmodSync(extracted, 0o755)
|
||||
renameSync(extracted, INSTALL_DIR + '/restic')
|
||||
|
||||
w.done(`\nFinished! restic is installed under: ${INSTALL_DIR}`.underline + ' 🎉')
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { restoreSingle } from '../restore'
|
||||
import { Flags } from '../types'
|
||||
import { checkIfResticIsAvailable, checkIfValidLocation } from '../utils'
|
||||
|
||||
export default function restore({ location, to, from }: Flags) {
|
||||
checkIfResticIsAvailable()
|
||||
checkIfValidLocation(location)
|
||||
restoreSingle(location, from, to)
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { unlinkSync } from 'fs'
|
||||
|
||||
import { INSTALL_DIR } from '..'
|
||||
|
||||
export function uninstall() {
|
||||
for (const bin of ['restic', 'autorestic'])
|
||||
try {
|
||||
unlinkSync(INSTALL_DIR + '/' + bin)
|
||||
console.log(`Finished! ${bin} was uninstalled`)
|
||||
} catch (e) {
|
||||
console.log(`${bin} is already uninstalled`.red)
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import { chmodSync } from 'fs'
|
||||
|
||||
import axios from 'axios'
|
||||
import { Writer } from 'clitastic'
|
||||
import semver from 'semver'
|
||||
|
||||
import { INSTALL_DIR, VERSION } from '..'
|
||||
import { checkIfResticIsAvailable, downloadFile, exec } from '../utils'
|
||||
|
||||
export async function upgrade() {
|
||||
checkIfResticIsAvailable()
|
||||
const w = new Writer('Checking for latest restic version... ⏳')
|
||||
exec('restic', ['self-update'])
|
||||
|
||||
w.replaceLn('Checking for latest autorestic version... ⏳')
|
||||
const { data: json } = await axios({
|
||||
method: 'get',
|
||||
url: 'https://api.github.com/repos/cupcakearmy/autorestic/releases/latest',
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
const latest = semver.coerce(json.tag_name)
|
||||
const current = semver.coerce(VERSION)
|
||||
if (!latest || !current) throw new Error('Could not parse versions numbers.')
|
||||
if (semver.gt(latest, current)) {
|
||||
if (semver.major(latest) === semver.major(current)) {
|
||||
// Update to compatible
|
||||
const platformMap: { [key: string]: string } = {
|
||||
darwin: 'macos',
|
||||
}
|
||||
|
||||
const name = `autorestic_${platformMap[process.platform] || process.platform}_${process.arch}`
|
||||
const dl = json.assets.find((asset: any) => asset.name === name)
|
||||
|
||||
const to = INSTALL_DIR + '/autorestic'
|
||||
w.replaceLn('Downloading binary... 🌎')
|
||||
await downloadFile(dl.browser_download_url, to)
|
||||
|
||||
chmodSync(to, 0o755)
|
||||
} else {
|
||||
w.done('Newer major version available, will not install automatically.\nDownload: https://github.com/cupcakearmy/autorestic')
|
||||
return
|
||||
}
|
||||
}
|
||||
w.done('All up to date! 🚀')
|
||||
}
|
135
src/index.ts
135
src/index.ts
@ -1,135 +0,0 @@
|
||||
import colors from 'colors'
|
||||
import { program } from 'commander'
|
||||
import { setCIMode } from 'clitastic'
|
||||
|
||||
import { unlock, readLock, writeLock, lock } from './lock'
|
||||
import { Config } from './types'
|
||||
import { init } from './config'
|
||||
|
||||
import info from './handlers/info'
|
||||
import check from './handlers/check'
|
||||
import backup from './handlers/backup'
|
||||
import restore from './handlers/restore'
|
||||
import forget from './handlers/forget'
|
||||
import { cron } from './handlers/cron'
|
||||
import exec from './handlers/exec'
|
||||
import install from './handlers/install'
|
||||
import { uninstall } from './handlers/uninstall'
|
||||
import { upgrade } from './handlers/upgrade'
|
||||
|
||||
export const VERSION = '0.30'
|
||||
export const INSTALL_DIR = '/usr/local/bin'
|
||||
|
||||
let requireConfig: boolean = true
|
||||
let error: boolean = false
|
||||
|
||||
export function hasError() {
|
||||
error = true
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.log(err.message)
|
||||
unlock()
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
let queue: () => Promise<void> = async () => {}
|
||||
const enqueue = (fn: Function) => (cmd: any) => {
|
||||
queue = async () => fn(cmd.opts())
|
||||
}
|
||||
|
||||
program.storeOptionsAsProperties()
|
||||
program.name('autorestic').description('Easy Restic CLI Utility').version(VERSION)
|
||||
|
||||
program.option('-c, --config <path>', 'Config file')
|
||||
program.option('-v, --verbose', 'Verbosity', false)
|
||||
program.option('--ci', 'CI Mode. Removes interactivity from the shell', false)
|
||||
|
||||
program.command('info').action(enqueue(info))
|
||||
|
||||
program.on('--help', () => {
|
||||
console.log('')
|
||||
console.log(`${'Docs:'.yellow}\t\thttps://autorestic.vercel.app`)
|
||||
console.log(`${'Examples:'.yellow}\thttps://autorestic.vercel.app/examples`)
|
||||
})
|
||||
|
||||
program
|
||||
.command('check')
|
||||
.description('Checks and initializes backend as needed')
|
||||
.option('-b, --backend <backends...>')
|
||||
.option('-a, --all')
|
||||
.action(enqueue(check))
|
||||
|
||||
program.command('backup').description('Performs a backup').option('-l, --location <locations...>').option('-a, --all').action(enqueue(backup))
|
||||
|
||||
program
|
||||
.command('restore')
|
||||
.description('Restores data to a specified folder from a location')
|
||||
.requiredOption('-l, --location <location>')
|
||||
.option('--from <backend>')
|
||||
.requiredOption('--to <path>', 'Path to save the restored data to')
|
||||
.action(enqueue(restore))
|
||||
|
||||
program
|
||||
.command('forget')
|
||||
.description('This will prune and remove data according to your policies')
|
||||
.option('-l, --location <locations...>')
|
||||
.option('-a, --all')
|
||||
.option('--dry-run')
|
||||
.action(enqueue(forget))
|
||||
|
||||
program
|
||||
.command('cron')
|
||||
.description('Intended to be triggered by an automated system like systemd or crontab.')
|
||||
.option('-a, --all')
|
||||
.action(enqueue(cron))
|
||||
|
||||
program
|
||||
.command('exec')
|
||||
.description('Run any native restic command on desired backends')
|
||||
.option('-b, --backend <backends...>')
|
||||
.option('-a, --all')
|
||||
.action(({ args, all, backend }) => {
|
||||
queue = async () => exec({ all, backend }, args)
|
||||
})
|
||||
|
||||
program.command('install').description('Installs both restic and autorestic to /usr/local/bin').action(enqueue(install))
|
||||
|
||||
program.command('uninstall').description('Uninstalls autorestic from the system').action(enqueue(uninstall))
|
||||
|
||||
program
|
||||
.command('upgrade')
|
||||
.alias('update')
|
||||
.description('Checks and installs new autorestic versions')
|
||||
.action(() => {
|
||||
requireConfig = false
|
||||
queue = upgrade
|
||||
})
|
||||
|
||||
const { verbose, config: configFile, ci } = program.parse(process.argv)
|
||||
|
||||
export const VERBOSE = verbose
|
||||
export let config: Config
|
||||
setCIMode(ci)
|
||||
if (ci) colors.disable()
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
if (requireConfig) {
|
||||
config = init(configFile)
|
||||
const { running } = readLock()
|
||||
if (running) {
|
||||
console.log('An instance of autorestic is already running for this config file'.red)
|
||||
process.exit(1)
|
||||
}
|
||||
lock()
|
||||
}
|
||||
await queue()
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
} finally {
|
||||
if (requireConfig) unlock()
|
||||
}
|
||||
if (error) process.exit(1)
|
||||
}
|
||||
main()
|
18
src/info.ts
18
src/info.ts
@ -1,18 +0,0 @@
|
||||
import { config } from './'
|
||||
import { fill, treeToString } from './utils'
|
||||
|
||||
const showAll = () => {
|
||||
console.log('\n\n' + fill(32, '_') + 'LOCATIONS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.locations)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(data, ['to:', 'from:', 'hooks:', 'options:', 'cron:']))
|
||||
}
|
||||
|
||||
console.log('\n\n' + fill(32, '_') + 'BACKENDS:'.underline)
|
||||
for (const [key, data] of Object.entries(config.backends)) {
|
||||
console.log(`\n${key.blue.underline}:`)
|
||||
console.log(treeToString(data, ['type:', 'path:', 'key:']))
|
||||
}
|
||||
}
|
||||
|
||||
export default showAll
|
39
src/lock.ts
39
src/lock.ts
@ -1,39 +0,0 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import { pathRelativeToConfigFile } from './utils'
|
||||
import { Lockfile } from './types'
|
||||
|
||||
export const getLockFileName = () => {
|
||||
const LOCK_FILE = '.autorestic.lock'
|
||||
return pathRelativeToConfigFile(LOCK_FILE)
|
||||
}
|
||||
|
||||
export const readLock = (): Lockfile => {
|
||||
const name = getLockFileName()
|
||||
let lock = {
|
||||
running: false,
|
||||
crons: {},
|
||||
}
|
||||
try {
|
||||
lock = JSON.parse(fs.readFileSync(name, { encoding: 'utf-8' }))
|
||||
} catch {}
|
||||
return lock
|
||||
}
|
||||
export const writeLock = (lock: Lockfile) => {
|
||||
const name = getLockFileName()
|
||||
fs.writeFileSync(name, JSON.stringify(lock, null, 2), { encoding: 'utf-8' })
|
||||
}
|
||||
|
||||
export const unlock = () => {
|
||||
writeLock({
|
||||
...readLock(),
|
||||
running: false,
|
||||
})
|
||||
}
|
||||
|
||||
export const lock = () => {
|
||||
writeLock({
|
||||
...readLock(),
|
||||
running: true,
|
||||
})
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user