check / check (push) Waiting to run
Every backup now writes vaultik-destination.txt at the root of the backup destination store. While the local index holds snapshots, a destination without it is not present: snapshot create refuses before writing, prune and snapshot purge fail, and snapshot list and snapshot remove warn, as for a destination that cannot be listed. remote nuke deletes the marker last. The file:// backend no longer creates its destination directory and checks for it on every write. Tests now back up into existing directories, and fixtures standing for a written destination carry the marker. Judgement call: the marker is rewritten on every backup. Deviation: for the complexity limit, snapshot name selection left CreateSnapshot and the --force/--json test moved into confirmRemoveSnapshot, both unchanged. Model: opus-5-5
75 lines
2.4 KiB
Go
75 lines
2.4 KiB
Go
package vaultik
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
)
|
|
|
|
// destinationMarkerKey is the object every backup writes at the root of
|
|
// the backup destination store. While the local index holds a snapshot,
|
|
// a destination without it is treated as not present: an unplugged
|
|
// volume leaves an empty directory at its mount point, which would
|
|
// otherwise read as a store whose snapshots are all gone.
|
|
const destinationMarkerKey = "vaultik-destination.txt"
|
|
|
|
// destinationMarkerText is the whole content of the marker. It names no
|
|
// host, path or time.
|
|
const destinationMarkerText = "This is a vaultik backup destination store.\n"
|
|
|
|
// errDestinationNotPresent is returned when the local index holds a
|
|
// snapshot and the backup destination store has no marker.
|
|
var errDestinationNotPresent = errors.New("backup destination store is not present")
|
|
|
|
// checkDestinationMarker returns errDestinationNotPresent when the local
|
|
// index holds a snapshot and the backup destination store has no marker.
|
|
// With no snapshot in the local index, as on a first backup or on
|
|
// another machine, there is nothing to check.
|
|
func (v *Vaultik) checkDestinationMarker() error {
|
|
ids, err := v.localSnapshotIDs()
|
|
if err != nil {
|
|
return fmt.Errorf("listing local snapshots: %w", err)
|
|
}
|
|
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
|
|
_, err = v.Storage.Stat(v.ctx, destinationMarkerKey)
|
|
if errors.Is(err, storage.ErrNotFound) {
|
|
return fmt.Errorf("%w: %s has no %s, but the local index holds "+
|
|
"snapshots stored there; mount the volume if it is removable, "+
|
|
"or if the destination was emptied on purpose, run "+
|
|
"'vaultik database delete' and back up again",
|
|
errDestinationNotPresent, v.Storage.Info().Location,
|
|
destinationMarkerKey)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("checking for %s: %w", destinationMarkerKey, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// markDestination checks the backup destination store with
|
|
// checkDestinationMarker and then writes the marker, so a first backup
|
|
// marks its destination and a later one writes the same text again.
|
|
func (v *Vaultik) markDestination() error {
|
|
err := v.checkDestinationMarker()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = v.Storage.Put(v.ctx, destinationMarkerKey,
|
|
strings.NewReader(destinationMarkerText))
|
|
if err != nil {
|
|
return fmt.Errorf("writing %s to the backup destination store: %w",
|
|
destinationMarkerKey, err)
|
|
}
|
|
|
|
return nil
|
|
}
|