Improvements and cleanups for connecting to kopia server (#870)

* repo: refactored connect code set up cache for server repositories

- improved logic to close the cache on last connection
- preemptively add all contents with a prefix to the cache
- refactored how config is loaded and saved

Now cache dir will be stored as relative and resolved to absolute as
part of loading and saving the file, in all other places cache dir
is expected to be absolute.

* server: removed cache directory from the API and UI

This won't be easily available and does not seem useful to expose
anyway.

* cli: enabled cache commands for server repositories

* cli: added KOPIA_CACHE_DIRECTORY environment variable

This is used on two occassions - when setting up connection (it gets
persisted in the config) and later when opening (to override the
cache location from config). It makes setting up docker container with
mounted cache somewhat easier with one environment variable.

* cli: show cache size for the server cache

* tls: present more helpful error message that includes SHA256 fingerprint of the TLS server on mismatch

* server: return the name of user who attempted to login when authentication fails
This commit is contained in:
Jarek Kowalski
2021-03-07 11:25:21 -08:00
committed by GitHub
parent b6e68fa28a
commit 1f1465f4ba
18 changed files with 288 additions and 181 deletions

View File

@@ -16,8 +16,13 @@
cacheClearCommandPartial = cacheClearCommand.Flag("partial", "Specifies the cache to clear").Enum("contents", "indexes", "metadata", "own-writes", "blob-list")
)
func runCacheClearCommand(ctx context.Context, rep repo.DirectRepository) error {
d := rep.CachingOptions().CacheDirectory
func runCacheClearCommand(ctx context.Context, rep repo.Repository) error {
opts, err := repo.GetCachingOptions(ctx, repositoryConfigFileName())
if err != nil {
return errors.Wrap(err, "error getting caching options")
}
d := opts.CacheDirectory
if d == "" {
return errors.New("caching not enabled")
}
@@ -52,5 +57,5 @@ func clearCacheDirectory(ctx context.Context, d string) error {
}
func init() {
cacheClearCommand.Action(directRepositoryReadAction(runCacheClearCommand))
cacheClearCommand.Action(repositoryReaderAction(runCacheClearCommand))
}

View File

@@ -17,20 +17,26 @@
cacheInfoPathOnly = cacheInfoCommand.Flag("path", "Only display cache path").Bool()
)
func runCacheInfoCommand(ctx context.Context, rep repo.DirectRepository) error {
func runCacheInfoCommand(ctx context.Context, rep repo.Repository) error {
opts, err := repo.GetCachingOptions(ctx, repositoryConfigFileName())
if err != nil {
return errors.Wrap(err, "error getting cache options")
}
if *cacheInfoPathOnly {
fmt.Println(rep.CachingOptions().CacheDirectory)
fmt.Println(opts.CacheDirectory)
return nil
}
entries, err := ioutil.ReadDir(rep.CachingOptions().CacheDirectory)
entries, err := ioutil.ReadDir(opts.CacheDirectory)
if err != nil {
return errors.Wrap(err, "unable to scan cache directory")
}
path2Limit := map[string]int64{
"contents": rep.CachingOptions().MaxCacheSizeBytes,
"metadata": rep.CachingOptions().MaxMetadataCacheSizeBytes,
"contents": opts.MaxCacheSizeBytes,
"metadata": opts.MaxMetadataCacheSizeBytes,
"server-contents": opts.MaxCacheSizeBytes,
}
for _, ent := range entries {
@@ -38,7 +44,7 @@ func runCacheInfoCommand(ctx context.Context, rep repo.DirectRepository) error {
continue
}
subdir := filepath.Join(rep.CachingOptions().CacheDirectory, ent.Name())
subdir := filepath.Join(opts.CacheDirectory, ent.Name())
fileCount, totalFileSize, err := scanCacheDir(subdir)
if err != nil {
@@ -51,7 +57,7 @@ func runCacheInfoCommand(ctx context.Context, rep repo.DirectRepository) error {
}
if ent.Name() == "blob-list" {
maybeLimit = fmt.Sprintf(" (duration %vs)", rep.CachingOptions().MaxListCacheDurationSec)
maybeLimit = fmt.Sprintf(" (duration %vs)", opts.MaxListCacheDurationSec)
}
fmt.Printf("%v: %v files %v%v\n", subdir, fileCount, units.BytesStringBase10(totalFileSize), maybeLimit)
@@ -64,5 +70,5 @@ func runCacheInfoCommand(ctx context.Context, rep repo.DirectRepository) error {
}
func init() {
cacheInfoCommand.Action(directRepositoryReadAction(runCacheInfoCommand))
cacheInfoCommand.Action(repositoryReaderAction(runCacheInfoCommand))
}

View File

@@ -18,8 +18,11 @@
cacheSetMaxListCacheDuration = cacheSetParamsCommand.Flag("max-list-cache-duration", "Duration of index cache").Default("-1ns").Duration()
)
func runCacheSetCommand(ctx context.Context, rep repo.DirectRepositoryWriter) error {
opts := rep.CachingOptions().CloneOrDefault()
func runCacheSetCommand(ctx context.Context, rep repo.RepositoryWriter) error {
opts, err := repo.GetCachingOptions(ctx, repositoryConfigFileName())
if err != nil {
return errors.Wrap(err, "error getting caching options")
}
changed := 0
@@ -53,9 +56,9 @@ func runCacheSetCommand(ctx context.Context, rep repo.DirectRepositoryWriter) er
return errors.Errorf("no changes")
}
return rep.SetCachingOptions(ctx, opts)
return repo.SetCachingOptions(ctx, repositoryConfigFileName(), opts)
}
func init() {
cacheSetParamsCommand.Action(directRepositoryWriteAction(runCacheSetCommand))
cacheSetParamsCommand.Action(repositoryWriterAction(runCacheSetCommand))
}

View File

@@ -31,7 +31,7 @@ func setupConnectOptions(cmd *kingpin.CmdClause) {
// Set up flags shared between 'create' and 'connect'. Note that because those flags are used by both command
// we must use *Var() methods, otherwise one of the commands would always get default flag values.
cmd.Flag("persist-credentials", "Persist credentials").Default("true").BoolVar(&connectPersistCredentials)
cmd.Flag("cache-directory", "Cache directory").PlaceHolder("PATH").StringVar(&connectCacheDirectory)
cmd.Flag("cache-directory", "Cache directory").PlaceHolder("PATH").Envar("KOPIA_CACHE_DIRECTORY").StringVar(&connectCacheDirectory)
cmd.Flag("content-cache-size-mb", "Size of local content cache").PlaceHolder("MB").Default("5000").Int64Var(&connectMaxCacheSizeMB)
cmd.Flag("metadata-cache-size-mb", "Size of local metadata cache").PlaceHolder("MB").Default("5000").Int64Var(&connectMaxMetadataCacheSizeMB)
cmd.Flag("max-list-cache-duration", "Duration of index cache").Default("30s").Hidden().DurationVar(&connectMaxListCacheDuration)

View File

@@ -2,7 +2,6 @@
import (
"context"
"os"
"github.com/alecthomas/kingpin"
"github.com/pkg/errors"
@@ -33,17 +32,10 @@ func connectToStorageFromConfig(ctx context.Context, isNew bool) (blob.Storage,
}
func connectToStorageFromConfigFile(ctx context.Context) (blob.Storage, error) {
var cfg repo.LocalConfig
f, err := os.Open(connectFromConfigFile) //nolint:gosec
cfg, err := repo.LoadConfigFromFile(connectFromConfigFile)
if err != nil {
return nil, errors.Wrap(err, "unable to open config")
}
defer f.Close() //nolint:errcheck,gosec
if err := cfg.Load(f); err != nil {
return nil, errors.Wrap(err, "unable to load config")
}
return blob.NewStorage(ctx, *cfg.Storage)
}

View File

@@ -141,10 +141,6 @@ export class RepoStatus extends Component {
<Form.Label>Config File</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.configFile} />
</Form.Group>
<Form.Group as={Col}>
<Form.Label>Cache Directory</Form.Label>
<Form.Control readOnly defaultValue={this.state.status.cacheDir} />
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col}>

View File

@@ -49,7 +49,6 @@ func (s *Server) handleRepoStatus(ctx context.Context, r *http.Request, body []b
return &serverapi.StatusResponse{
Connected: true,
ConfigFile: dr.ConfigFilename(),
CacheDir: dr.CachingOptions().CacheDirectory,
Hash: dr.ContentReader().ContentFormat().Hash,
Encryption: dr.ContentReader().ContentFormat().Encryption,
MaxPackSize: dr.ContentReader().ContentFormat().MaxPackSize,

View File

@@ -59,9 +59,11 @@ func (s *Server) authenticateGRPCSession(ctx context.Context) (string, error) {
if s.authenticator(ctx, s.rep, username, password) {
return username, nil
}
return "", status.Errorf(codes.PermissionDenied, "access denied for %v", username)
}
return "", status.Errorf(codes.PermissionDenied, "access denied")
return "", status.Errorf(codes.PermissionDenied, "missing credentials")
}
// Session handles GRPC session from a repository client.

View File

@@ -20,7 +20,6 @@
type StatusResponse struct {
Connected bool `json:"connected"`
ConfigFile string `json:"configFile,omitempty"`
CacheDir string `json:"cacheDir,omitempty"`
Hash string `json:"hash,omitempty"`
Encryption string `json:"encryption,omitempty"`
Splitter string `json:"splitter,omitempty"`

View File

@@ -136,13 +136,19 @@ func verifyPeerCertificate(sha256Fingerprint string) func(rawCerts [][]byte, ver
sha256Fingerprint = strings.ToLower(sha256Fingerprint)
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
var serverCerts []string
for _, c := range rawCerts {
h := sha256.Sum256(c)
if hex.EncodeToString(h[:]) == sha256Fingerprint {
serverCert := hex.EncodeToString(h[:])
if serverCert == sha256Fingerprint {
return nil
}
serverCerts = append(serverCerts, serverCert)
}
return errors.Errorf("can't find certificate matching SHA256 fingerprint %q", sha256Fingerprint)
return errors.Errorf("can't find certificate matching SHA256 fingerprint %q (server had %v)", sha256Fingerprint, serverCerts)
}
}

View File

@@ -5,10 +5,7 @@
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
@@ -40,7 +37,8 @@ type apiServerRepository struct {
omgr *object.Manager
wso WriteSessionOptions
contentCache *cache.PersistentCache
isSharedReadOnlySession bool
contentCache *cache.PersistentCache
}
func (r *apiServerRepository) APIServerURL() string {
@@ -148,6 +146,7 @@ func (r *apiServerRepository) NewWriter(ctx context.Context, opt WriteSessionOpt
w.omgr = omgr
w.wso = opt
w.isSharedReadOnlySession = false
return w, nil
}
@@ -195,6 +194,11 @@ func (r *apiServerRepository) WriteContent(ctx context.Context, data []byte, pre
return "", errors.Wrapf(err, "error writing content %v", contentID)
}
if prefix != "" {
// add all prefixed contents to the cache.
r.contentCache.Put(ctx, string(contentID), data)
}
return contentID, nil
}
@@ -204,8 +208,17 @@ func (r *apiServerRepository) UpdateDescription(d string) {
}
func (r *apiServerRepository) Close(ctx context.Context) error {
if err := r.omgr.Close(); err != nil {
return errors.Wrap(err, "error closing object manager")
if r.omgr != nil {
if err := r.omgr.Close(); err != nil {
return errors.Wrap(err, "error closing object manager")
}
r.omgr = nil
}
if r.isSharedReadOnlySession && r.contentCache != nil {
r.contentCache.Close(ctx)
r.contentCache = nil
}
return nil
@@ -233,6 +246,7 @@ func openRestAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts Clien
wso: WriteSessionOptions{
OnUpload: func(i int64) {},
},
isSharedReadOnlySession: true,
}
var p remoterepoapi.Parameters
@@ -265,19 +279,13 @@ func ConnectAPIServer(ctx context.Context, configFile string, si *APIServerInfo,
lc := LocalConfig{
APIServer: si,
ClientOptions: opt.ClientOptions.ApplyDefaults(ctx, "API Server: "+si.BaseURL),
Caching: opt.CachingOptions.CloneOrDefault(),
}
d, err := json.MarshalIndent(&lc, "", " ")
if err != nil {
return errors.Wrap(err, "unable to marshal config JSON")
if err := setupCachingOptionsWithDefaults(ctx, configFile, &lc, &opt.CachingOptions, []byte(si.BaseURL)); err != nil {
return errors.Wrap(err, "unable to set up caching")
}
if err = os.MkdirAll(filepath.Dir(configFile), 0o700); err != nil {
return errors.Wrap(err, "unable to create config directory")
}
if err = ioutil.WriteFile(configFile, d, 0o600); err != nil {
if err := lc.writeToFile(configFile); err != nil {
return errors.Wrap(err, "unable to write config file")
}

77
repo/caching.go Normal file
View File

@@ -0,0 +1,77 @@
package repo
import (
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/kopia/kopia/repo/content"
)
// GetCachingOptions reads caching configuration for a given repository.
func GetCachingOptions(ctx context.Context, configFile string) (*content.CachingOptions, error) {
lc, err := LoadConfigFromFile(configFile)
if err != nil {
return nil, err
}
return lc.Caching.CloneOrDefault(), nil
}
// SetCachingOptions changes caching configuration for a given repository.
func SetCachingOptions(ctx context.Context, configFile string, opt *content.CachingOptions) error {
lc, err := LoadConfigFromFile(configFile)
if err != nil {
return err
}
if err = setupCachingOptionsWithDefaults(ctx, configFile, lc, opt, nil); err != nil {
return errors.Wrap(err, "unable to set up caching")
}
return lc.writeToFile(configFile)
}
func setupCachingOptionsWithDefaults(ctx context.Context, configPath string, lc *LocalConfig, opt *content.CachingOptions, uniqueID []byte) error {
opt = opt.CloneOrDefault()
if opt.MaxCacheSizeBytes == 0 {
lc.Caching = &content.CachingOptions{}
return nil
}
if lc.Caching == nil {
lc.Caching = &content.CachingOptions{}
}
if opt.CacheDirectory == "" {
cacheDir, err := os.UserCacheDir()
if err != nil {
return errors.Wrap(err, "unable to determine cache directory")
}
h := sha256.New()
h.Write(uniqueID) //nolint:errcheck
h.Write([]byte(configPath)) //nolint:errcheck
lc.Caching.CacheDirectory = filepath.Join(cacheDir, "kopia", hex.EncodeToString(h.Sum(nil))[0:16])
} else {
d, err := filepath.Abs(opt.CacheDirectory)
if err != nil {
return errors.Wrap(err, "unable to determine absolute cache path")
}
lc.Caching.CacheDirectory = d
}
lc.Caching.MaxCacheSizeBytes = opt.MaxCacheSizeBytes
lc.Caching.MaxMetadataCacheSizeBytes = opt.MaxMetadataCacheSizeBytes
lc.Caching.MaxListCacheDurationSec = opt.MaxListCacheDurationSec
log(ctx).Debugf("Creating cache directory '%v' with max size %v", lc.Caching.CacheDirectory, lc.Caching.MaxCacheSizeBytes)
return nil
}

View File

@@ -2,10 +2,6 @@
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
@@ -54,20 +50,11 @@ func Connect(ctx context.Context, configFile string, st blob.Storage, password s
lc.Storage = &ci
lc.ClientOptions = opt.ClientOptions.ApplyDefaults(ctx, "Repository in "+st.DisplayName())
if err = setupCaching(ctx, configFile, &lc, &opt.CachingOptions, f.UniqueID); err != nil {
if err = setupCachingOptionsWithDefaults(ctx, configFile, &lc, &opt.CachingOptions, f.UniqueID); err != nil {
return errors.Wrap(err, "unable to set up caching")
}
d, err := json.MarshalIndent(&lc, "", " ")
if err != nil {
return errors.Wrap(err, "unable to serialize JSON")
}
if err = os.MkdirAll(filepath.Dir(configFile), 0o700); err != nil {
return errors.Wrap(err, "unable to create config directory")
}
if err = ioutil.WriteFile(configFile, d, 0o600); err != nil {
if err := lc.writeToFile(configFile); err != nil {
return errors.Wrap(err, "unable to write config file")
}
@@ -98,56 +85,9 @@ func verifyConnect(ctx context.Context, configFile, password string, persist boo
return r.Close(ctx)
}
func setupCaching(ctx context.Context, configPath string, lc *LocalConfig, opt *content.CachingOptions, uniqueID []byte) error {
opt = opt.CloneOrDefault()
if opt.MaxCacheSizeBytes == 0 {
lc.Caching = &content.CachingOptions{}
return nil
}
if lc.Caching == nil {
lc.Caching = &content.CachingOptions{}
}
if opt.CacheDirectory == "" {
cacheDir, err := os.UserCacheDir()
if err != nil {
return errors.Wrap(err, "unable to determine cache directory")
}
h := sha256.New()
h.Write(uniqueID) //nolint:errcheck
h.Write([]byte(configPath)) //nolint:errcheck
opt.CacheDirectory = filepath.Join(cacheDir, "kopia", hex.EncodeToString(h.Sum(nil))[0:16])
}
var err error
// try computing relative pathname from config dir to the cache dir.
lc.Caching.CacheDirectory, err = filepath.Rel(filepath.Dir(configPath), opt.CacheDirectory)
if err != nil {
// fall back to storing absolute path
lc.Caching.CacheDirectory, err = filepath.Abs(opt.CacheDirectory)
}
if err != nil {
return errors.Wrap(err, "error computing cache directory")
}
lc.Caching.MaxCacheSizeBytes = opt.MaxCacheSizeBytes
lc.Caching.MaxMetadataCacheSizeBytes = opt.MaxMetadataCacheSizeBytes
lc.Caching.MaxListCacheDurationSec = opt.MaxListCacheDurationSec
log(ctx).Debugf("Creating cache directory '%v' with max size %v", lc.Caching.CacheDirectory, lc.Caching.MaxCacheSizeBytes)
return nil
}
// Disconnect removes the specified configuration file and any local cache directories.
func Disconnect(ctx context.Context, configFile string) error {
cfg, err := loadConfigFromFile(configFile)
cfg, err := LoadConfigFromFile(configFile)
if err != nil {
return err
}
@@ -155,6 +95,10 @@ func Disconnect(ctx context.Context, configFile string) error {
deletePassword(ctx, configFile)
if cfg.Caching != nil && cfg.Caching.CacheDirectory != "" {
if !filepath.IsAbs(cfg.Caching.CacheDirectory) {
return errors.Errorf("cache directory was not absolute, refusing to delete")
}
if err = os.RemoveAll(cfg.Caching.CacheDirectory); err != nil {
log(ctx).Warningf("unable to remove cache directory: %v", err)
}
@@ -170,21 +114,12 @@ func Disconnect(ctx context.Context, configFile string) error {
// SetClientOptions updates client options stored in the provided configuration file.
func SetClientOptions(ctx context.Context, configFile string, cliOpt ClientOptions) error {
lc, err := loadConfigFromFile(configFile)
lc, err := LoadConfigFromFile(configFile)
if err != nil {
return err
}
lc.ClientOptions = cliOpt
d, err := json.MarshalIndent(lc, "", " ")
if err != nil {
return errors.Wrap(err, "error marshaling config JSON")
}
if err = ioutil.WriteFile(configFile, d, 0o600); err != nil {
return errors.Wrap(err, "unable to write config file")
}
return nil
return lc.writeToFile(configFile)
}

View File

@@ -481,7 +481,7 @@ func errorFromSessionResponse(rr *apipb.ErrorResponse) error {
case apipb.ErrorResponse_CONTENT_NOT_FOUND:
return content.ErrContentNotFound
case apipb.ErrorResponse_STREAM_BROKEN:
return io.EOF
return errors.Wrap(io.EOF, rr.Message)
default:
return errors.New(rr.Message)
}
@@ -552,6 +552,11 @@ func (r *grpcRepositoryClient) WriteContent(ctx context.Context, data []byte, pr
return "", err
}
if prefix != "" {
// add all prefixed contents to the cache.
r.contentCache.Put(ctx, string(contentID), data)
}
return v.(content.ID), nil
}
@@ -586,10 +591,17 @@ func (r *grpcRepositoryClient) UpdateDescription(d string) {
}
func (r *grpcRepositoryClient) Close(ctx context.Context) error {
if r.omgr == nil {
// already closed
return nil
}
if err := r.omgr.Close(); err != nil {
return errors.Wrap(err, "error closing object manager")
}
r.omgr = nil
if atomic.AddInt32(r.connRefCount, -1) == 0 {
log(ctx).Debugf("closing GPRC connection to %v", r.conn.Target())

View File

@@ -1,13 +1,15 @@
package repo
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/atomicfile"
"github.com/kopia/kopia/repo/blob"
"github.com/kopia/kopia/repo/content"
"github.com/kopia/kopia/repo/object"
@@ -88,26 +90,34 @@ type repositoryObjectFormat struct {
object.Format
}
// Load reads local configuration from the specified reader.
func (lc *LocalConfig) Load(r io.Reader) error {
*lc = LocalConfig{}
return json.NewDecoder(r).Decode(lc)
}
// writeToFile writes the config to a given file.
func (lc *LocalConfig) writeToFile(filename string) error {
lc2 := *lc
// Save writes the configuration to the specified writer.
func (lc *LocalConfig) Save(w io.Writer) error {
b, err := json.MarshalIndent(lc, "", " ")
if err != nil {
return nil
if lc.Caching != nil {
lc2.Caching = lc.Caching.CloneOrDefault()
// try computing relative pathname from config dir to the cache dir.
d, err := filepath.Rel(filepath.Dir(filename), lc.Caching.CacheDirectory)
if err == nil {
lc2.Caching.CacheDirectory = d
}
}
_, err = w.Write(b)
b, err := json.MarshalIndent(lc2, "", " ")
if err != nil {
return errors.Wrap(err, "error creating config file contents")
}
return errors.Wrap(err, "error saving local config")
if err = os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
return errors.Wrap(err, "unable to create config directory")
}
return errors.Wrap(atomicfile.Write(filename, bytes.NewReader(b)), "error writing file")
}
// loadConfigFromFile reads the local configuration from the specified file.
func loadConfigFromFile(fileName string) (*LocalConfig, error) {
// LoadConfigFromFile reads the local configuration from the specified file.
func LoadConfigFromFile(fileName string) (*LocalConfig, error) {
f, err := os.Open(fileName) //nolint:gosec
if err != nil {
return nil, errors.Wrap(err, "error loading config file")
@@ -116,8 +126,20 @@ func loadConfigFromFile(fileName string) (*LocalConfig, error) {
var lc LocalConfig
if err := lc.Load(f); err != nil {
return nil, err
if err := json.NewDecoder(f).Decode(&lc); err != nil {
return nil, errors.Wrap(err, "error decoding config json")
}
// cache directory is stored as relative to config file name, resolve it to absolute.
if lc.Caching != nil {
if lc.Caching.CacheDirectory != "" && !filepath.IsAbs(lc.Caching.CacheDirectory) {
lc.Caching.CacheDirectory = filepath.Join(filepath.Dir(fileName), lc.Caching.CacheDirectory)
}
// override cache directory from the environment variable.
if cd := os.Getenv("KOPIA_CACHE_DIRECTORY"); cd != "" && filepath.IsAbs(cd) {
lc.Caching.CacheDirectory = cd
}
}
return &lc, nil

84
repo/local_config_test.go Normal file
View File

@@ -0,0 +1,84 @@
package repo
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/testutil"
"github.com/kopia/kopia/repo/content"
)
func TestLocalConfig_withCaching(t *testing.T) {
td := testutil.TempDirectory(t)
originalLC := &LocalConfig{
Caching: &content.CachingOptions{
CacheDirectory: filepath.Join(td, "cache-dir"),
},
}
cfgFile := filepath.Join(td, "repository.config")
must(t, originalLC.writeToFile(cfgFile))
rawLC := LocalConfig{}
mustParseJSONFile(t, cfgFile, &rawLC)
loadedLC, err := LoadConfigFromFile(cfgFile)
must(t, err)
if filepath.IsAbs(rawLC.Caching.CacheDirectory) {
t.Fatalf("cache directory must be stored relative, was %v", rawLC.Caching.CacheDirectory)
}
if got, want := loadedLC.Caching.CacheDirectory, originalLC.Caching.CacheDirectory; got != want {
t.Fatalf("cache directory did not round trip: %v, want %v", got, want)
}
}
func TestLocalConfig_noCaching(t *testing.T) {
td := testutil.TempDirectory(t)
originalLC := &LocalConfig{}
cfgFile := filepath.Join(td, "repository.config")
must(t, originalLC.writeToFile(cfgFile))
rawLC := LocalConfig{}
mustParseJSONFile(t, cfgFile, &rawLC)
loadedLC, err := LoadConfigFromFile(cfgFile)
must(t, err)
if got, want := loadedLC.Caching, originalLC.Caching; got != want {
t.Fatalf("cacheing did not round trip: %v, want %v", got, want)
}
}
func TestLocalConfig_notFound(t *testing.T) {
if _, err := LoadConfigFromFile("nosuchfile.json"); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("unexpected error %v: wanted ErrNotExist", err)
}
}
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
func mustParseJSONFile(t *testing.T, fname string, o interface{}) {
t.Helper()
f, err := os.Open(fname)
must(t, err)
defer f.Close()
must(t, json.NewDecoder(f).Decode(o))
}

View File

@@ -3,7 +3,6 @@
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
@@ -72,18 +71,11 @@ func Open(ctx context.Context, configFile, password string, options *Options) (r
return nil, errors.Wrap(err, "error resolving config file path")
}
lc, err := loadConfigFromFile(configFile)
lc, err := LoadConfigFromFile(configFile)
if err != nil {
return nil, err
}
// cache directory is stored as relative to config file name, resolve it to absolute.
if lc.Caching != nil {
if lc.Caching.CacheDirectory != "" && !filepath.IsAbs(lc.Caching.CacheDirectory) {
lc.Caching.CacheDirectory = filepath.Join(filepath.Dir(configFile), lc.Caching.CacheDirectory)
}
}
if lc.APIServer != nil {
return OpenAPIServer(ctx, lc.APIServer, lc.ClientOptions, lc.Caching, password)
}
@@ -279,29 +271,6 @@ func writeCacheMarker(cacheDir string) error {
return f.Close()
}
// SetCachingOptions changes caching configuration for a given repository.
func (r *directRepository) SetCachingOptions(ctx context.Context, opt *content.CachingOptions) error {
lc, err := loadConfigFromFile(r.configFile)
if err != nil {
return err
}
if err = setupCaching(ctx, r.configFile, lc, opt, r.uniqueID); err != nil {
return errors.Wrap(err, "unable to set up caching")
}
d, err := json.MarshalIndent(&lc, "", " ")
if err != nil {
return errors.Wrap(err, "error marshaling JSON")
}
if err := ioutil.WriteFile(r.configFile, d, 0o600); err != nil {
return nil
}
return nil
}
func readAndCacheFormatBlobBytes(ctx context.Context, st blob.Storage, cacheDirectory string) ([]byte, error) {
cachedFile := filepath.Join(cacheDirectory, "kopia.repository")

View File

@@ -59,9 +59,6 @@ type DirectRepository interface {
ConfigFilename() string
DeriveKey(purpose []byte, keyLength int) []byte
Token(password string) (string, error)
CachingOptions() *content.CachingOptions
SetCachingOptions(ctx context.Context, opt *content.CachingOptions) error
}
// DirectRepositoryWriter provides low-level write access to the repository.
@@ -238,11 +235,6 @@ func (r *directRepository) Flush(ctx context.Context) error {
return r.cmgr.Flush(ctx)
}
// CachingOptions returns caching options.
func (r *directRepository) CachingOptions() *content.CachingOptions {
return r.cachingOptions.CloneOrDefault()
}
// ObjectFormat returns the object format.
func (r *directRepository) ObjectFormat() object.Format {
return r.omgr.Format