check / check (push) Successful in 9m13s
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
112 lines
3.8 KiB
Go
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()
|
|
}
|