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 }