mirror of
https://github.com/kopia/kopia.git
synced 2025-12-23 22:57:50 -05:00
Generalize a couple of functions in the units package using generics. This allows removing duplicate code and simplifying callers by removing unnecessary integer conversions. Additional cleanups: - make "/s" part of the Printf format string ; - simplify setSizeMBParameter; - generalize cli.maybeHumanReadable*` helpers; - remove unneeded receiver in commandRepositorySetParameters helpers.
304 lines
9.2 KiB
Go
304 lines
9.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/kopia/kopia/internal/scrubber"
|
|
"github.com/kopia/kopia/internal/units"
|
|
"github.com/kopia/kopia/repo"
|
|
"github.com/kopia/kopia/repo/blob"
|
|
"github.com/kopia/kopia/repo/content/index"
|
|
"github.com/kopia/kopia/repo/format"
|
|
)
|
|
|
|
type commandRepositoryStatus struct {
|
|
statusReconnectToken bool
|
|
statusReconnectTokenIncludePassword bool
|
|
|
|
svc advancedAppServices
|
|
jo jsonOutput
|
|
out textOutput
|
|
}
|
|
|
|
// RepositoryStatus is used to display the repository info in JSON format.
|
|
type RepositoryStatus struct {
|
|
ConfigFile string `json:"configFile"`
|
|
UniqueIDHex string `json:"uniqueIDHex"`
|
|
|
|
ClientOptions repo.ClientOptions `json:"clientOptions"`
|
|
Storage blob.ConnectionInfo `json:"storage"`
|
|
Capacity *blob.Capacity `json:"volume,omitempty"`
|
|
ContentFormat format.ContentFormat `json:"contentFormat"`
|
|
ObjectFormat format.ObjectFormat `json:"objectFormat"`
|
|
BlobRetention format.BlobStorageConfiguration `json:"blobRetention"`
|
|
}
|
|
|
|
func (c *commandRepositoryStatus) setup(svc advancedAppServices, parent commandParent) {
|
|
cmd := parent.Command("status", "Display the status of connected repository.")
|
|
cmd.Flag("reconnect-token", "Display reconnect command").Short('t').BoolVar(&c.statusReconnectToken)
|
|
cmd.Flag("reconnect-token-with-password", "Include password in reconnect token").Short('s').BoolVar(&c.statusReconnectTokenIncludePassword)
|
|
cmd.Action(svc.repositoryReaderAction(c.run))
|
|
|
|
c.svc = svc
|
|
c.out.setup(svc)
|
|
c.jo.setup(svc, cmd)
|
|
}
|
|
|
|
func (c *commandRepositoryStatus) outputJSON(ctx context.Context, r repo.Repository) error {
|
|
s := RepositoryStatus{
|
|
ConfigFile: c.svc.repositoryConfigFileName(),
|
|
ClientOptions: r.ClientOptions(),
|
|
}
|
|
|
|
dr, ok := r.(repo.DirectRepository)
|
|
if ok {
|
|
ci := dr.BlobReader().ConnectionInfo()
|
|
s.UniqueIDHex = hex.EncodeToString(dr.UniqueID())
|
|
s.ObjectFormat = dr.ObjectFormat()
|
|
s.BlobRetention, _ = dr.FormatManager().BlobCfgBlob(ctx)
|
|
s.Storage = scrubber.ScrubSensitiveData(reflect.ValueOf(ci)).Interface().(blob.ConnectionInfo) //nolint:forcetypeassert
|
|
s.ContentFormat = dr.FormatManager().ScrubbedContentFormat()
|
|
|
|
switch cp, err := dr.BlobVolume().GetCapacity(ctx); {
|
|
case err == nil:
|
|
s.Capacity = &cp
|
|
case errors.Is(err, blob.ErrNotAVolume):
|
|
// This is okay, we will just not populate the result.
|
|
default:
|
|
return errors.Wrap(err, "unable to get storage volume capacity")
|
|
}
|
|
}
|
|
|
|
c.out.printStdout("%s\n", c.jo.jsonBytes(s))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *commandRepositoryStatus) dumpUpgradeStatus(ctx context.Context, dr repo.DirectRepository) error {
|
|
drw, isDr := dr.(repo.DirectRepositoryWriter)
|
|
if !isDr {
|
|
return nil
|
|
}
|
|
|
|
l, err := drw.FormatManager().GetUpgradeLockIntent(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get the upgrade lock intent")
|
|
}
|
|
|
|
if l == nil {
|
|
return nil
|
|
}
|
|
|
|
locked, drainedClients := l.IsLocked(drw.Time())
|
|
upgradeTime := l.UpgradeTime()
|
|
|
|
c.out.printStdout("\n")
|
|
c.out.printStdout("Ongoing upgrade: %s\n", l.Message)
|
|
c.out.printStdout("Upgrade Time: %s\n", upgradeTime.Local())
|
|
c.out.printStdout("Upgrade Owner: %s\n", l.OwnerID)
|
|
|
|
if locked {
|
|
c.out.printStdout("Upgrade lock: Locked\n")
|
|
} else {
|
|
c.out.printStdout("Upgrade lock: Unlocked\n")
|
|
}
|
|
|
|
if drainedClients {
|
|
c.out.printStdout("Lock status: Fully Established\n")
|
|
} else {
|
|
c.out.printStdout("Lock status: Draining\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *commandRepositoryStatus) dumpRetentionStatus(ctx context.Context, dr repo.DirectRepository) {
|
|
if blobcfg, _ := dr.FormatManager().BlobCfgBlob(ctx); blobcfg.IsRetentionEnabled() {
|
|
c.out.printStdout("\n")
|
|
c.out.printStdout("Blob retention mode: %s\n", blobcfg.RetentionMode)
|
|
c.out.printStdout("Blob retention period: %s\n", blobcfg.RetentionPeriod)
|
|
}
|
|
}
|
|
|
|
//nolint:funlen,gocyclo
|
|
func (c *commandRepositoryStatus) run(ctx context.Context, rep repo.Repository) error {
|
|
if c.jo.jsonOutput {
|
|
return c.outputJSON(ctx, rep)
|
|
}
|
|
|
|
c.out.printStdout("Config file: %v\n", c.svc.repositoryConfigFileName())
|
|
c.out.printStdout("\n")
|
|
c.out.printStdout("Description: %v\n", rep.ClientOptions().Description)
|
|
c.out.printStdout("Hostname: %v\n", rep.ClientOptions().Hostname)
|
|
c.out.printStdout("Username: %v\n", rep.ClientOptions().Username)
|
|
c.out.printStdout("Read-only: %v\n", rep.ClientOptions().ReadOnly)
|
|
|
|
t := rep.ClientOptions().FormatBlobCacheDuration
|
|
if t > 0 {
|
|
c.out.printStdout("Format blob cache: %v\n", t)
|
|
} else {
|
|
c.out.printStdout("Format blob cache: disabled\n")
|
|
}
|
|
|
|
dr, isDr := rep.(repo.DirectRepository)
|
|
if !isDr {
|
|
return nil
|
|
}
|
|
|
|
c.out.printStdout("\n")
|
|
|
|
ci := dr.BlobReader().ConnectionInfo()
|
|
c.out.printStdout("Storage type: %v\n", ci.Type)
|
|
|
|
switch cp, err := dr.BlobVolume().GetCapacity(ctx); {
|
|
case err == nil:
|
|
c.out.printStdout("Storage capacity: %v\n", units.BytesString(cp.SizeB))
|
|
c.out.printStdout("Storage available: %v\n", units.BytesString(cp.FreeB))
|
|
case errors.Is(err, blob.ErrNotAVolume):
|
|
c.out.printStdout("Storage capacity: unbounded\n")
|
|
default:
|
|
return errors.Wrap(err, "unable to get storage volume capacity")
|
|
}
|
|
|
|
if cjson, err := json.MarshalIndent(scrubber.ScrubSensitiveData(reflect.ValueOf(ci.Config)).Interface(), " ", " "); err == nil {
|
|
c.out.printStdout("Storage config: %v\n", string(cjson))
|
|
}
|
|
|
|
contentFormat := dr.ContentReader().ContentFormat()
|
|
|
|
mp, mperr := contentFormat.GetMutableParameters(ctx)
|
|
if mperr != nil {
|
|
return errors.Wrap(mperr, "mutable parameters")
|
|
}
|
|
|
|
c.out.printStdout("\n")
|
|
c.out.printStdout("Unique ID: %x\n", dr.UniqueID())
|
|
c.out.printStdout("Hash: %v\n", contentFormat.GetHashFunction())
|
|
c.out.printStdout("Encryption: %v\n", contentFormat.GetEncryptionAlgorithm())
|
|
c.out.printStdout("Splitter: %v\n", dr.ObjectFormat().Splitter)
|
|
c.out.printStdout("Format version: %v\n", mp.Version)
|
|
c.out.printStdout("Content compression: %v\n", mp.IndexVersion >= index.Version2)
|
|
c.out.printStdout("Password changes: %v\n", contentFormat.SupportsPasswordChange())
|
|
|
|
c.outputRequiredFeatures(ctx, dr)
|
|
|
|
c.out.printStdout("Max pack length: %v\n", units.BytesString(mp.MaxPackSize))
|
|
c.out.printStdout("Index Format: v%v\n", mp.IndexVersion)
|
|
|
|
emgr, epochMgrEnabled, emerr := dr.ContentReader().EpochManager(ctx)
|
|
if emerr != nil {
|
|
return errors.Wrap(emerr, "epoch manager")
|
|
}
|
|
|
|
if epochMgrEnabled {
|
|
c.out.printStdout("\n")
|
|
c.out.printStdout("Epoch Manager: enabled\n")
|
|
|
|
snap, err := emgr.Current(ctx)
|
|
if err == nil {
|
|
c.out.printStdout("Current Epoch: %v\n", snap.WriteEpoch)
|
|
}
|
|
|
|
c.out.printStdout("\n")
|
|
c.out.printStdout("Epoch refresh frequency: %v\n", mp.EpochParameters.EpochRefreshFrequency)
|
|
c.out.printStdout("Epoch advance on: %v blobs or %v, minimum %v\n", mp.EpochParameters.EpochAdvanceOnCountThreshold, units.BytesString(mp.EpochParameters.EpochAdvanceOnTotalSizeBytesThreshold), mp.EpochParameters.MinEpochDuration)
|
|
c.out.printStdout("Epoch cleanup margin: %v\n", mp.EpochParameters.CleanupSafetyMargin)
|
|
c.out.printStdout("Epoch checkpoint every: %v epochs\n", mp.EpochParameters.FullCheckpointFrequency)
|
|
} else {
|
|
c.out.printStdout("Epoch Manager: disabled\n")
|
|
}
|
|
|
|
c.dumpRetentionStatus(ctx, dr)
|
|
|
|
if err := c.dumpUpgradeStatus(ctx, dr); err != nil {
|
|
return errors.Wrap(err, "failed to dump upgrade status")
|
|
}
|
|
|
|
if !c.statusReconnectToken {
|
|
return nil
|
|
}
|
|
|
|
pass := ""
|
|
|
|
if c.statusReconnectTokenIncludePassword {
|
|
var err error
|
|
|
|
pass, err = c.svc.getPasswordFromFlags(ctx, false, true)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting password")
|
|
}
|
|
}
|
|
|
|
tok, err := dr.Token(pass)
|
|
if err != nil {
|
|
return errors.Wrap(err, "error computing repository token")
|
|
}
|
|
|
|
c.out.printStdout("\nTo reconnect to the repository use:\n\n$ kopia repository connect from-config --token %v\n\n", tok)
|
|
|
|
if pass != "" {
|
|
c.out.printStdout("NOTICE: The token printed above can be trivially decoded to reveal the repository password. Do not store it in an unsecured place.\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *commandRepositoryStatus) outputRequiredFeatures(ctx context.Context, dr repo.DirectRepository) {
|
|
if req, _ := dr.FormatManager().RequiredFeatures(ctx); len(req) > 0 {
|
|
var featureIDs []string
|
|
|
|
for _, r := range req {
|
|
featureIDs = append(featureIDs, string(r.Feature))
|
|
}
|
|
|
|
c.out.printStdout("Required Features: %v\n", strings.Join(featureIDs, " "))
|
|
}
|
|
}
|
|
|
|
func scanCacheDir(dirname string) (fileCount int, totalFileLength int64, err error) {
|
|
entries, err := os.ReadDir(dirname)
|
|
if err != nil {
|
|
return 0, 0, errors.Wrap(err, "unable to read cache directory")
|
|
}
|
|
|
|
for _, e := range entries {
|
|
fi, err := e.Info()
|
|
if os.IsNotExist(err) {
|
|
// we lost the race, the file was deleted since it was listed.
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
return 0, 0, errors.Wrap(err, "unable to read file info")
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
subdir := filepath.Join(dirname, fi.Name())
|
|
|
|
c, l, err2 := scanCacheDir(subdir)
|
|
if err2 != nil {
|
|
return 0, 0, err2
|
|
}
|
|
|
|
fileCount += c
|
|
totalFileLength += l
|
|
|
|
continue
|
|
}
|
|
|
|
fileCount++
|
|
|
|
totalFileLength += fi.Size()
|
|
}
|
|
|
|
return fileCount, totalFileLength, nil
|
|
}
|