check / check (push) Canceled after 0s
Every backup writes vaultik-destination.txt at the destination root. While the local index holds snapshots stored there, a destination without it is not present: snapshot create refuses before writing, prune and snapshot purge fail, and snapshot list and snapshot remove act as for a destination that cannot be listed. snapshot create and snapshot remove check it first. remote nuke deletes the marker last. The file:// backend never creates its destination directory, which protects a directory on a removable volume, not a mount point. Tests back up into directories that exist; fixtures for a written destination carry the marker. Judgement call: the marker is rewritten on every backup. Deviation: for the complexity limit, two blocks moved, unchanged, into snapshotNamesToCreate and confirmRemoveSnapshot. Model: opus-5-5
91 lines
2.8 KiB
Go
91 lines
2.8 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
|
|
// stored there, 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 stored on the backup destination store and the store has no
|
|
// marker.
|
|
var errDestinationNotPresent = errors.New("backup destination store is not present")
|
|
|
|
// checkDestinationMarker returns errDestinationNotPresent when the local
|
|
// index holds a snapshot stored on the backup destination store and the
|
|
// 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; nor
|
|
// when the local index is bound to a different destination than the
|
|
// configured one, since its snapshots are stored there.
|
|
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
|
|
}
|
|
|
|
// Config is nil only in tests, which then record no binding (see
|
|
// EnsureStorageBinding).
|
|
if v.Config != nil {
|
|
stored, configured, err := v.storageBinding()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if stored != "" && stored != configured {
|
|
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
|
|
}
|