.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | # Gasper
7 |
8 | 
9 | 
10 | 
11 | 
12 |
13 | Back-up & encrypt your files in a distributed manner, across multiple stores of your choice, by splitting them to shares.
14 | Retrieve them at any point, with only a minimum number of shares required for retrieval.
15 |
16 | Each file is being split to multiple shares, all of which are distributed to different destinations defined by you (be it AWS S3, Dropbox, Google Drive, your local filesystem, FTP Server, etc...). You can retrieve your file at any given moment, even if you only have access to a part of the stores you originally used (down to a minimium threshold of your choice).
17 |
18 | Gasper is based on the awesome [Shamir's Secret Sharing algorithm](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing).
19 |
20 |
21 | 
22 | Source: Robin's Snippet blog
23 |
24 |
25 | # Demo
26 | #### Using local store
27 | 
28 |
29 | ## Supported stores
30 |
31 | | Type | Description | Attributes |
32 | | ----------------- |-----------------------| --------------------------|
33 | | `local` | Store share in a local directory | `directory-path` (string) |
34 |
35 | Feel free to contribute your own stores - S3, Google Drive, Twitter, FTP, or anything else you'd like :)
36 |
37 | ### Adding a new store
38 | 1. Implement the `Store` interface (`pkg/storage/stores/store.go`):
39 |
40 | ```
41 | // Store lets you store shares.
42 | type Store interface {
43 | // Store type.
44 | Type() string
45 |
46 | // Is store available?
47 | // Useful especially for remote stores, such as ftp servers or s3 buckets.
48 | Available() (bool, error)
49 |
50 | // Puts a share in store.
51 | Put(share *shares.Share) error
52 |
53 | // Retrieves a share from store.
54 | // If no share with the given File ID exists, returns ErrShareNotExists.
55 | Get(fileID string) (*shares.Share, error)
56 |
57 | // Deletes a share from store.
58 | // If no share with the given File ID exists, returns ErrShareNotExists.
59 | Delete(fileID string) error
60 | }
61 | ```
62 | 2. Add it to the stores factory function `FromConfig()` (`pkg/storage/stores/factory.go`), so it can be used out-of-the-box in the CLI.
63 | 3. Enjoy!
64 |
65 | For an example, see `pkg/storage/stores/local.go`.
66 |
67 | ## Installation
68 | ```
69 | go get -u github.com/talhof8/gasper
70 | ```
71 |
72 | ## Usage
73 | #### Store
74 | ```
75 | gasper store --stores-config --file [--encrypt --salt --share-count --shares-threshold --verbose]
76 | ```
77 | Outputs file ID and checksum on success which should be used for retrieval.
78 |
79 | #### Retrieve
80 | ```
81 | gasper retrieve --stores-config --file-id --destination [--checksum --encrypt --salt --verbose]
82 | ```
83 |
84 | #### Delete
85 | Best effort deletion.
86 | ```
87 | gasper delete --stores-config --file-id [--verbose]
88 | ```
89 |
90 | Stores configuration file:
91 | ```
92 | {
93 | "stores": [
94 | {
95 | "type": "",
96 | "": "",
97 | "": "",
98 | "": ""
99 | },
100 | {
101 | "type": "",
102 | "": "",
103 | ...
104 | }
105 | ]
106 | }
107 | ```
108 |
109 | ## License
110 | Gasper is released under GPL. See LICENSE.txt.
111 |
--------------------------------------------------------------------------------
/assets/demo-local.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhof8/gasper/b1b55c473c0180f47235d3e7c60af561de84d4f1/assets/demo-local.gif
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talhof8/gasper/b1b55c473c0180f47235d3e7c60af561de84d4f1/assets/logo.png
--------------------------------------------------------------------------------
/cmd/delete.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/gasper/internal/encryption"
5 | "github.com/gasper/pkg"
6 | storesPkg "github.com/gasper/pkg/storage/stores"
7 | "github.com/spf13/cobra"
8 | "go.uber.org/zap"
9 | )
10 |
11 | func init() {
12 | deleteCmd.PersistentFlags().StringVarP(&fileID, "file-id", "i", "",
13 | "file id to retrieve (required)")
14 |
15 | if err := deleteCmd.MarkPersistentFlagRequired("file-id"); err != nil {
16 | panic("Failed to mark 'file-id' flag as required")
17 | }
18 |
19 | rootCmd.AddCommand(deleteCmd)
20 | }
21 |
22 | var deleteCmd = &cobra.Command{
23 | Use: "delete",
24 | Short: "Delete a file",
25 | Long: "Delete a file from the provided stores",
26 | Run: func(cmd *cobra.Command, args []string) {
27 | gasper, err := pkg.NewGasper(extractStores(), &encryption.Settings{
28 | TurnedOn: false,
29 | })
30 | if err != nil {
31 | zap.L().Fatal("Failed to initialize Gasper", zap.Error(err))
32 | }
33 |
34 | deletedShares := 0
35 |
36 | zap.L().Info("Delete shares from stores")
37 | for _, store := range gasper.Stores() {
38 | store := store
39 | storeType := store.Type()
40 |
41 | if skip := checkStoreAvailability(store); skip {
42 | continue
43 | }
44 |
45 | zap.L().Debug("Available! Delete file from store", zap.String("StoreType", storeType))
46 | if err := store.Delete(fileID); err != nil {
47 | if err == storesPkg.ErrShareNotExists {
48 | zap.L().Debug("No match found in store, trying the next one", zap.String("StoreType",
49 | storeType))
50 | continue
51 | }
52 |
53 | zap.L().Error("Failed to delete share from store", zap.String("StoreType", storeType),
54 | zap.Error(err))
55 | continue // Best effort - keep trying other stores...
56 | }
57 |
58 | deletedShares++
59 | }
60 |
61 | if deletedShares == 0 {
62 | zap.L().Warn("No shares were found/deleted")
63 | return
64 | }
65 |
66 | zap.L().Info("File shares deleted successfully.", zap.String("FileID", fileID),
67 | zap.Int("DeletedShares", deletedShares))
68 | },
69 | }
70 |
--------------------------------------------------------------------------------
/cmd/retrieve.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/gasper/internal/encryption"
5 | "github.com/gasper/pkg"
6 | sharesPkg "github.com/gasper/pkg/shares"
7 | storesPkg "github.com/gasper/pkg/storage/stores"
8 | "github.com/spf13/cobra"
9 | "go.uber.org/zap"
10 | )
11 |
12 | var (
13 | fileID string
14 | destination string
15 | checksum string
16 | decryptionTurnedOn bool
17 | decryptionSalt string
18 | )
19 |
20 | func init() {
21 | retrieveCmd.PersistentFlags().StringVarP(&fileID, "file-id", "i", "",
22 | "file id to retrieve (required)")
23 | retrieveCmd.PersistentFlags().StringVarP(&destination, "destination", "d", "",
24 | "where to save the retrieved file (required)")
25 | retrieveCmd.PersistentFlags().StringVarP(&checksum, "checksum", "m", "",
26 | "checksum of the shared file (required)")
27 | retrieveCmd.PersistentFlags().BoolVarP(&decryptionTurnedOn, "decrypt", "e", false,
28 | "whether file was encrypted before storing it (default: false)")
29 | retrieveCmd.PersistentFlags().StringVarP(&decryptionSalt, "salt", "s", "",
30 | "decryption salt (required if decryption mode is turned on)")
31 |
32 | if err := retrieveCmd.MarkPersistentFlagRequired("file-id"); err != nil {
33 | panic("Failed to mark 'file-id' flag as required")
34 | } else if err := retrieveCmd.MarkPersistentFlagRequired("destination"); err != nil {
35 | panic("Failed to mark 'destination' flag as required")
36 | } else if err := retrieveCmd.MarkPersistentFlagRequired("checksum"); err != nil {
37 | panic("Failed to mark 'checksum' flag as required")
38 | }
39 |
40 | rootCmd.AddCommand(retrieveCmd)
41 | }
42 |
43 | var retrieveCmd = &cobra.Command{
44 | Use: "retrieve",
45 | Short: "Retrieve a file",
46 | Long: "Retrieve a file from the provided stores",
47 | Run: func(cmd *cobra.Command, args []string) {
48 | if decryptionTurnedOn && decryptionSalt == "" {
49 | zap.L().Fatal("Decryption salt is required when decryption mode is turned on")
50 | }
51 |
52 | gasper, err := pkg.NewGasper(extractStores(), &encryption.Settings{
53 | TurnedOn: decryptionTurnedOn,
54 | Salt: decryptionSalt,
55 | })
56 | if err != nil {
57 | zap.L().Fatal("Failed to initialize Gasper", zap.Error(err))
58 | }
59 |
60 | sharedFile := &sharesPkg.SharedFile{
61 | ID: fileID,
62 | Checksum: checksum,
63 | Shares: make([]*sharesPkg.Share, 0),
64 | }
65 |
66 | zap.L().Info("Collect shares from stores")
67 | for _, store := range gasper.Stores() {
68 | store := store
69 | storeType := store.Type()
70 |
71 | if skip := checkStoreAvailability(store); skip {
72 | continue
73 | }
74 |
75 | zap.L().Debug("Available! Search share in store", zap.String("StoreType", storeType))
76 | share, err := store.Get(fileID)
77 | if err != nil {
78 | if err == storesPkg.ErrShareNotExists {
79 | zap.L().Debug("No match found in store, trying the next one", zap.String("StoreType",
80 | storeType))
81 | continue
82 | }
83 |
84 | zap.L().Error("Failed to search share in store", zap.String("StoreType", storeType),
85 | zap.Error(err))
86 | continue
87 | }
88 |
89 | sharedFile.Shares = append(sharedFile.Shares, share)
90 | }
91 |
92 | if len(sharedFile.Shares) == 0 {
93 | zap.L().Warn("No shares found for requested file ID", zap.String("FileID", fileID))
94 | return
95 | } else if len(sharedFile.Shares) < int(minSharesThreshold) {
96 | zap.L().Warn("Didn't find enough shares", zap.Int8("Need", minSharesThreshold),
97 | zap.Int("Got", len(sharedFile.Shares)))
98 | return
99 | }
100 |
101 | zap.L().Debug("Dump shared file")
102 | if err := gasper.DumpSharedFile(sharedFile, destination); err != nil {
103 | zap.L().Error("Failed dump shared file", zap.String("FileID", fileID),
104 | zap.String("Destination", destination), zap.Error(err))
105 | return
106 | }
107 |
108 | zap.L().Info("File retrieved successfully.", zap.String("FileID", fileID))
109 | },
110 | }
111 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "github.com/gasper/internal/logging"
6 | storesPkg "github.com/gasper/pkg/storage/stores"
7 | "github.com/spf13/cobra"
8 | "github.com/spf13/viper"
9 | "go.uber.org/zap"
10 | "os"
11 | )
12 |
13 | var (
14 | storesFile string
15 | verbose bool
16 | )
17 |
18 | var rootCmd = &cobra.Command{
19 | Use: "gasper",
20 | Short: "Gasper lets you store files in a distributed manner on all sorts of different stores",
21 | Long: "Back-up your files in a distributed manner, across multiple stores of your choice.\n" +
22 | "Retrieve them at any point, with only a minimum number of them required for retrieval.",
23 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
24 | logger, err := logging.NewLogger("gasper", verbose)
25 | if err != nil {
26 | return err
27 | }
28 |
29 | zap.ReplaceGlobals(logger)
30 | return nil
31 | },
32 | PersistentPostRun: func(cmd *cobra.Command, args []string) {
33 | _ = zap.L().Sync()
34 | _ = zap.S().Sync()
35 | },
36 | }
37 |
38 | func init() {
39 | rootCmd.PersistentFlags().StringVarP(&storesFile, "stores-config", "c", "", "stores config file (required)")
40 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "extra verbosity")
41 |
42 | if err := rootCmd.MarkPersistentFlagRequired("stores-config"); err != nil {
43 | panic("Failed to mark 'stores-config' flag as required")
44 | }
45 | }
46 |
47 | func extractStores() []storesPkg.Store {
48 | config := viper.New()
49 | config.SetConfigFile(storesFile)
50 | if err := config.ReadInConfig(); err != nil {
51 | zap.L().Fatal("Failed to read stores config file", zap.String("Path", filePath), zap.Error(err))
52 | }
53 |
54 | storesConfigRaw := config.Get("stores")
55 | if storesConfigRaw == nil {
56 | zap.L().Fatal("Empty stores config")
57 | }
58 |
59 | storesConfig, ok := storesConfigRaw.([]interface{})
60 | if !ok {
61 | zap.L().Fatal("Invalid configuration scheme")
62 | }
63 |
64 | stores := make([]storesPkg.Store, 0)
65 | for _, storeConfig := range storesConfig {
66 | storeConfigMap, ok := storeConfig.(map[string]interface{})
67 | if !ok {
68 | zap.L().Fatal("Invalid configuration scheme")
69 | }
70 |
71 | store, err := storesPkg.FromConfig(storeConfigMap)
72 | if err != nil {
73 | zap.L().Fatal("Failed to create store from config", zap.Any("RawConfig", storeConfig),
74 | zap.Error(err))
75 | }
76 |
77 | stores = append(stores, store)
78 | }
79 | return stores
80 | }
81 |
82 | func checkStoreAvailability(store storesPkg.Store) bool {
83 | storeType := store.Type()
84 |
85 | zap.L().Debug("Check store availability", zap.String("StoreType", storeType))
86 | available, err := store.Available()
87 | if err != nil {
88 | zap.L().Warn("Store availability check failed", zap.String("StoreType", storeType), zap.Error(err))
89 | return true
90 | } else if !available {
91 | zap.L().Debug("Skipping unavailable store", zap.String("StoreType", storeType))
92 | return true
93 | }
94 |
95 | return false
96 | }
97 |
98 | func Execute() {
99 | if err := rootCmd.Execute(); err != nil {
100 | fmt.Println(err)
101 | os.Exit(1)
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/cmd/store.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/gasper/internal/encryption"
5 | "github.com/gasper/pkg"
6 | storesPkg "github.com/gasper/pkg/storage/stores"
7 | "github.com/spf13/cobra"
8 | "go.uber.org/zap"
9 | )
10 |
11 | var (
12 | filePath string
13 | shareCount int8
14 | minSharesThreshold int8
15 | encryptionTurnedOn bool
16 | encryptionSalt string
17 | )
18 |
19 | func init() {
20 | storeCmd.PersistentFlags().StringVarP(&filePath, "file", "f", "",
21 | "file to store (required)")
22 | storeCmd.PersistentFlags().Int8VarP(&shareCount, "share-count", "a", 2,
23 | "share count (default: 2)")
24 | storeCmd.PersistentFlags().Int8VarP(&minSharesThreshold, "shares-threshold", "t", 2,
25 | "threshold of minimum shares which can be used for retrieval (default: 2)")
26 | storeCmd.PersistentFlags().BoolVarP(&encryptionTurnedOn, "encrypt", "e", false,
27 | "whether to encrypt file (AES) before storing it (default: false)")
28 | storeCmd.PersistentFlags().StringVarP(&encryptionSalt, "salt", "s", "",
29 | "32-byte long encryption salt (required if encryption mode is turned on)")
30 |
31 | if err := storeCmd.MarkPersistentFlagRequired("file"); err != nil {
32 | panic("Failed to mark 'file' flag as required")
33 | }
34 |
35 | rootCmd.AddCommand(storeCmd)
36 | }
37 |
38 | var storeCmd = &cobra.Command{
39 | Use: "store",
40 | Short: "Store a file",
41 | Long: "Store a file on the provided stores",
42 | Run: func(cmd *cobra.Command, args []string) {
43 | if minSharesThreshold > shareCount {
44 | zap.L().Fatal("Minimum shares threshold cannot be larger than share count")
45 | } else if encryptionTurnedOn && encryptionSalt == "" {
46 | zap.L().Fatal("Encryption salt is required when encryption mode is turned on")
47 | }
48 |
49 | gasper, err := pkg.NewGasper(extractStores(), &encryption.Settings{
50 | TurnedOn: decryptionTurnedOn,
51 | Salt: decryptionSalt,
52 | })
53 | if err != nil {
54 | zap.L().Fatal("Failed to initialize Gasper", zap.Error(err))
55 | }
56 |
57 | zap.L().Info("Getting file shares")
58 | sharedFile, err := gasper.SharesFromFile(filePath, byte(shareCount), byte(minSharesThreshold))
59 | if err != nil {
60 | zap.L().Fatal("Failed to get file shares", zap.Error(err))
61 | }
62 |
63 | zap.L().Info("Check general stores availability")
64 | stores := gasper.Stores()
65 | availableStores := make([]storesPkg.Store, 0, len(stores))
66 | for _, store := range stores {
67 | if skip := checkStoreAvailability(store); skip {
68 | continue
69 | }
70 |
71 | availableStores = append(availableStores, store)
72 | }
73 |
74 | availableStoresCount := len(availableStores)
75 | if int(minSharesThreshold) > availableStoresCount {
76 | zap.L().Error("Not enough available stores", zap.Int8("Need", minSharesThreshold),
77 | zap.Int("Got", availableStoresCount), zap.Int8("Recommended", shareCount))
78 | return
79 | }
80 |
81 | zap.L().Info("Put shares in stores")
82 | i := 0
83 | for _, share := range sharedFile.Shares {
84 | if i > len(stores)-1 {
85 | zap.L().Error("All stores exhausted")
86 | return
87 | }
88 |
89 | store := stores[i]
90 | storeType := store.Type()
91 | i++
92 |
93 | zap.L().Debug("Available! Put share in store", zap.String("StoreType", storeType),
94 | zap.String("ShareID", share.ID))
95 | if err := store.Put(share); err != nil {
96 | zap.L().Error("Failed to put share in store", zap.String("StoreType", storeType),
97 | zap.Error(err))
98 | continue
99 | }
100 | }
101 |
102 | zap.L().Info("Success! Keep the following info for later use", zap.String("FileID", sharedFile.ID),
103 | zap.String("Checksum", sharedFile.Checksum))
104 | },
105 | }
106 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/gasper
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/codahale/sss v0.0.0-20160501174526-0cb9f6d3f7f1
7 | github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
8 | github.com/pkg/errors v0.9.1
9 | github.com/spf13/cobra v1.0.0
10 | github.com/spf13/pflag v1.0.5 // indirect
11 | github.com/spf13/viper v1.7.1
12 | go.uber.org/zap v1.16.0
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
14 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
16 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
17 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
18 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
19 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
20 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
21 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
22 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
23 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
24 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
25 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
26 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
27 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
28 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
29 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
30 | github.com/codahale/sss v0.0.0-20160501174526-0cb9f6d3f7f1 h1:PJJtqFbZH8ZW9PtsfB+ALZKVPRiRwNbPrNe+gliLpGo=
31 | github.com/codahale/sss v0.0.0-20160501174526-0cb9f6d3f7f1/go.mod h1:0Vm/twPonvi1fkJ3kW8TbuttPQ4EyspL1xHUVr1I3uU=
32 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
33 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
34 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
35 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
36 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
37 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
38 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
39 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
41 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
43 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
44 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
45 | github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY=
46 | github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
47 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
48 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
49 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
50 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
51 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
52 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
53 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
54 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
55 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
56 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
57 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
58 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
59 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
60 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
61 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
62 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
63 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
64 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
65 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
66 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
67 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
68 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
69 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
70 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
71 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
72 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
73 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
74 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
75 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
76 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
77 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
78 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
79 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
80 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
81 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
82 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
83 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
84 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
85 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
86 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
87 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
88 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
89 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
90 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
91 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
92 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
93 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
94 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
95 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
96 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
97 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
98 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
99 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
100 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
101 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
102 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
103 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
104 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
105 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
106 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
107 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
108 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
109 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
110 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
111 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
112 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
113 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
114 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
115 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
116 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
117 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
118 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
119 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
120 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
121 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
122 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
123 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
124 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
125 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
126 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
127 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
128 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
129 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
130 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
131 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
132 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
133 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
134 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
135 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
136 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
137 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
138 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
139 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
140 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
141 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
142 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
143 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
144 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
145 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
146 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
147 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
148 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
149 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
150 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
151 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
152 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
153 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
154 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
155 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
156 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
157 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
158 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
159 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
160 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
161 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
162 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
163 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
164 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
165 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
166 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
167 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
168 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
169 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
170 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
171 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
172 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
173 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
174 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
175 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
176 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
177 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
178 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
179 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
180 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
181 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
182 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
183 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
184 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
185 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
186 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
187 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
188 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
189 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
190 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
191 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
192 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
193 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
194 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
195 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
196 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
197 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
198 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
199 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
200 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
201 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
202 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
203 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
204 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
205 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
206 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
207 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
208 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
209 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
210 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
211 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
212 | go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
213 | go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
214 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
215 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
216 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
217 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
218 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
219 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
220 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
221 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
222 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
223 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
224 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
225 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
226 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
227 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
228 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
229 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
230 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
231 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
232 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
233 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
234 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
235 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
236 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
237 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
238 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
239 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
240 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
241 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
242 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
243 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
244 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
245 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
246 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
247 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
248 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
249 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
250 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
251 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
252 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
253 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
254 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
255 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
256 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
257 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
258 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
259 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
260 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
261 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
262 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
263 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
264 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
265 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
266 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
267 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
268 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
269 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
270 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
271 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
272 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
273 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
274 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
275 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
276 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
277 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
278 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
279 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
280 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
281 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
282 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
283 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
284 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
285 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
286 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
287 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
288 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
289 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
290 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
291 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
292 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
293 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
294 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
295 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
296 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
297 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
298 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
299 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8=
300 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
301 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
302 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
303 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
304 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
305 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
306 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
307 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
308 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
309 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
310 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
311 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
312 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
313 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
314 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
315 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
316 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
317 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
318 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
319 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
320 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
321 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
322 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
323 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
324 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
325 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
326 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
327 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
328 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
329 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
330 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
331 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
332 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
333 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
334 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
335 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
336 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
337 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
338 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
339 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
340 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
341 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
342 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
343 |
--------------------------------------------------------------------------------
/internal/encryption/encryptor.go:
--------------------------------------------------------------------------------
1 | package encryption
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/rand"
7 | "github.com/pkg/errors"
8 | )
9 |
10 | // todo: make an interface and support other types of encryption.
11 | type Encryptor struct {
12 | settings *Settings
13 | }
14 |
15 | func NewEncryptor(settings *Settings) *Encryptor {
16 | return &Encryptor{settings: settings}
17 | }
18 |
19 | func (e *Encryptor) Encrypt(data []byte) ([]byte, error) {
20 | if !e.settings.TurnedOn {
21 | return data, nil
22 |
23 | }
24 |
25 | blockCipher, err := e.newAesCipher()
26 | if err != nil {
27 | return nil, errors.WithMessage(err, "new AES cipher")
28 | }
29 |
30 | gcm, err := e.newGCM(blockCipher)
31 | if err != nil {
32 | return nil, errors.WithMessage(err, "new GCM")
33 | }
34 |
35 | nonce := make([]byte, gcm.NonceSize())
36 | if _, err = rand.Read(nonce); err != nil {
37 | return nil, errors.WithMessage(err, "read random nonce")
38 | }
39 |
40 | ciphertext := gcm.Seal(nonce, nonce, data, nil)
41 | return ciphertext, nil
42 | }
43 |
44 | func (e *Encryptor) Decrypt(data []byte) ([]byte, error) {
45 | if !e.settings.TurnedOn {
46 | return data, nil
47 | }
48 |
49 | blockCipher, err := e.newAesCipher()
50 | if err != nil {
51 | return nil, errors.WithMessage(err, "new aes cipher")
52 | }
53 |
54 | gcm, err := e.newGCM(blockCipher)
55 | if err != nil {
56 | return nil, errors.WithMessage(err, "new GCM")
57 | }
58 |
59 | nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():]
60 |
61 | plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
62 | if err != nil {
63 | return nil, errors.WithMessage(err, "open GCM")
64 | }
65 | return plaintext, nil
66 | }
67 |
68 | func (e *Encryptor) newAesCipher() (cipher.Block, error) {
69 | return aes.NewCipher(e.saltBytes())
70 | }
71 |
72 | func (e *Encryptor) newGCM(blockCipher cipher.Block) (cipher.AEAD, error) {
73 | return cipher.NewGCM(blockCipher)
74 | }
75 |
76 | func (e *Encryptor) saltBytes() []byte {
77 | return []byte(e.settings.Salt)
78 | }
79 |
--------------------------------------------------------------------------------
/internal/encryption/settings.go:
--------------------------------------------------------------------------------
1 | package encryption
2 |
3 | import "errors"
4 |
5 | type Settings struct {
6 | TurnedOn bool
7 | Salt string
8 | }
9 |
10 | func (s *Settings) Validate() error {
11 | if s.TurnedOn {
12 | saltBytes := []byte(s.Salt)
13 | k := len(saltBytes)
14 |
15 | switch k {
16 | default:
17 | return errors.New("salt needs to be either 16, 24, or 32-byte long")
18 | case 16, 24, 32:
19 | return nil
20 | }
21 | }
22 |
23 | return nil
24 | }
25 |
--------------------------------------------------------------------------------
/internal/logging/logger.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | "go.uber.org/zap"
6 | )
7 |
8 | func NewLogger(name string, verbose bool) (*zap.Logger, error) {
9 | var (
10 | logger *zap.Logger
11 | err error
12 | )
13 |
14 | if verbose {
15 | logger, err = zap.NewDevelopment()
16 | } else {
17 | logger, err = zap.NewProduction()
18 | }
19 |
20 | if err != nil {
21 | return nil, errors.WithMessage(err, "new logger")
22 | }
23 |
24 | return logger.Named(name), nil
25 | }
26 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/gasper/cmd"
5 | )
6 |
7 | func main() {
8 | cmd.Execute()
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/errors.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import "github.com/pkg/errors"
4 |
5 | var (
6 | ErrInvalidSharesThreshold = errors.New("minimum shares threshold cannot be larger than share count")
7 | ErrNilSharedFile = errors.New("nil shared file")
8 | )
9 |
--------------------------------------------------------------------------------
/pkg/gasper.go:
--------------------------------------------------------------------------------
1 | package pkg
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "github.com/codahale/sss"
7 | petname "github.com/dustinkirkland/golang-petname"
8 | "github.com/gasper/internal/encryption"
9 | sharesPkg "github.com/gasper/pkg/shares"
10 | storesPkg "github.com/gasper/pkg/storage/stores"
11 | "github.com/pkg/errors"
12 | "io/ioutil"
13 | "math/rand"
14 | "os"
15 | "strconv"
16 | "time"
17 | )
18 |
19 | // todo: more elegant and efficient way to read & write big files.
20 |
21 | const (
22 | fileIDWordCount = 2
23 | fileIDWordSeparator = "-"
24 | )
25 |
26 | // Gasper lets you store, load, and delete files in a multi-part, distributed manner, using on Shamir's Secret Sharing.
27 | // It holds a list of stores being used for distribution, and encryption settings.
28 | type Gasper struct {
29 | stores []storesPkg.Store
30 | encryptor *encryption.Encryptor
31 | }
32 |
33 | func NewGasper(stores []storesPkg.Store, encryptionSettings *encryption.Settings) (*Gasper, error) {
34 | if err := encryptionSettings.Validate(); err != nil {
35 | return nil, errors.WithMessage(err, "validate encryption settings")
36 | }
37 |
38 | return &Gasper{
39 | stores: stores,
40 | encryptor: encryption.NewEncryptor(encryptionSettings),
41 | }, nil
42 | }
43 |
44 | // Retrieves stores.
45 | func (g *Gasper) Stores() []storesPkg.Store {
46 | return g.stores
47 | }
48 |
49 | // Splits file into its shares.
50 | func (g *Gasper) SharesFromFile(filePath string, shareCount, minSharesThreshold byte) (*sharesPkg.SharedFile, error) {
51 | if minSharesThreshold > shareCount {
52 | return nil, ErrInvalidSharesThreshold
53 | }
54 |
55 | fileID := g.uniqueFileId()
56 |
57 | data, err := ioutil.ReadFile(filePath)
58 | if err != nil {
59 | return nil, errors.WithMessagef(err, "read file '%s'", filePath)
60 | }
61 |
62 | encryptedData, err := g.encryptor.Encrypt(data)
63 | if err != nil {
64 | return nil, errors.WithMessage(err, "encrypt data")
65 | }
66 |
67 | sharesBytes, err := sss.Split(shareCount, minSharesThreshold, encryptedData)
68 | if err != nil {
69 | return nil, errors.WithMessage(err, "split data to shares")
70 | }
71 |
72 | shares := make([]*sharesPkg.Share, 0, len(sharesBytes))
73 | for shareID, shareBytes := range sharesBytes {
74 | share := &sharesPkg.Share{
75 | ID: strconv.Itoa(int(shareID)),
76 | FileID: fileID,
77 | Data: shareBytes,
78 | }
79 |
80 | shares = append(shares, share)
81 | }
82 |
83 | checksum := md5.Sum(data)
84 |
85 | return &sharesPkg.SharedFile{
86 | ID: fileID,
87 | Checksum: hex.EncodeToString(checksum[:]),
88 | Shares: shares,
89 | }, nil
90 | }
91 |
92 | func (g *Gasper) uniqueFileId() string {
93 | rand.Seed(time.Now().UnixNano())
94 | return petname.Generate(fileIDWordCount, fileIDWordSeparator)
95 | }
96 |
97 | // Dumps shared file to a local filesystem destination.
98 | // If md5 checksum is set, will use it to check file authenticity, otherwise will skip checksum check.
99 | func (g *Gasper) DumpSharedFile(sharedFile *sharesPkg.SharedFile, destination string) error {
100 | if sharedFile == nil {
101 | return ErrNilSharedFile
102 | }
103 |
104 | rawShares := make(map[byte][]byte, len(sharedFile.Shares))
105 | for _, share := range sharedFile.Shares {
106 | shareIDInt, err := strconv.Atoi(share.ID)
107 | if err != nil {
108 | return errors.WithMessage(err, "convert share ID from string to int")
109 | }
110 | rawShares[byte(shareIDInt)] = share.Data
111 | }
112 |
113 | combinedBytes := sss.Combine(rawShares)
114 |
115 | decryptedData, err := g.encryptor.Decrypt(combinedBytes)
116 | if err != nil {
117 | return errors.WithMessage(err, "decrypt data")
118 | }
119 |
120 | if sharedFile.Checksum != "" {
121 | if err := g.validateChecksum(decryptedData, sharedFile.Checksum); err != nil {
122 | return err
123 | }
124 | }
125 |
126 | if err := ioutil.WriteFile(destination, decryptedData, os.ModePerm); err != nil {
127 | return errors.WithMessagef(err, "write file '%s'", destination)
128 | }
129 | return nil
130 | }
131 |
132 | func (g *Gasper) validateChecksum(decryptedData []byte, originalChecksum string) error {
133 | currentChecksumBytes := md5.Sum(decryptedData)
134 | currentChecksum := hex.EncodeToString(currentChecksumBytes[:])
135 |
136 | if currentChecksum != originalChecksum {
137 | return errors.Errorf("got corrupt data, checksums didn't match (original: '%s', got: '%s')",
138 | originalChecksum, currentChecksum)
139 | }
140 | return nil
141 | }
142 |
--------------------------------------------------------------------------------
/pkg/shares/file.go:
--------------------------------------------------------------------------------
1 | package shares
2 |
3 | type SharedFile struct {
4 | ID string
5 | Checksum string
6 | Shares []*Share
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/shares/share.go:
--------------------------------------------------------------------------------
1 | package shares
2 |
3 | type Share struct {
4 | ID string
5 | FileID string
6 | Data []byte
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/storage/stores/errors.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import "github.com/pkg/errors"
4 |
5 | var (
6 | ErrShareNotExists = errors.New("share doesn't exist in store")
7 | ErrMoreThanOneMatch = errors.New("found more than one match for share")
8 |
9 | // Missing/invalid config attributes errors.
10 | ErrInvalidStoreType = errors.New("invalid store type")
11 | ErrMissingStoreTypeAttr = errors.New("missing store type")
12 | ErrMissingDirectoryPathAttr = errors.New("missing 'directory-path' attribute")
13 | ErrInvalidDirectoryPathAttr = errors.New("invalid 'directory-path' attribute (strings only)")
14 | )
15 |
--------------------------------------------------------------------------------
/pkg/storage/stores/factory.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | func FromConfig(config map[string]interface{}) (Store, error) {
4 | storeType, ok := config["type"]
5 | if !ok {
6 | return nil, ErrMissingStoreTypeAttr
7 | }
8 |
9 | switch storeType {
10 | case TypeLocalStore:
11 | return localStore(config)
12 | }
13 |
14 | return nil, ErrInvalidStoreType
15 | }
16 |
17 | func localStore(config map[string]interface{}) (Store, error) {
18 | directoryPathRaw, ok := config["directory-path"]
19 | if !ok {
20 | return nil, ErrMissingDirectoryPathAttr
21 | }
22 |
23 | directoryPath, ok := directoryPathRaw.(string)
24 | if !ok {
25 | return nil, ErrInvalidDirectoryPathAttr
26 | }
27 |
28 | return NewLocalStore(directoryPath)
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/storage/stores/local.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import (
4 | "fmt"
5 | "github.com/gasper/pkg/shares"
6 | "github.com/pkg/errors"
7 | "io/ioutil"
8 | "os"
9 | "path"
10 | "path/filepath"
11 | "strings"
12 | )
13 |
14 | const TypeLocalStore = "local"
15 |
16 | // Stores files in a local directory.
17 | // Note: needs to get an absolute path.
18 | type LocalStore struct {
19 | directoryPath string
20 | }
21 |
22 | func NewLocalStore(directoryPath string) (*LocalStore, error) {
23 | return &LocalStore{
24 | directoryPath: directoryPath,
25 | }, nil
26 | }
27 |
28 | func (ls *LocalStore) Type() string {
29 | return TypeLocalStore
30 | }
31 |
32 | func (ls *LocalStore) Available() (bool, error) {
33 | _, err := os.Stat(ls.directoryPath)
34 | if err != nil {
35 | if os.IsNotExist(err) {
36 | return false, nil
37 | }
38 | return false, err
39 | }
40 | return true, err
41 | }
42 |
43 | func (ls *LocalStore) Put(share *shares.Share) error {
44 | filePath := path.Join(ls.directoryPath, ls.filename(share))
45 | return ioutil.WriteFile(filePath, share.Data, os.ModePerm)
46 | }
47 |
48 | func (ls *LocalStore) Get(fileID string) (*shares.Share, error) {
49 | filePath, err := ls.findFileByID(fileID)
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | data, err := ioutil.ReadFile(filePath)
55 | if err != nil {
56 | return nil, errors.WithMessagef(err, "read file '%s'", filePath)
57 | }
58 |
59 | splitFilename := strings.Split(path.Base(filePath), ".")
60 | if len(splitFilename) != 3 { // Note: should not happen, otherwise glob wouldn't match. But just in case...
61 | return nil, errors.New("invalid file format (should be: '..gasper')")
62 | }
63 |
64 | shareID := splitFilename[1]
65 |
66 | return &shares.Share{
67 | ID: shareID,
68 | FileID: fileID,
69 | Data: data,
70 | }, nil
71 | }
72 |
73 | func (ls *LocalStore) Delete(fileID string) error {
74 | filePath, err := ls.findFileByID(fileID)
75 | if err != nil {
76 | return err
77 | }
78 |
79 | return os.RemoveAll(filePath)
80 | }
81 |
82 | func (ls *LocalStore) filename(share *shares.Share) string {
83 | return fmt.Sprintf("%s.%s.gasper", share.FileID, share.ID)
84 | }
85 |
86 | func (ls *LocalStore) findFileByID(fileID string) (string, error) {
87 | pattern := path.Join(ls.directoryPath, fmt.Sprintf("%s.*.gasper", fileID))
88 | matches, err := filepath.Glob(pattern)
89 | if err != nil {
90 | return "", errors.WithMessagef(err, "glob pattern '%s'", pattern)
91 | }
92 |
93 | if len(matches) == 0 {
94 | return "", ErrShareNotExists
95 | } else if len(matches) > 1 {
96 | return "", ErrMoreThanOneMatch
97 | }
98 |
99 | return matches[0], nil
100 | }
101 |
--------------------------------------------------------------------------------
/pkg/storage/stores/store.go:
--------------------------------------------------------------------------------
1 | package stores
2 |
3 | import "github.com/gasper/pkg/shares"
4 |
5 | // Store lets you store shares.
6 | type Store interface {
7 | // Store type.
8 | Type() string
9 |
10 | // Is store available?
11 | // Useful especially for remote stores, such as ftp servers or s3 buckets.
12 | Available() (bool, error)
13 |
14 | // Puts a share in store.
15 | Put(share *shares.Share) error
16 |
17 | // Retrieves a share from store.
18 | // If no share with the given File ID exists, returns ErrShareNotExists.
19 | Get(fileID string) (*shares.Share, error)
20 |
21 | // Deletes a share from store.
22 | // If no share with the given File ID exists, returns ErrShareNotExists.
23 | Delete(fileID string) error
24 | }
25 |
--------------------------------------------------------------------------------