Skip to content

Commit

Permalink
Added support for setting and changing repository client options (des…
Browse files Browse the repository at this point in the history
…cription, read-only, hostname, username) (#589)

* repo: refactored client-specific options (hostname,username,description,readonly) into new struct that is JSON-compatible with current config

* cli: added 'repository set-client' to configure parameters of connected repository

* cli: cleaned up 'repository status' output
  • Loading branch information
jkowalski committed Sep 4, 2020
1 parent a5838ff commit 29ce181
Show file tree
Hide file tree
Showing 28 changed files with 309 additions and 141 deletions.
5 changes: 5 additions & 0 deletions .golangci.yml
Expand Up @@ -13,6 +13,11 @@ linters-settings:
goconst:
min-len: 5
min-occurrences: 3
exhaustive:
# indicates that switch statements are to be considered exhaustive if a
# 'default' case is present, even if all enum members aren't listed in the
# switch
default-signifies-exhaustive: true
misspell:
locale: US
lll:
Expand Down
2 changes: 1 addition & 1 deletion cli/app.go
Expand Up @@ -167,7 +167,7 @@ func maybeRunMaintenance(ctx context.Context, rep repo.Repository) error {
return nil
}

if rep.IsReadOnly() {
if rep.ClientOptions().ReadOnly {
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion cli/command_policy.go
Expand Up @@ -31,7 +31,7 @@ func policyTargets(ctx context.Context, rep repo.Repository, globalFlag *bool, t
continue
}

target, err := snapshot.ParseSourceInfo(ts, rep.Hostname(), rep.Username())
target, err := snapshot.ParseSourceInfo(ts, rep.ClientOptions().Hostname, rep.ClientOptions().Username)
if err != nil {
return nil, err
}
Expand Down
11 changes: 8 additions & 3 deletions cli/command_repository_connect.go
Expand Up @@ -23,6 +23,7 @@ var (
connectUsername string
connectCheckForUpdates bool
connectReadonly bool
connectDescription string
)

func setupConnectOptions(cmd *kingpin.CmdClause) {
Expand All @@ -37,6 +38,7 @@ func setupConnectOptions(cmd *kingpin.CmdClause) {
cmd.Flag("override-username", "Override username used by this repository connection").Hidden().StringVar(&connectUsername)
cmd.Flag("check-for-updates", "Periodically check for Kopia updates on GitHub").Default("true").Envar(checkForUpdatesEnvar).BoolVar(&connectCheckForUpdates)
cmd.Flag("readonly", "Make repository read-only to avoid accidental changes").BoolVar(&connectReadonly)
cmd.Flag("description", "Human-readable description of the repository").StringVar(&connectDescription)
}

func connectOptions() *repo.ConnectOptions {
Expand All @@ -48,9 +50,12 @@ func connectOptions() *repo.ConnectOptions {
MaxMetadataCacheSizeBytes: connectMaxMetadataCacheSizeMB << 20, //nolint:gomnd
MaxListCacheDurationSec: int(connectMaxListCacheDuration.Seconds()),
},
HostnameOverride: connectHostname,
UsernameOverride: connectUsername,
ReadOnly: connectReadonly,
ClientOptions: repo.ClientOptions{
Hostname: connectHostname,
Username: connectUsername,
ReadOnly: connectReadonly,
Description: connectDescription,
},
}
}

Expand Down
78 changes: 78 additions & 0 deletions cli/command_repository_set_client.go
@@ -0,0 +1,78 @@
package cli

import (
"context"

"github.com/pkg/errors"

"github.com/kopia/kopia/repo"
)

var (
repoClientOptionsCommand = repositoryCommands.Command("set-client", "Set repository client options.")

repoClientOptionsReadOnly = repoClientOptionsCommand.Flag("read-only", "Set repository to read-only").Bool()
repoClientOptionsReadWrite = repoClientOptionsCommand.Flag("read-write", "Set repository to read-write").Bool()
repoClientOptionsDescription = repoClientOptionsCommand.Flag("description", "Change description").Strings()
repoClientOptionsUsername = repoClientOptionsCommand.Flag("username", "Change username").Strings()
repoClientOptionsHostname = repoClientOptionsCommand.Flag("hostname", "Change hostname").Strings()
)

func runRepoClientOptionsCommand(ctx context.Context, rep repo.Repository) error {
var anyChange bool

opt := rep.ClientOptions()

if *repoClientOptionsReadOnly {
if opt.ReadOnly {
printStderr("Repository is already in read-only mode.\n")
} else {
opt.ReadOnly = true
anyChange = true

printStderr("Setting repository to read-only mode.\n")
}
}

if *repoClientOptionsReadWrite {
if !opt.ReadOnly {
printStderr("Repository is already in read-write mode.\n")
} else {
opt.ReadOnly = false
anyChange = true

printStderr("Setting repository to read-write mode.\n")
}
}

if v := *repoClientOptionsDescription; len(v) > 0 {
opt.Description = v[0]
anyChange = true

printStderr("Setting description to %v\n", opt.Description)
}

if v := *repoClientOptionsUsername; len(v) > 0 {
opt.Username = v[0]
anyChange = true

printStderr("Setting local username to %v\n", opt.Username)
}

if v := *repoClientOptionsHostname; len(v) > 0 {
opt.Hostname = v[0]
anyChange = true

printStderr("Setting local hostname to %v\n", opt.Hostname)
}

if !anyChange {
return errors.Errorf("no changes")
}

return repo.SetClientOptions(ctx, repositoryConfigFileName(), opt)
}

func init() {
repoClientOptionsCommand.Action(repositoryAction(runRepoClientOptionsCommand))
}
39 changes: 23 additions & 16 deletions cli/command_repository_status.go
Expand Up @@ -21,28 +21,35 @@ var (
statusReconnectTokenIncludePassword = statusCommand.Flag("reconnect-token-with-password", "Include password in reconnect token").Short('s').Bool()
)

func runStatusCommand(ctx context.Context, rep *repo.DirectRepository) error {
fmt.Printf("Config file: %v\n", rep.ConfigFile)
func runStatusCommand(ctx context.Context, rep repo.Repository) error {
fmt.Printf("Config file: %v\n", repositoryConfigFileName())
fmt.Println()
fmt.Printf("Description: %v\n", rep.ClientOptions().Description)
fmt.Printf("Hostname: %v\n", rep.ClientOptions().Hostname)
fmt.Printf("Username: %v\n", rep.ClientOptions().Username)
fmt.Printf("Read-only: %v\n", rep.ClientOptions().ReadOnly)

dr, ok := rep.(*repo.DirectRepository)
if !ok {
return nil
}

ci := rep.Blobs.ConnectionInfo()
fmt.Println()

ci := dr.Blobs.ConnectionInfo()
fmt.Printf("Storage type: %v\n", ci.Type)

if cjson, err := json.MarshalIndent(scrubber.ScrubSensitiveData(reflect.ValueOf(ci.Config)).Interface(), " ", " "); err == nil {
fmt.Printf("Storage config: %v\n", string(cjson))
}

fmt.Println()
fmt.Printf("Unique ID: %x\n", rep.UniqueID)
fmt.Printf("Hostname: %v\n", rep.Hostname())
fmt.Printf("Username: %v\n", rep.Username())
fmt.Printf("Read-only: %v\n", rep.IsReadOnly())

fmt.Println()
fmt.Printf("Hash: %v\n", rep.Content.Format.Hash)
fmt.Printf("Encryption: %v\n", rep.Content.Format.Encryption)
fmt.Printf("Splitter: %v\n", rep.Objects.Format.Splitter)
fmt.Printf("Format version: %v\n", rep.Content.Format.Version)
fmt.Printf("Max pack length: %v\n", units.BytesStringBase2(int64(rep.Content.Format.MaxPackSize)))
fmt.Printf("Unique ID: %x\n", dr.UniqueID)
fmt.Printf("Hash: %v\n", dr.Content.Format.Hash)
fmt.Printf("Encryption: %v\n", dr.Content.Format.Encryption)
fmt.Printf("Splitter: %v\n", dr.Objects.Format.Splitter)
fmt.Printf("Format version: %v\n", dr.Content.Format.Version)
fmt.Printf("Max pack length: %v\n", units.BytesStringBase2(int64(dr.Content.Format.MaxPackSize)))

if !*statusReconnectToken {
return nil
Expand All @@ -59,7 +66,7 @@ func runStatusCommand(ctx context.Context, rep *repo.DirectRepository) error {
}
}

tok, err := rep.Token(pass)
tok, err := dr.Token(pass)
if err != nil {
return err
}
Expand Down Expand Up @@ -103,5 +110,5 @@ func scanCacheDir(dirname string) (fileCount int, totalFileLength int64, err err
}

func init() {
statusCommand.Action(directRepositoryAction(runStatusCommand))
statusCommand.Action(repositoryAction(runStatusCommand))
}
8 changes: 4 additions & 4 deletions cli/command_snapshot_create.go
Expand Up @@ -78,8 +78,8 @@ func runSnapshotCommand(ctx context.Context, rep repo.Repository) error {

sourceInfo := snapshot.SourceInfo{
Path: filepath.Clean(dir),
Host: rep.Hostname(),
UserName: rep.Username(),
Host: rep.ClientOptions().Hostname,
UserName: rep.ClientOptions().Username,
}

if err := snapshotSingleSource(ctx, rep, u, sourceInfo); err != nil {
Expand Down Expand Up @@ -269,7 +269,7 @@ func findPreviousSnapshotManifest(ctx context.Context, rep repo.Repository, sour
}

func getLocalBackupPaths(ctx context.Context, rep repo.Repository) ([]string, error) {
log(ctx).Debugf("Looking for previous backups of '%v@%v'...", rep.Hostname(), rep.Username())
log(ctx).Debugf("Looking for previous backups of '%v@%v'...", rep.ClientOptions().Hostname, rep.ClientOptions().Username)

sources, err := snapshot.ListSources(ctx, rep)
if err != nil {
Expand All @@ -279,7 +279,7 @@ func getLocalBackupPaths(ctx context.Context, rep repo.Repository) ([]string, er
var result []string

for _, src := range sources {
if src.Host == rep.Hostname() && src.UserName == rep.Username() {
if src.Host == rep.ClientOptions().Hostname && src.UserName == rep.ClientOptions().Username {
result = append(result, src.Path)
}
}
Expand Down
4 changes: 2 additions & 2 deletions cli/command_snapshot_estimate.go
Expand Up @@ -72,8 +72,8 @@ func runSnapshotEstimateCommand(ctx context.Context, rep repo.Repository) error

sourceInfo := snapshot.SourceInfo{
Path: filepath.Clean(path),
Host: rep.Hostname(),
UserName: rep.Username(),
Host: rep.ClientOptions().Hostname,
UserName: rep.ClientOptions().Username,
}

var stats snapshot.Stats
Expand Down
2 changes: 1 addition & 1 deletion cli/command_snapshot_expire.go
Expand Up @@ -25,7 +25,7 @@ func getSnapshotSourcesToExpire(ctx context.Context, rep repo.Repository) ([]sna
var result []snapshot.SourceInfo

for _, p := range *snapshotExpirePaths {
src, err := snapshot.ParseSourceInfo(p, rep.Hostname(), rep.Username())
src, err := snapshot.ParseSourceInfo(p, rep.ClientOptions().Hostname, rep.ClientOptions().Username)
if err != nil {
return nil, err
}
Expand Down
8 changes: 5 additions & 3 deletions cli/command_snapshot_list.go
Expand Up @@ -71,7 +71,7 @@ func findManifestIDs(ctx context.Context, rep repo.Repository, source string) ([
return man, "", err
}

si, err := snapshot.ParseSourceInfo(source, rep.Hostname(), rep.Username())
si, err := snapshot.ParseSourceInfo(source, rep.ClientOptions().Hostname, rep.ClientOptions().Username)
if err != nil {
return nil, "", errors.Errorf("invalid directory: '%s': %s", source, err)
}
Expand Down Expand Up @@ -103,11 +103,13 @@ func shouldOutputSnapshotSource(rep repo.Repository, src snapshot.SourceInfo) bo
return true
}

if src.Host != rep.Hostname() {
co := rep.ClientOptions()

if src.Host != co.Hostname {
return false
}

return src.UserName == rep.Username()
return src.UserName == co.Username
}

func outputManifestGroups(ctx context.Context, rep repo.Repository, manifests []*snapshot.Manifest, relPathParts []string) error {
Expand Down
2 changes: 1 addition & 1 deletion cli/command_snapshot_migrate.go
Expand Up @@ -283,7 +283,7 @@ func getSourcesToMigrate(ctx context.Context, rep repo.Repository) ([]snapshot.S
var result []snapshot.SourceInfo

for _, s := range *migrateSources {
si, err := snapshot.ParseSourceInfo(s, rep.Hostname(), rep.Username())
si, err := snapshot.ParseSourceInfo(s, rep.ClientOptions().Hostname, rep.ClientOptions().Username)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion cli/command_snapshot_verify.go
Expand Up @@ -255,7 +255,7 @@ func loadSourceManifests(ctx context.Context, rep repo.Repository, sources []str
manifestIDs = append(manifestIDs, man...)
} else {
for _, srcStr := range sources {
src, err := snapshot.ParseSourceInfo(srcStr, rep.Hostname(), rep.Username())
src, err := snapshot.ParseSourceInfo(srcStr, rep.ClientOptions().Hostname, rep.ClientOptions().Username)
if err != nil {
return nil, errors.Wrapf(err, "error parsing %q", srcStr)
}
Expand Down
2 changes: 0 additions & 2 deletions fs/localfs/local_fs.go
Expand Up @@ -259,7 +259,6 @@ func NewEntry(path string) (fs.Entry, error) {
return nil, err
}

// nolint:exhaustive
switch fi.Mode() & os.ModeType {
case os.ModeDir:
return &filesystemDirectory{newEntry(fi, filepath.Dir(path))}, nil
Expand Down Expand Up @@ -290,7 +289,6 @@ func Directory(path string) (fs.Directory, error) {
}

func entryFromChildFileInfo(fi os.FileInfo, parentDir string) (fs.Entry, error) {
// nolint:exhaustive
switch fi.Mode() & os.ModeType {
case os.ModeDir:
return &filesystemDirectory{newEntry(fi, parentDir)}, nil
Expand Down
2 changes: 1 addition & 1 deletion htmlui/src/RepoStatus.js
Expand Up @@ -121,7 +121,7 @@ export class RepoStatus extends Component {
<Form.Row>
<Form.Group as={Col}>
<Form.Label>Connected as:</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.username + "@" + this.state.status.host} />
<Form.Control readOnly defaultValue={this.state.status.username + "@" + this.state.status.hostname} />
</Form.Group>
</Form.Row>
<Button variant="danger" onClick={this.disconnect}>Disconnect</Button>
Expand Down
1 change: 0 additions & 1 deletion internal/scrubber/scrub_sensitive.go
Expand Up @@ -9,7 +9,6 @@ import (
// ScrubSensitiveData returns a copy of a given value with sensitive fields scrubbed.
// Fields are marked as sensitive with truct field tag `kopia:"sensitive"`.
func ScrubSensitiveData(v reflect.Value) reflect.Value {
// nolint:exhaustive
switch v.Kind() {
case reflect.Ptr:
return ScrubSensitiveData(v.Elem()).Addr()
Expand Down
26 changes: 12 additions & 14 deletions internal/server/api_repo.go
Expand Up @@ -47,16 +47,15 @@ func (s *Server) handleRepoStatus(ctx context.Context, r *http.Request, body []b
dr, ok := s.rep.(*repo.DirectRepository)
if ok {
return &serverapi.StatusResponse{
Connected: true,
ConfigFile: dr.ConfigFile,
CacheDir: dr.Content.CachingOptions.CacheDirectory,
Hash: dr.Content.Format.Hash,
Encryption: dr.Content.Format.Encryption,
MaxPackSize: dr.Content.Format.MaxPackSize,
Splitter: dr.Objects.Format.Splitter,
Storage: dr.Blobs.ConnectionInfo().Type,
Username: dr.Username(),
Host: dr.Hostname(),
Connected: true,
ConfigFile: dr.ConfigFile,
CacheDir: dr.Content.CachingOptions.CacheDirectory,
Hash: dr.Content.Format.Hash,
Encryption: dr.Content.Format.Encryption,
MaxPackSize: dr.Content.Format.MaxPackSize,
Splitter: dr.Objects.Format.Splitter,
Storage: dr.Blobs.ConnectionInfo().Type,
ClientOptions: dr.ClientOptions(),
}, nil
}

Expand All @@ -65,9 +64,8 @@ func (s *Server) handleRepoStatus(ctx context.Context, r *http.Request, body []b
}

result := &serverapi.StatusResponse{
Connected: true,
Username: s.rep.Username(),
Host: s.rep.Hostname(),
Connected: true,
ClientOptions: s.rep.ClientOptions(),
}

if rr, ok := s.rep.(remoteRepository); ok {
Expand Down Expand Up @@ -128,7 +126,7 @@ func (s *Server) handleRepoCreate(ctx context.Context, r *http.Request, body []b

if dr, ok := s.rep.(*repo.DirectRepository); ok {
p := maintenance.DefaultParams()
p.Owner = s.rep.Username() + "@" + s.rep.Hostname()
p.Owner = dr.Username() + "@" + dr.Hostname()

if err := maintenance.SetParams(ctx, dr, &p); err != nil {
return nil, internalServerError(errors.Wrap(err, "unable to set maintenance params"))
Expand Down

0 comments on commit 29ce181

Please sign in to comment.