From bf81a0853be9affc6a7750ee919a390280fb201b Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 13 Oct 2026 20:48:40 +0000 Subject: [PATCH] Read the rclone config file named by RCLONE_CONFIG (closes #321) An rclone destination whose remote is defined only in the file RCLONE_CONFIG names was reported as not found: rclone reads that variable only as the value of its --config flag, which vaultik does not have, so vaultik always used rclone's default config file. NewRcloneStorer now sets rclone's config path from RCLONE_CONFIG when the variable is set, as that flag would, including an empty value, which rclone takes to mean no config file. Model: opus-5-5 --- internal/storage/rclone.go | 11 ++++++++++ internal/storage/rclone_test.go | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/internal/storage/rclone.go b/internal/storage/rclone.go index 43ac7ba..4ca2be1 100644 --- a/internal/storage/rclone.go +++ b/internal/storage/rclone.go @@ -7,10 +7,12 @@ import ( "errors" "fmt" "io" + "os" "strings" "time" "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config/configfile" "github.com/rclone/rclone/fs/operations" @@ -33,6 +35,15 @@ type RcloneStorer struct { // The remote parameter is the rclone remote name (as configured via `rclone config`). // The path parameter is the path within the remote. func NewRcloneStorer(ctx context.Context, remote, path string) (*RcloneStorer, error) { + // rclone reads RCLONE_CONFIG only as the value of its --config flag, + // which vaultik does not have, so it is applied here as that flag would. + if configPath, ok := os.LookupEnv("RCLONE_CONFIG"); ok { + err := config.SetConfigPath(configPath) + if err != nil { + return nil, fmt.Errorf("using RCLONE_CONFIG %q: %w", configPath, err) + } + } + // Install the default config file handler configfile.Install() diff --git a/internal/storage/rclone_test.go b/internal/storage/rclone_test.go index c666a64..1f2d9ee 100644 --- a/internal/storage/rclone_test.go +++ b/internal/storage/rclone_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/fs/config/configmap" "sneak.berlin/go/vaultik/internal/storage" ) @@ -390,3 +391,41 @@ func TestNewRcloneStorerUnknownRemote(t *testing.T) { t.Errorf("NewRcloneStorer error = %v, want ErrRemoteNotFound", err) } } + +// TestNewRcloneStorerReadsRcloneConfigEnv checks that a remote defined only +// in the file RCLONE_CONFIG names is found and written to, as rclone itself +// would. +func TestNewRcloneStorerReadsRcloneConfigEnv(t *testing.T) { + // rclone keeps the config file path in a process-global variable, which + // NewRcloneStorer leaves pointing at this test's file. + previous := config.GetConfigPath() + + t.Cleanup(func() { _ = config.SetConfigPath(previous) }) + + confFile := filepath.Join(t.TempDir(), "rclone.conf") + + err := os.WriteFile(confFile, []byte("[loc]\ntype = local\n"), 0o600) + if err != nil { + t.Fatalf("writing rclone config: %v", err) + } + + t.Setenv("RCLONE_CONFIG", confFile) + + dir := t.TempDir() + ctx := context.Background() + + s, err := storage.NewRcloneStorer(ctx, "loc", dir) + if err != nil { + t.Fatalf("NewRcloneStorer: %v", err) + } + + err = s.Put(ctx, testBlobKey, strings.NewReader("blob-bytes")) + if err != nil { + t.Fatalf("Put: %v", err) + } + + _, err = os.Stat(filepath.Join(dir, testBlobKey)) + if err != nil { + t.Errorf("object is not in the remote's directory: %v", err) + } +}