Refuse a relative index_path at config load (closes #430)
check / check (push) Successful in 11m12s
check / check (push) Successful in 11m12s
A relative index_path resolved against the working directory, so a backup run from another directory started from an empty index. Validate now refuses it, naming the value, with the same wording as the file:// storage_url check. ~ is still expanded first. The check sees the final value, so a relative VAULTIK_INDEX_PATH is refused too; an explicitly empty index_path is refused as well. Model: opus-5-5
This commit is contained in:
1 parent
d8d13d1c8b
commit
75a9ef0889
4 files changed
+73
-1
No files matched your search
@@ -295,6 +295,7 @@ storage_url: "rclone://myremote/path/to/backups"
|
|||||||
|
|
||||||
# Path to local SQLite index database
|
# Path to local SQLite index database
|
||||||
# This database tracks file state for incremental backups
|
# This database tracks file state for incremental backups
|
||||||
|
# It must be an absolute path; ~ is expanded
|
||||||
# Default: the platform data directory, e.g.
|
# Default: the platform data directory, e.g.
|
||||||
# macOS: ~/Library/Application Support/vaultik/index.sqlite
|
# macOS: ~/Library/Application Support/vaultik/index.sqlite
|
||||||
# Linux: ~/.local/share/vaultik/index.sqlite
|
# Linux: ~/.local/share/vaultik/index.sqlite
|
||||||
|
|||||||
@@ -276,7 +276,8 @@ storage_url: ""
|
|||||||
# Hostname used in snapshot IDs. Default: system hostname.
|
# Hostname used in snapshot IDs. Default: system hostname.
|
||||||
# hostname: myserver
|
# hostname: myserver
|
||||||
|
|
||||||
# Path to the local SQLite index database.
|
# Path to the local SQLite index database. It must be an absolute path;
|
||||||
|
# ~ is expanded.
|
||||||
# Default: the platform data directory, e.g.
|
# Default: the platform data directory, e.g.
|
||||||
# macOS: ~/Library/Application Support/vaultik/index.sqlite
|
# macOS: ~/Library/Application Support/vaultik/index.sqlite
|
||||||
# Linux: ~/.local/share/vaultik/index.sqlite
|
# Linux: ~/.local/share/vaultik/index.sqlite
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ var (
|
|||||||
errFileURLNotAbsolute = errors.New(
|
errFileURLNotAbsolute = errors.New(
|
||||||
"a file:// storage_url needs an absolute path, " +
|
"a file:// storage_url needs an absolute path, " +
|
||||||
"as in file:///path/to/backup")
|
"as in file:///path/to/backup")
|
||||||
|
errIndexPathNotAbsolute = errors.New(
|
||||||
|
"index_path and VAULTIK_INDEX_PATH need an absolute path, " +
|
||||||
|
"as in ~/.local/share/vaultik/index.sqlite")
|
||||||
errNegativePruneGracePeriod = errors.New(
|
errNegativePruneGracePeriod = errors.New(
|
||||||
"prune_grace_period must not be negative")
|
"prune_grace_period must not be negative")
|
||||||
errStorageNotConfigured = errors.New(
|
errStorageNotConfigured = errors.New(
|
||||||
@@ -387,6 +390,8 @@ func Parse(data []byte) (*Config, error) {
|
|||||||
// create needs a recipient and it checks for one itself
|
// create needs a recipient and it checks for one itself
|
||||||
// - At least one snapshot must be configured with at least one path
|
// - At least one snapshot must be configured with at least one path
|
||||||
// - Storage must be configured (either storage_url or s3.* fields)
|
// - Storage must be configured (either storage_url or s3.* fields)
|
||||||
|
// - The index path must be absolute: a relative one would resolve against
|
||||||
|
// the working directory, so each directory would get its own index
|
||||||
// - Chunk size must be at least 1MB
|
// - Chunk size must be at least 1MB
|
||||||
// - Blob size limit must be at least the largest chunk the chunker can emit
|
// - Blob size limit must be at least the largest chunk the chunker can emit
|
||||||
// (chunk_size times chunker.ChunkSizeSpread), so a single-chunk blob never
|
// (chunk_size times chunker.ChunkSizeSpread), so a single-chunk blob never
|
||||||
@@ -420,6 +425,10 @@ func (c *Config) Validate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !filepath.IsAbs(c.IndexPath) {
|
||||||
|
return fmt.Errorf("%w: %q", errIndexPathNotAbsolute, c.IndexPath)
|
||||||
|
}
|
||||||
|
|
||||||
if c.ChunkSize.Int64() < minChunkSize {
|
if c.ChunkSize.Int64() < minChunkSize {
|
||||||
return errChunkSizeTooSmall
|
return errChunkSizeTooSmall
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ func TestValidateBlobSizeLimit(t *testing.T) {
|
|||||||
AgeRecipients: []string{testSneakAgePublicKey},
|
AgeRecipients: []string{testSneakAgePublicKey},
|
||||||
Snapshots: map[string]SnapshotConfig{"test": {Paths: []string{"/tmp/src"}}},
|
Snapshots: map[string]SnapshotConfig{"test": {Paths: []string{"/tmp/src"}}},
|
||||||
StorageURL: "file:///tmp/vaultik-test-store",
|
StorageURL: "file:///tmp/vaultik-test-store",
|
||||||
|
IndexPath: "/tmp/vaultik-test-index.sqlite",
|
||||||
ChunkSize: chunkSize,
|
ChunkSize: chunkSize,
|
||||||
BlobSizeLimit: blobLimit,
|
BlobSizeLimit: blobLimit,
|
||||||
CompressionLevel: 3,
|
CompressionLevel: 3,
|
||||||
@@ -510,6 +511,65 @@ func TestLoadRejectsRelativeFileURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLoadIndexPath checks that a relative index_path fails at load, naming
|
||||||
|
// the value. Accepted, it would resolve against the working directory, so a
|
||||||
|
// backup run from another directory would start from an empty index. A path
|
||||||
|
// starting with ~ is expanded and loads.
|
||||||
|
func TestLoadIndexPath(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("home directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
load := func(t *testing.T, indexPath string) (*Config, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
data := "snapshots:\n" +
|
||||||
|
" test:\n" +
|
||||||
|
" paths: [/tmp/vaultik-test-source]\n" +
|
||||||
|
"storage_url: file:///tmp/vaultik-test-storage\n" +
|
||||||
|
"index_path: " + indexPath + "\n"
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "config.yml")
|
||||||
|
|
||||||
|
err := os.WriteFile(path, []byte(data), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Load(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("relative path is rejected", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := load(t, "relative/index.sqlite")
|
||||||
|
if !errors.Is(err, errIndexPathNotAbsolute) {
|
||||||
|
t.Fatalf("Load() error = %v, want errIndexPathNotAbsolute", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), `"relative/index.sqlite"`) {
|
||||||
|
t.Errorf("error %q does not name the value", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("path starting with ~ is expanded", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cfg, err := load(t, "~/vaultik/index.sqlite")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := filepath.Join(home, "vaultik", "index.sqlite")
|
||||||
|
if cfg.IndexPath != want {
|
||||||
|
t.Errorf("IndexPath = %q, want %q", cfg.IndexPath, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestValidateAgeRecipients checks that recipients are parsed at config load
|
// TestValidateAgeRecipients checks that recipients are parsed at config load
|
||||||
// (a bad entry fails immediately, not mid-backup) and that no invalid entry —
|
// (a bad entry fails immediately, not mid-backup) and that no invalid entry —
|
||||||
// least of all a pasted secret key — is echoed in the error. An empty list
|
// least of all a pasted secret key — is echoed in the error. An empty list
|
||||||
@@ -522,6 +582,7 @@ func TestValidateAgeRecipients(t *testing.T) {
|
|||||||
AgeRecipients: recipients,
|
AgeRecipients: recipients,
|
||||||
Snapshots: map[string]SnapshotConfig{"test": {Paths: []string{"/tmp/src"}}},
|
Snapshots: map[string]SnapshotConfig{"test": {Paths: []string{"/tmp/src"}}},
|
||||||
StorageURL: "file:///tmp/vaultik-test-store",
|
StorageURL: "file:///tmp/vaultik-test-store",
|
||||||
|
IndexPath: "/tmp/vaultik-test-index.sqlite",
|
||||||
ChunkSize: Size(10 * 1024 * 1024),
|
ChunkSize: Size(10 * 1024 * 1024),
|
||||||
BlobSizeLimit: Size(10 * 1024 * 1024 * 1024),
|
BlobSizeLimit: Size(10 * 1024 * 1024 * 1024),
|
||||||
CompressionLevel: 3,
|
CompressionLevel: 3,
|
||||||
|
|||||||
Reference in new issue
Block a user