check / check (push) Successful in 13m30s
snapshot restore and snapshot verify given an ID that names no snapshot in the local index or at the destination failed with "object not found" on a hashed metadata path. They now look the ID up the way snapshot remove does and say that no such snapshot exists, before printing anything else. Model: opus-5-5
94 lines
2.8 KiB
Go
94 lines
2.8 KiB
Go
package vaultik
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
)
|
|
|
|
// remoteKeyHexLen is the length of a full remote snapshot key: a SHA256
|
|
// digest rendered as lowercase hex.
|
|
const remoteKeyHexLen = 64
|
|
|
|
// Sentinel errors for resolving a snapshot identifier against the store.
|
|
var (
|
|
errSnapshotKeyNotFound = errors.New(
|
|
"no snapshot on the destination store matches this identifier")
|
|
errSnapshotKeyAmbiguous = errors.New(
|
|
"identifier matches more than one snapshot on the destination store")
|
|
)
|
|
|
|
// resolveSnapshotRemoteKey turns a snapshot identifier supplied on the
|
|
// command line into the remote key that names the snapshot's metadata
|
|
// directory on the destination store. Every remote path a restore or
|
|
// verify reads is built from that key.
|
|
//
|
|
// Two forms are accepted, matching the two things a host can know:
|
|
//
|
|
// - A human snapshot ID (hostname_name_timestamp), which a host holding
|
|
// the local index has. It is hashed to its remote key. An ID that
|
|
// names no snapshot in the local index or on the destination store
|
|
// is an error (see checkSnapshotExists).
|
|
// - A remote key, or the leading part of one, which is all a host with
|
|
// no local index can know — it is exactly what `snapshot list` prints
|
|
// for a remote-only snapshot (see formatRemoteOnlyID). It is resolved
|
|
// against the destination store's metadata listing; an identifier that
|
|
// matches no snapshot, or more than one, is an error.
|
|
//
|
|
// The two are told apart by shape: a remote key is lowercase hex, and a
|
|
// human snapshot ID never is (it carries a hostname, underscores, and an
|
|
// RFC3339 timestamp).
|
|
func (v *Vaultik) resolveSnapshotRemoteKey(identifier string) (string, error) {
|
|
if !isRemoteKeyOrPrefix(identifier) {
|
|
_, err := v.checkSnapshotExists(identifier, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return snapshot.RemoteSnapshotKey(identifier), nil
|
|
}
|
|
|
|
keys, err := v.listAllRemoteSnapshotKeys()
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"listing destination store to resolve %q: %w", identifier, err)
|
|
}
|
|
|
|
var matches []string
|
|
|
|
for _, key := range keys {
|
|
if strings.HasPrefix(key, identifier) {
|
|
matches = append(matches, key)
|
|
}
|
|
}
|
|
|
|
switch len(matches) {
|
|
case 1:
|
|
return matches[0], nil
|
|
case 0:
|
|
return "", fmt.Errorf("%w: %s", errSnapshotKeyNotFound, identifier)
|
|
default:
|
|
return "", fmt.Errorf("%w: %s (%d matches)",
|
|
errSnapshotKeyAmbiguous, identifier, len(matches))
|
|
}
|
|
}
|
|
|
|
// isRemoteKeyOrPrefix reports whether s is a full remote key or the
|
|
// leading part of one: 1 to 64 lowercase hex characters. A human snapshot
|
|
// ID is never all hex, so this shape test is enough to tell the two apart.
|
|
func isRemoteKeyOrPrefix(s string) bool {
|
|
if s == "" || len(s) > remoteKeyHexLen {
|
|
return false
|
|
}
|
|
|
|
for _, r := range s {
|
|
if (r < '0' || r > '9') && (r < 'a' || r > 'f') {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|