Compare commits
2
Commits
f3d4b559b5
...
0c468504dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c468504dc | ||
|
|
b30e79ee45 |
@@ -577,22 +577,16 @@ complete annotated example also lives in
|
||||
|
||||
## roadmap
|
||||
|
||||
Items still to do before / shortly after 1.0. Loosely ordered by
|
||||
priority.
|
||||
Work planned after 1.0. Loosely ordered by priority.
|
||||
|
||||
### correctness and operability
|
||||
|
||||
* **Security audit of the encryption implementation.** Pre-1.0
|
||||
blocker if we're advertising "secure" at the top of this README.
|
||||
age + zstd + content-defined chunking is mostly off-the-shelf
|
||||
pieces, but the seams (key handling, recipient parsing, manifest
|
||||
trust boundary, restore-time identity validation) need an outside
|
||||
read.
|
||||
* **Error-condition tests.** Today's coverage is the happy path
|
||||
plus a few specific regressions. Need fault-injection coverage:
|
||||
network failures mid-blob, disk-full during restore, corrupted /
|
||||
truncated / missing blobs, partial uploads, kill -9 between
|
||||
manifest and db.zst.age writes.
|
||||
* **Outside security audit.** Before 1.0 the encryption and
|
||||
blob-generation code was reviewed and every finding fixed; no
|
||||
outside audit has been done. age + zstd + content-defined chunking
|
||||
is mostly off-the-shelf pieces, but the seams (key handling,
|
||||
recipient parsing, manifest trust boundary, restore-time identity
|
||||
validation) need an outside read.
|
||||
* **Verify restored content end-to-end in CI.** The current
|
||||
integration test does this for a small synthetic snapshot but
|
||||
not at scale. A nightly job against a multi-GB representative
|
||||
@@ -615,13 +609,15 @@ priority.
|
||||
doesn't resume from where it stopped or skip already-present
|
||||
files. A `--resume` mode that checks targets before fetching
|
||||
blobs would matter for very large restores.
|
||||
* **Daemon mode.** A long-running mode that watches for file
|
||||
changes so frequent backups, such as hourly, skip the full scan.
|
||||
It adds little for the usual runs from cron every 12 to 36 hours.
|
||||
See [issue #204](https://git.eeqj.de/sneak/vaultik/issues/204).
|
||||
|
||||
### usability
|
||||
|
||||
* **Man pages and richer `--help` examples.** Cobra generates
|
||||
basic help; man pages would be a separate target.
|
||||
* **`--bwlimit` style human-readable size flags** across the
|
||||
command surface where they're currently raw integers.
|
||||
* **`vaultik snapshot diff <a> <b>`** — show which files changed
|
||||
between two snapshots without restoring either.
|
||||
* **Status reporting hook for `--cron`.** When a backup fails
|
||||
|
||||
@@ -14,14 +14,11 @@ pre-1.0
|
||||
|
||||
# Next Step
|
||||
|
||||
Define the remaining scope for the first tagged release under the 1.0.0
|
||||
milestone, then cut that tag. The mechanism to cut it now exists and is
|
||||
exercised; what is left is the scope decision, which is the owner's.
|
||||
This step deliberately names one version number: it previously said
|
||||
"cut v0.1.0" while the `Makefile` baked in `1.0.0-rc.1` and the issue
|
||||
milestone said 1.0.0, and three different answers to "what is the next
|
||||
release" is exactly the contradiction
|
||||
[issue #65](https://git.eeqj.de/sneak/vaultik/issues/65) was filed over.
|
||||
The 1.0 work is complete on `next`: the scope settled on
|
||||
[issue #125](https://git.eeqj.de/sneak/vaultik/issues/125) was the work
|
||||
already planned for 1.0, and all of it has landed. The mechanism to cut
|
||||
the tag exists and is exercised; what is left is merging `next` to
|
||||
`main` and tagging, both the owner's.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
@@ -693,4 +690,5 @@ release" is exactly the contradiction
|
||||
|
||||
# Future Steps
|
||||
|
||||
None queued; the release-scoping item is now the Next Step.
|
||||
Work planned after 1.0 is listed in the README
|
||||
[roadmap](README.md#roadmap).
|
||||
|
||||
@@ -680,18 +680,10 @@ func faultScannerFactory(
|
||||
|
||||
// Scenario 5: the restore target runs out of space mid-file. Restore
|
||||
// must fail with an out-of-space error, and must not leave a truncated
|
||||
// file at the target path presenting as a complete restore. Restore
|
||||
// today writes each file straight to its final path and does not remove
|
||||
// it when a write fails, so the truncated file survives; deleting it is
|
||||
// tracked by https://git.eeqj.de/sneak/vaultik/issues/163. Skipped until
|
||||
// that lands, so the destination assertion below is recorded rather than
|
||||
// dropped.
|
||||
// file at the target path presenting as a complete restore.
|
||||
//
|
||||
//nolint:paralleltest // installs the global logger via log.Initialize
|
||||
func TestRestoreReportsDiskFull(t *testing.T) {
|
||||
t.Skip("blocked on https://git.eeqj.de/sneak/vaultik/issues/163: " +
|
||||
"a disk-full write leaves a truncated file at the target path " +
|
||||
"instead of removing it")
|
||||
log.Initialize(log.Config{})
|
||||
|
||||
osFS := afero.NewOsFs()
|
||||
@@ -717,10 +709,10 @@ func TestRestoreReportsDiskFull(t *testing.T) {
|
||||
id := fullFaultBackup(ctx, t, osFS, inner, cfg, repos, dataDir, dbPath, "diskfull")
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
// Restore onto a filesystem that allows only a few bytes of file
|
||||
// content: enough to create files, far too little to hold them.
|
||||
// Restore onto a target that allows only a few bytes of file content:
|
||||
// enough to create files, far too little to hold them.
|
||||
budget := int64(8)
|
||||
quota := "aFS{Fs: osFS, remaining: &budget}
|
||||
quota := "aFS{Fs: osFS, dir: restoreDir, remaining: &budget}
|
||||
|
||||
v := newReaderVaultik(ctx, cfg, inner, nil, quota)
|
||||
err = v.Restore(&vaultik.RestoreOptions{SnapshotID: id, TargetDir: restoreDir})
|
||||
@@ -754,21 +746,26 @@ func assertRestoredTree(
|
||||
// budget is exhausted, mirroring a real ENOSPC.
|
||||
var errNoSpace = errors.New("no space left on device")
|
||||
|
||||
// quotaFS is an afero.Fs whose files may write only a fixed total number
|
||||
// of content bytes before failing, simulating a full restore target. It
|
||||
// wraps the interface so every method except Create delegates to the
|
||||
// real filesystem; only file writes are capped.
|
||||
// quotaFS is an afero.Fs on which files opened under dir may write only
|
||||
// a fixed total number of content bytes before failing, simulating a full
|
||||
// restore target. Every other method, and every file outside dir, goes
|
||||
// straight to the real filesystem: restore also writes the decrypted
|
||||
// metadata database under $TMPDIR through this filesystem, and capping
|
||||
// that would fail the restore before it wrote anything to the target.
|
||||
type quotaFS struct {
|
||||
afero.Fs
|
||||
|
||||
dir string
|
||||
remaining *int64
|
||||
}
|
||||
|
||||
//nolint:ireturn // afero.Fs.Create's signature requires returning afero.File.
|
||||
func (q *quotaFS) Create(name string) (afero.File, error) {
|
||||
f, err := q.Fs.Create(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
//nolint:ireturn // afero.Fs.OpenFile's signature requires returning afero.File.
|
||||
func (q *quotaFS) OpenFile(
|
||||
name string, flag int, perm os.FileMode,
|
||||
) (afero.File, error) {
|
||||
f, err := q.Fs.OpenFile(name, flag, perm)
|
||||
if err != nil || !strings.HasPrefix(name, q.dir) {
|
||||
return f, err
|
||||
}
|
||||
|
||||
return "aFile{File: f, remaining: q.remaining}, nil
|
||||
|
||||
Reference in New Issue
Block a user