diff --git a/backend/generic.go b/backend/generic.go index 49ea0f561..6e09729b3 100644 --- a/backend/generic.go +++ b/backend/generic.go @@ -7,9 +7,14 @@ import ( "encoding/hex" "errors" "io/ioutil" + "sort" "sync" ) +const ( + MinPrefixLength = 4 +) + var idPool = sync.Pool{New: func() interface{} { return ID(make([]byte, IDSize)) }} var ( @@ -143,3 +148,32 @@ func FindSnapshot(be Server, s string) (ID, error) { return id, nil } + +// PrefixLength returns the number of bytes required so that all prefixes of +// all IDs of type t are unique. +func PrefixLength(be Lister, t Type) (int, error) { + // load all IDs of the given type + list, err := be.List(t) + if err != nil { + return 0, err + } + + sort.Sort(list) + + // select prefixes of length l, test if the last one is the same as the current one +outer: + for l := MinPrefixLength; l < IDSize; l++ { + var last ID + + for _, id := range list { + if bytes.Equal(last, id[:l]) { + continue outer + } + last = id[:l] + } + + return l, nil + } + + return IDSize, nil +} diff --git a/backend/generic_test.go b/backend/generic_test.go index 5b8b968ca..818ac67a2 100644 --- a/backend/generic_test.go +++ b/backend/generic_test.go @@ -6,6 +6,8 @@ import ( "reflect" "runtime" "testing" + + "github.com/fd0/khepri/backend" ) // assert fails the test if the condition is false. @@ -34,3 +36,43 @@ func equals(tb testing.TB, exp, act interface{}) { tb.FailNow() } } + +func str2id(s string) backend.ID { + id, err := backend.ParseID(s) + if err != nil { + panic(err) + } + + return id +} + +type IDList backend.IDs + +var samples = IDList{ + str2id("20bdc1402a6fc9b633aaffffffffffffffffffffffffffffffffffffffffffff"), + str2id("20bdc1402a6fc9b633ccd578c4a92d0f4ef1a457fa2e16c596bc73fb409d6cc0"), + str2id("20bdc1402a6fc9b633ffffffffffffffffffffffffffffffffffffffffffffff"), + str2id("20ff988befa5fc40350f00d531a767606efefe242c837aaccb80673f286be53d"), + str2id("326cb59dfe802304f96ee9b5b9af93bdee73a30f53981e5ec579aedb6f1d0f07"), + str2id("86b60b9594d1d429c4aa98fa9562082cabf53b98c7dc083abe5dae31074dd15a"), + str2id("96c8dbe225079e624b5ce509f5bd817d1453cd0a85d30d536d01b64a8669aeae"), + str2id("fa31d65b87affcd167b119e9d3d2a27b8236ca4836cb077ed3e96fcbe209b792"), +} + +func (l IDList) List(backend.Type) (backend.IDs, error) { + return backend.IDs(l), nil +} + +func TestPrefixLength(t *testing.T) { + l, err := backend.PrefixLength(samples, backend.Snapshot) + ok(t, err) + equals(t, 10, l) + + l, err = backend.PrefixLength(samples[:3], backend.Snapshot) + ok(t, err) + equals(t, 10, l) + + l, err = backend.PrefixLength(samples[3:], backend.Snapshot) + ok(t, err) + equals(t, 4, l) +} diff --git a/backend/interface.go b/backend/interface.go index bfaabacb6..ec1abb3a3 100644 --- a/backend/interface.go +++ b/backend/interface.go @@ -21,10 +21,14 @@ var ( ErrAlreadyPresent = errors.New("blob is already present in backend") ) +type Lister interface { + List(Type) (IDs, error) +} + type Server interface { Create(Type, []byte) (ID, error) Get(Type, ID) ([]byte, error) - List(Type) (IDs, error) + Lister Test(Type, ID) (bool, error) Remove(Type, ID) error Version() uint diff --git a/cmd/khepri/cmd_snapshots.go b/cmd/khepri/cmd_snapshots.go index 43a5dffe5..e91ea25b0 100644 --- a/cmd/khepri/cmd_snapshots.go +++ b/cmd/khepri/cmd_snapshots.go @@ -52,6 +52,10 @@ func commandSnapshots(be backend.Server, key *khepri.Key, args []string) error { fmt.Printf("%s\n", strings.Repeat("-", 80)) list := []*khepri.Snapshot{} + plen, err := backend.PrefixLength(be, backend.Snapshot) + if err != nil { + return err + } backend.EachID(be, backend.Snapshot, func(id backend.ID) { sn, err := ch.LoadSnapshot(id) @@ -74,7 +78,7 @@ func commandSnapshots(be backend.Server, key *khepri.Key, args []string) error { }) for _, sn := range list { - fmt.Printf("%-8s %-19s %-10s %s\n", sn.ID().String()[:8], sn.Time.Format(TimeFormat), sn.Hostname, sn.Dir) + fmt.Printf("%-8s %-19s %-10s %s\n", sn.ID()[:plen], sn.Time.Format(TimeFormat), sn.Hostname, sn.Dir) } return nil