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 // 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) } // 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 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 }