parent
805bed7db1
commit
03cbbfd91c
@ -1,4 +0,0 @@
|
|||||||
semi: false
|
|
||||||
singleQuote: true
|
|
||||||
trailingComma: 'es5'
|
|
||||||
printWidth: 150
|
|
@ -1,21 +1,202 @@
|
|||||||
MIT License
|
|
||||||
|
Apache License
|
||||||
Copyright (c) 2019 Nicco
|
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
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
1. Definitions.
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
other entities that control, are controlled by, or are under common
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
control with that entity. For the purposes of this definition,
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
direction or management of such entity, whether by contract or
|
||||||
SOFTWARE.
|
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.
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cupcakearmy/autorestic/internal"
|
||||||
|
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// backupCmd represents the backup command
|
||||||
|
var backupCmd = &cobra.Command{
|
||||||
|
Use: "backup",
|
||||||
|
Short: "A brief description of your command",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
config := internal.GetConfig()
|
||||||
|
{
|
||||||
|
err := config.CheckConfig()
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
err := lock.Lock()
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
}
|
||||||
|
defer lock.Unlock()
|
||||||
|
{
|
||||||
|
backup(internal.GetAllOrLocation(cmd, false), config)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(backupCmd)
|
||||||
|
backupCmd.PersistentFlags().StringSliceP("location", "l", []string{}, "Locations")
|
||||||
|
backupCmd.PersistentFlags().BoolP("all", "a", false, "Backup all locations")
|
||||||
|
}
|
||||||
|
|
||||||
|
func backup(locations []string, config *internal.Config) {
|
||||||
|
for _, name := range locations {
|
||||||
|
location, ok := config.Locations[name]
|
||||||
|
if !ok {
|
||||||
|
fmt.Println(fmt.Errorf("location `%s` does not exist", name))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Backing up: `%s`", name)
|
||||||
|
location.Backup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/cupcakearmy/autorestic/internal"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkCmd represents the check command
|
||||||
|
var checkCmd = &cobra.Command{
|
||||||
|
Use: "check",
|
||||||
|
Short: "Check if everything is setup",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if !internal.CheckIfResticIsCallable() {
|
||||||
|
cobra.CheckErr(errors.New("restic is not callable. Install: https://restic.readthedocs.io/en/stable/020_installation.html"))
|
||||||
|
}
|
||||||
|
config := internal.GetConfig()
|
||||||
|
err := config.CheckConfig()
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(checkCmd)
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cupcakearmy/autorestic/internal"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// execCmd represents the exec command
|
||||||
|
var execCmd = &cobra.Command{
|
||||||
|
Use: "exec",
|
||||||
|
Short: "A brief description of your command",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
config := internal.GetConfig()
|
||||||
|
if err := config.CheckConfig(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
exec(internal.GetAllOrLocation(cmd, true), config, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(execCmd)
|
||||||
|
execCmd.PersistentFlags().StringSliceP("backend", "b", []string{}, "backends")
|
||||||
|
execCmd.PersistentFlags().BoolP("all", "a", false, "Exec in all backends")
|
||||||
|
}
|
||||||
|
|
||||||
|
func exec(backends []string, config *internal.Config, args []string) {
|
||||||
|
for _, name := range backends {
|
||||||
|
fmt.Println(name)
|
||||||
|
backend := config.Backends[name]
|
||||||
|
backend.Exec(args)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/cupcakearmy/autorestic/internal"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cfgFile string
|
||||||
|
|
||||||
|
// rootCmd represents the base command when called without any subcommands
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "autorestic",
|
||||||
|
Short: "CLI Wrapper for restic",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
cobra.CheckErr(rootCmd.Execute())
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cobra.OnInitialize(initConfig)
|
||||||
|
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
// Cobra supports persistent flags, which, if defined here,
|
||||||
|
// will be global for your application.
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.autorestic.yml or ./.autorestic.yml)")
|
||||||
|
|
||||||
|
// Cobra also supports local flags, which will only run
|
||||||
|
// when this action is called directly.
|
||||||
|
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConfig reads in config file and ENV variables if set.
|
||||||
|
func initConfig() {
|
||||||
|
if cfgFile != "" {
|
||||||
|
// Use config file from the flag.
|
||||||
|
viper.SetConfigFile(cfgFile)
|
||||||
|
} else {
|
||||||
|
// Find home directory.
|
||||||
|
home, err := homedir.Dir()
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
viper.AddConfigPath(home)
|
||||||
|
viper.SetConfigName(".autorestic")
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.AutomaticEnv() // read in environment variables that match
|
||||||
|
|
||||||
|
// If a config file is found, read it in.
|
||||||
|
if err := viper.ReadInConfig(); err == nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal.GetConfig()
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
module github.com/cupcakearmy/autorestic
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/buger/goterm v1.0.0
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
|
github.com/spf13/cobra v1.1.3
|
||||||
|
github.com/spf13/viper v1.7.1
|
||||||
|
)
|
@ -0,0 +1,317 @@
|
|||||||
|
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/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/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-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
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/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 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
|
||||||
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/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=
|
@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
OUT_FILE=/usr/local/bin/autorestic
|
|
||||||
|
|
||||||
if [[ "$OSTYPE" == "linux-gnu" ]]; then
|
|
||||||
TYPE=linux
|
|
||||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
|
||||||
TYPE=macos
|
|
||||||
else
|
|
||||||
echo "Unsupported OS"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl -s https://api.github.com/repos/cupcakearmy/autorestic/releases/latest \
|
|
||||||
| grep "browser_download_url.*_${TYPE}" \
|
|
||||||
| cut -d : -f 2,3 \
|
|
||||||
| tr -d \" \
|
|
||||||
| wget -O ${OUT_FILE} -i -
|
|
||||||
chmod +x ${OUT_FILE}
|
|
||||||
|
|
||||||
autorestic install
|
|
||||||
autorestic --help
|
|
@ -0,0 +1,55 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Backend struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
Path string `mapstructure:"path"`
|
||||||
|
Key string `mapstructure:"key"`
|
||||||
|
Env map[string]string `mapstructure:"env"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Backend) generateRepo() (string, error) {
|
||||||
|
switch b.Type {
|
||||||
|
case "local":
|
||||||
|
return GetPathRelativeToConfig(b.Path), nil
|
||||||
|
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 {
|
||||||
|
env := make(map[string]string)
|
||||||
|
env["RESTIC_PASSWORD"] = b.Key
|
||||||
|
repo, err := b.generateRepo()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
env["RESTIC_REPOSITORY"] = repo
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Backend) validate() error {
|
||||||
|
options := ExecuteOptions{Envs: b.getEnv()}
|
||||||
|
// Check if already initialized
|
||||||
|
_, err := ExecuteResticCommand(options, "snapshots")
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
// If not initialize
|
||||||
|
out, err := ExecuteResticCommand(options, "init")
|
||||||
|
fmt.Println(out)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Backend) Exec(args []string) error {
|
||||||
|
options := ExecuteOptions{Envs: b.getEnv()}
|
||||||
|
out, err := ExecuteResticCommand(options, args...)
|
||||||
|
fmt.Println(out)
|
||||||
|
return err
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
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() {
|
||||||
|
config = &Config{}
|
||||||
|
if err := viper.UnmarshalExact(config); err != nil {
|
||||||
|
log.Fatal("Nope ", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPathRelativeToConfig(p string) string {
|
||||||
|
if path.IsAbs(p) {
|
||||||
|
return p
|
||||||
|
} else if strings.HasPrefix(p, "~") {
|
||||||
|
home, err := homedir.Dir()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return path.Join(home, strings.TrimPrefix(p, "~"))
|
||||||
|
} else {
|
||||||
|
return path.Join(path.Dir(viper.ConfigFileUsed()), p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) CheckConfig() error {
|
||||||
|
for name, backend := range c.Backends {
|
||||||
|
if err := backend.validate(); err != nil {
|
||||||
|
return fmt.Errorf("backend \"%s\": %s", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, location := range c.Locations {
|
||||||
|
if err := location.validate(c); err != nil {
|
||||||
|
return fmt.Errorf("location \"%s\": %s", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllOrLocation(cmd *cobra.Command, backends bool) []string {
|
||||||
|
var list []string
|
||||||
|
if backends {
|
||||||
|
for key := range config.Backends {
|
||||||
|
list = append(list, key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for key := range config.Locations {
|
||||||
|
list = append(list, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all, _ := cmd.Flags().GetBool("all")
|
||||||
|
if all {
|
||||||
|
return list
|
||||||
|
} else {
|
||||||
|
var selected []string
|
||||||
|
if backends {
|
||||||
|
tmp, _ := cmd.Flags().GetStringSlice("backend")
|
||||||
|
selected = tmp
|
||||||
|
} else {
|
||||||
|
tmp, _ := cmd.Flags().GetStringSlice("location")
|
||||||
|
selected = tmp
|
||||||
|
}
|
||||||
|
for _, s := range selected {
|
||||||
|
found := false
|
||||||
|
for _, l := range list {
|
||||||
|
if l == s {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
panic("invalid key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HookArray = []string
|
||||||
|
|
||||||
|
type Hooks struct {
|
||||||
|
Before HookArray `mapstructure:"before"`
|
||||||
|
After HookArray `mapstructure:"after"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options map[string]map[string][]string
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
From string `mapstructure:"from"`
|
||||||
|
To []string `mapstructure:"to"`
|
||||||
|
Hooks Hooks `mapstructure:"hooks"`
|
||||||
|
Cron string `mapstructure:"cron"`
|
||||||
|
Options Options `mapstructure:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Location) validate(c Config) error {
|
||||||
|
// Check if backends are all valid
|
||||||
|
for _, to := range l.To {
|
||||||
|
_, ok := c.Backends[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 {
|
||||||
|
for _, command := range commands {
|
||||||
|
out, err := ExecuteCommand(options, "-c", command)
|
||||||
|
fmt.Println(out)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Location) Backup() error {
|
||||||
|
c := GetConfig()
|
||||||
|
from := GetPathRelativeToConfig(l.From)
|
||||||
|
for _, to := range l.To {
|
||||||
|
backend := c.Backends[to]
|
||||||
|
options := ExecuteOptions{
|
||||||
|
Command: "bash",
|
||||||
|
Envs: backend.getEnv(),
|
||||||
|
Dir: from,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ExecuteHooks(l.Hooks.Before, options); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{
|
||||||
|
flags := l.getOptions("backup")
|
||||||
|
cmd := []string{"backup"}
|
||||||
|
cmd = append(cmd, flags...)
|
||||||
|
cmd = append(cmd, ".")
|
||||||
|
out, err := ExecuteResticCommand(options, cmd...)
|
||||||
|
fmt.Println(out)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ExecuteHooks(l.Hooks.After, options); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
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 set(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 Lock() error {
|
||||||
|
return set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unlock() error {
|
||||||
|
return set(false)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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...)
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
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 "github.com/cupcakearmy/autorestic/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
@ -1,28 +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": {
|
|
||||||
"axios": "^0.19",
|
|
||||||
"clitastic": "^0.1.2",
|
|
||||||
"colors": "^1",
|
|
||||||
"commander": "^6.2",
|
|
||||||
"cron-parser": "2.x.x",
|
|
||||||
"js-yaml": "3.x.x",
|
|
||||||
"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)))
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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,37 +0,0 @@
|
|||||||
import { chmodSync } from 'fs'
|
|
||||||
|
|
||||||
import axios from 'axios'
|
|
||||||
import { Writer } from 'clitastic'
|
|
||||||
|
|
||||||
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',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (json.tag_name != VERSION) {
|
|
||||||
const platformMap: { [key: string]: string } = {
|
|
||||||
darwin: 'macos',
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = `autorestic_${platformMap[process.platform] || process.platform}_${process.arch}`
|
|
||||||
const dl = json.assets.find((asset: any) => asset.name === name)
|
|
||||||
|
|
||||||
const to = INSTALL_DIR + '/autorestic'
|
|
||||||
w.replaceLn('Downloading binary... 🌎')
|
|
||||||
await downloadFile(dl.browser_download_url, to)
|
|
||||||
|
|
||||||
chmodSync(to, 0o755)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.done('All up to date! 🚀')
|
|
||||||
}
|
|
@ -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.27'
|
|
||||||
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()
|
|
@ -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,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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
import { Writer } from 'clitastic'
|
|
||||||
import { resolve } from 'path'
|
|
||||||
|
|
||||||
import { config } from './'
|
|
||||||
import { getEnvFromBackend } from './backend'
|
|
||||||
import { LocationFromPrefixes } from './config'
|
|
||||||
import { Backend } from './types'
|
|
||||||
import { checkIfDockerVolumeExistsOrFail, decodeLocationFromPrefix, exec, execPlain, getPathFromVolume } from './utils'
|
|
||||||
|
|
||||||
export const restoreToFilesystem = (from: string, to: string, backend: Backend) => {
|
|
||||||
exec('restic', ['restore', 'latest', '--path', resolve(from), '--target', to], { env: getEnvFromBackend(backend) })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const restoreToVolume = (volume: string, backend: Backend) => {
|
|
||||||
const tmp = getPathFromVolume(volume)
|
|
||||||
try {
|
|
||||||
restoreToFilesystem(tmp, tmp, backend)
|
|
||||||
try {
|
|
||||||
checkIfDockerVolumeExistsOrFail(volume)
|
|
||||||
} catch {
|
|
||||||
execPlain(`docker volume create ${volume}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For incremental backups. Unfortunately due to how the docker mounts work the permissions get lost.
|
|
||||||
// execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine cp -aT /backup /data`)
|
|
||||||
execPlain(`docker run --rm -v ${volume}:/data -v ${tmp}:/backup alpine tar xf /backup/archive.tar -C /data`)
|
|
||||||
} finally {
|
|
||||||
execPlain(`rm -rf ${tmp}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const restoreSingle = (locationName: string, from: string, to?: string) => {
|
|
||||||
const location = config.locations[locationName]
|
|
||||||
|
|
||||||
const baseText = locationName.green + '\t\t'
|
|
||||||
const w = new Writer(baseText + `Restoring...`)
|
|
||||||
|
|
||||||
let backendName: string = Array.isArray(location.to) ? location.to[0] : location.to
|
|
||||||
if (from) {
|
|
||||||
if (!location.to.includes(from)) {
|
|
||||||
w.done(baseText + `Backend ${from} is not a valid location for ${locationName}`.red)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
backendName = from
|
|
||||||
w.replaceLn(baseText + `Restoring from ${backendName.blue}...`)
|
|
||||||
} else if (Array.isArray(location.to) && location.to.length > 1) {
|
|
||||||
w.replaceLn(baseText + `Restoring from ${backendName.blue}...\tTo select a specific backend pass the ${'--from'.blue} flag`)
|
|
||||||
}
|
|
||||||
const backend = config.backends[backendName]
|
|
||||||
|
|
||||||
const [type, value] = decodeLocationFromPrefix(location.from)
|
|
||||||
switch (type) {
|
|
||||||
case LocationFromPrefixes.Filesystem:
|
|
||||||
if (!to) throw new Error(`You need to specify the restore path with --to`.red)
|
|
||||||
restoreToFilesystem(value, to, backend)
|
|
||||||
break
|
|
||||||
|
|
||||||
case LocationFromPrefixes.DockerVolume:
|
|
||||||
restoreToVolume(value, backend)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
w.done(locationName.green + '\t\tDone 🎉')
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
export type StringOrArray = string | string[]
|
|
||||||
|
|
||||||
// BACKENDS
|
|
||||||
|
|
||||||
type BackendLocal = {
|
|
||||||
type: 'local'
|
|
||||||
key: string
|
|
||||||
path: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackendSFTP = {
|
|
||||||
type: 'sftp'
|
|
||||||
key: string
|
|
||||||
path: string
|
|
||||||
password?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackendREST = {
|
|
||||||
type: 'rest'
|
|
||||||
key: string
|
|
||||||
path: string
|
|
||||||
user?: string
|
|
||||||
password?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackendS3 = {
|
|
||||||
type: 's3'
|
|
||||||
key: string
|
|
||||||
path: string
|
|
||||||
aws_access_key_id: string
|
|
||||||
aws_secret_access_key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackendB2 = {
|
|
||||||
type: 'b2'
|
|
||||||
key: string
|
|
||||||
path: string
|
|
||||||
b2_account_id: string
|
|
||||||
b2_account_key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackendAzure = {
|
|
||||||
type: 'azure'
|
|
||||||
key: string
|
|
||||||
path: string
|
|
||||||
azure_account_name: string
|
|
||||||
azure_account_key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackendGS = {
|
|
||||||
type: 'gs'
|
|
||||||
key: string
|
|
||||||
path: string
|
|
||||||
google_project_id: string
|
|
||||||
google_application_credentials: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Backend = BackendAzure | BackendB2 | BackendGS | BackendLocal | BackendREST | BackendS3 | BackendSFTP
|
|
||||||
|
|
||||||
export type Backends = { [name: string]: Backend }
|
|
||||||
|
|
||||||
// LOCATIONS
|
|
||||||
|
|
||||||
export type Location = {
|
|
||||||
from: string
|
|
||||||
to: StringOrArray
|
|
||||||
cron?: string
|
|
||||||
hooks?: {
|
|
||||||
before?: StringOrArray
|
|
||||||
after?: StringOrArray
|
|
||||||
}
|
|
||||||
options?: {
|
|
||||||
[key: string]: {
|
|
||||||
[key: string]: StringOrArray
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Locations = { [name: string]: Location }
|
|
||||||
|
|
||||||
// OTHER
|
|
||||||
|
|
||||||
export type Config = {
|
|
||||||
locations: Locations
|
|
||||||
backends: Backends
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Lockfile = {
|
|
||||||
running: boolean
|
|
||||||
crons: {
|
|
||||||
[name: string]: {
|
|
||||||
lastRun: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Flags = { [arg: string]: any }
|
|
@ -1,201 +0,0 @@
|
|||||||
import { spawnSync, SpawnSyncOptions } from 'child_process'
|
|
||||||
import { createHash, randomBytes } from 'crypto'
|
|
||||||
import { createWriteStream, renameSync, unlinkSync } from 'fs'
|
|
||||||
import { homedir, tmpdir } from 'os'
|
|
||||||
import { dirname, isAbsolute, join, resolve } from 'path'
|
|
||||||
|
|
||||||
import axios from 'axios'
|
|
||||||
import { Duration, Humanizer } from 'uhrwerk'
|
|
||||||
|
|
||||||
import { CONFIG_FILE, LocationFromPrefixes } from './config'
|
|
||||||
import { Backends, Location, Locations } from './types'
|
|
||||||
import { config } from '.'
|
|
||||||
|
|
||||||
export const exec = (command: string, args: string[], { env, ...rest }: SpawnSyncOptions = {}) => {
|
|
||||||
const { stdout, stderr, status } = spawnSync(command, args, {
|
|
||||||
...rest,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
...env,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const out = stdout && stdout.toString().trim()
|
|
||||||
const err = stderr && stderr.toString().trim()
|
|
||||||
|
|
||||||
return { out, err, status }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const execPlain = (command: string, opt: SpawnSyncOptions = {}) => {
|
|
||||||
const split = command.split(' ')
|
|
||||||
if (split.length < 1) throw new Error(`The command ${command} is not valid`.red)
|
|
||||||
|
|
||||||
return exec(split[0], split.slice(1), { shell: true, ...opt })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const checkIfResticIsAvailable = () =>
|
|
||||||
checkIfCommandIsAvailable(
|
|
||||||
'restic',
|
|
||||||
'restic is not installed'.red +
|
|
||||||
'\nEither run ' +
|
|
||||||
'autorestic install'.green +
|
|
||||||
'\nOr go to https://restic.readthedocs.io/en/latest/020_installation.html#stable-releases'
|
|
||||||
)
|
|
||||||
|
|
||||||
export const checkIfCommandIsAvailable = (cmd: string, errorMsg?: string) => {
|
|
||||||
const error = spawnSync(cmd, { shell: true }).stderr
|
|
||||||
if (error.length) throw new Error(errorMsg ? errorMsg : `"${cmd}" is not installed`.red)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeObjectKeysLowercase = (object: Object): any =>
|
|
||||||
Object.fromEntries(Object.entries(object).map(([key, value]) => [key.toLowerCase(), value]))
|
|
||||||
|
|
||||||
export function rand(length = 32): string {
|
|
||||||
return randomBytes(length / 2).toString('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const filterObject = <T>(obj: { [key: string]: T }, filter: (item: [string, T]) => boolean): { [key: string]: T } =>
|
|
||||||
Object.fromEntries(Object.entries(obj).filter(filter))
|
|
||||||
|
|
||||||
export const filterObjectByKey = <T>(obj: { [key: string]: T }, keys: string[]) => filterObject(obj, ([key]) => keys.includes(key))
|
|
||||||
|
|
||||||
export const downloadFile = async (url: string, to: string) =>
|
|
||||||
new Promise<void>(async (res) => {
|
|
||||||
const { data: file } = await axios({
|
|
||||||
method: 'get',
|
|
||||||
url: url,
|
|
||||||
responseType: 'stream',
|
|
||||||
})
|
|
||||||
|
|
||||||
const tmp = join(tmpdir(), rand(64))
|
|
||||||
const stream = createWriteStream(tmp)
|
|
||||||
|
|
||||||
const writer = file.pipe(stream)
|
|
||||||
writer.on('close', () => {
|
|
||||||
stream.close()
|
|
||||||
try {
|
|
||||||
// Delete file if already exists. Needed if the binary wants to replace itself.
|
|
||||||
// Unix does not allow to overwrite a file that is being executed, but you can remove it and save other one at its place
|
|
||||||
unlinkSync(to)
|
|
||||||
} catch {}
|
|
||||||
renameSync(tmp, to)
|
|
||||||
res()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if is an absolute path, otherwise get the path relative to the config file
|
|
||||||
export const pathRelativeToConfigFile = (path: string): string => (isAbsolute(path) ? path : resolve(dirname(CONFIG_FILE), path))
|
|
||||||
|
|
||||||
export const resolveTildePath = (path: string): string | null => (path.length === 0 || path[0] !== '~' ? null : join(homedir(), path.slice(1)))
|
|
||||||
|
|
||||||
export const getFlagsFromLocation = (location: Location, command?: string): string[] => {
|
|
||||||
if (!location.options) return []
|
|
||||||
|
|
||||||
const all = {
|
|
||||||
...location.options.global,
|
|
||||||
...(location.options[command || ''] || {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags: string[] = []
|
|
||||||
// Map the flags to an array for the exec function.
|
|
||||||
for (let [flag, values] of Object.entries(all))
|
|
||||||
for (const value of makeArrayIfIsNot(values)) {
|
|
||||||
const stringValue = String(value)
|
|
||||||
const resolvedTilde = resolveTildePath(stringValue)
|
|
||||||
flags = [...flags, `--${String(flag)}`, resolvedTilde === null ? stringValue : resolvedTilde]
|
|
||||||
}
|
|
||||||
|
|
||||||
return flags
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseBackend(backends: string[] = [], all: boolean = false): Backends {
|
|
||||||
if (all) return config.backends
|
|
||||||
if (backends.length) {
|
|
||||||
for (const backend of backends) if (!config.backends[backend]) throw new Error('Invalid backend: '.red + backend)
|
|
||||||
return filterObjectByKey(config.backends, backends)
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'No backends specified.'.red + '\n-a, --all, -a\t\t\tSelect all.' + '\n-b, --backend <backends...>\t\tSpecify one or more backend'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkIfValidLocation(location: string) {
|
|
||||||
if (!config.locations[location]) throw new Error('Invalid location: '.red + location)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseLocations(locations: string[] = [], all: boolean = false): Locations {
|
|
||||||
if (all) {
|
|
||||||
return config.locations
|
|
||||||
}
|
|
||||||
if (locations.length) {
|
|
||||||
for (const location of locations) checkIfValidLocation(location)
|
|
||||||
return filterObjectByKey(config.locations, locations)
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
'No locations specified.'.red + '\n-a, --all\t\t\tSelect all.' + '\n-l, --location <locations...>\t\t\tSpecify one or more location'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeArrayIfIsNot = <T>(maybeArray: T | T[]): T[] => (Array.isArray(maybeArray) ? maybeArray : [maybeArray])
|
|
||||||
|
|
||||||
export const fill = (length: number, filler = ' '): string => new Array(length).fill(filler).join('')
|
|
||||||
|
|
||||||
export const capitalize = (string: string): string => string.charAt(0).toUpperCase() + string.slice(1)
|
|
||||||
|
|
||||||
export const treeToString = (obj: Object, highlight = [] as string[]): string => {
|
|
||||||
let cleaned = JSON.stringify(obj, null, 2)
|
|
||||||
.replace(/[{}"\[\],]/g, '')
|
|
||||||
.replace(/^ {2}/gm, '')
|
|
||||||
.replace(/\n\s*\n/g, '\n')
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
for (const word of highlight) cleaned = cleaned.replace(word, capitalize(word).green)
|
|
||||||
|
|
||||||
return cleaned
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MeasureDuration {
|
|
||||||
private static Humanizer: Humanizer = [
|
|
||||||
[(d) => d.hours() > 0, (d) => `${d.hours()}h ${d.minutes()}min`],
|
|
||||||
[(d) => d.minutes() > 0, (d) => `${d.minutes()}min ${d.seconds()}s`],
|
|
||||||
[(d) => d.seconds() > 0, (d) => `${d.seconds()}s`],
|
|
||||||
[() => true, (d) => `${d.milliseconds()}ms`],
|
|
||||||
]
|
|
||||||
|
|
||||||
private start = Date.now()
|
|
||||||
|
|
||||||
finished(human?: false): number
|
|
||||||
finished(human?: true): string
|
|
||||||
finished(human?: boolean): number | string {
|
|
||||||
const delta = Date.now() - this.start
|
|
||||||
|
|
||||||
return human ? new Duration(delta, 'ms').humanize(MeasureDuration.Humanizer) : delta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const decodeLocationFromPrefix = (from: string): [LocationFromPrefixes, string] => {
|
|
||||||
const firstDelimiter = from.indexOf(':')
|
|
||||||
if (firstDelimiter === -1) return [LocationFromPrefixes.Filesystem, from]
|
|
||||||
|
|
||||||
const type = from.substr(0, firstDelimiter)
|
|
||||||
const value = from.substr(firstDelimiter + 1)
|
|
||||||
|
|
||||||
switch (type.toLowerCase()) {
|
|
||||||
case 'volume':
|
|
||||||
return [LocationFromPrefixes.DockerVolume, value]
|
|
||||||
case 'path':
|
|
||||||
return [LocationFromPrefixes.Filesystem, value]
|
|
||||||
default:
|
|
||||||
throw new Error(`Could not decode the location from: ${from}`.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const hash = (plain: string): string => createHash('sha1').update(plain).digest().toString('hex')
|
|
||||||
|
|
||||||
export const getPathFromVolume = (volume: string) => pathRelativeToConfigFile(hash(volume))
|
|
||||||
|
|
||||||
export const checkIfDockerVolumeExistsOrFail = (volume: string) => {
|
|
||||||
const cmd = exec('docker', ['volume', 'inspect', volume])
|
|
||||||
if (cmd.err.length > 0) throw new Error('Volume not found')
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2019",
|
|
||||||
"module": "commonjs",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"strictNullChecks": true
|
|
||||||
},
|
|
||||||
"include": ["./src"]
|
|
||||||
}
|
|
Loading…
Reference in new issue