check / check (push) Successful in 9m32s
A snapshot ID that names no snapshot in the local index or on the destination store made restore and verify fail on a missing object at a hashed path, and verify printed a header for it first. Resolving a snapshot ID now uses the look-up `snapshot remove` already does, so all three commands give the same error, and verify prints its header only after the snapshot is found. 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 findSnapshot).
|
|
// - 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.findSnapshot(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
|
|
}
|