Files
vaultik/internal/vaultik/destination_marker.go
T
clawbot c07db21b45
check / check (push) Successful in 9m13s
Bind the local index only after a backup writes the marker (closes #420)
A first backup that failed before writing anything, on a missing bucket, a refused credential or missing recipients, still bound the local index to the configured destination, so correcting a mistyped storage_url was refused afterwards. A backup now records the binding only after it has written the marker file at the destination. prune, snapshot purge, snapshot remove and remote nuke only refuse a different binding and leave an unbound index unbound.

Model: opus-5-5
2026-11-11 04:49:11 +01:00

112 lines
3.8 KiB
Go

package vaultik
import (
"errors"
"fmt"
"strings"
"sneak.berlin/go/vaultik/internal/log"
"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 that answers a look-up of it with not
// found 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 answers
// that it 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 answers that it 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.
//
// A look-up of the marker that fails for any other reason than not
// found is logged and otherwise ignored, because the check could not be
// made: S3 refuses it to credentials that allow only PUT, which is all
// a backup needs. The check exists for a file:// volume's empty mount
// point, which always answers not found, and a destination that cannot
// be reached fails on the next request made to it, such as the backup's
// PUT of the marker.
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
// checkStorageBinding).
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)
}
// Info, not a warning: --cron must stay silent on a successful
// backup from a host whose credentials allow only PUT.
if err != nil {
log.Info("Could not check the backup destination store for its marker",
"key", destinationMarkerKey, "error", err)
}
return nil
}
// markDestination refuses a local index bound to a different
// destination, checks the backup destination store for its marker,
// writes the marker, and then records the binding. A first backup marks
// its destination and a later one writes the same text again. The
// binding is recorded last, so a first backup that cannot write to the
// destination leaves the local index unbound.
func (v *Vaultik) markDestination() error {
err := v.checkStorageBinding()
if err != nil {
return err
}
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 v.EnsureStorageBinding()
}