diff --git a/backend/generic.go b/backend/generic.go index 01b7f3f25..49ea0f561 100644 --- a/backend/generic.go +++ b/backend/generic.go @@ -4,12 +4,19 @@ import ( "bytes" "compress/zlib" "crypto/sha256" + "encoding/hex" + "errors" "io/ioutil" "sync" ) var idPool = sync.Pool{New: func() interface{} { return ID(make([]byte, IDSize)) }} +var ( + ErrNoIDPrefixFound = errors.New("no ID found") + ErrMultipleIDMatches = errors.New("multiple IDs with prefix found") +) + // Each lists all entries of type t in the backend and calls function f() with // the id and data. func Each(be Server, t Type, f func(id ID, data []byte, err error)) error { @@ -85,3 +92,54 @@ func Hash(data []byte) ID { copy(id, h[:]) return id } + +// Find loads the list of all blobs of type t and searches for IDs which start +// with prefix. If none is found, nil and ErrNoIDPrefixFound is returned. If +// more than one is found, nil and ErrMultipleIDMatches is returned. +func Find(be Server, t Type, prefix string) (ID, error) { + p, err := hex.DecodeString(prefix) + if err != nil { + return nil, err + } + + list, err := be.List(t) + if err != nil { + return nil, err + } + + match := ID(nil) + + // TODO: optimize by sorting list etc. + for _, id := range list { + if bytes.Equal(p, id[:len(p)]) { + if match == nil { + match = id + } else { + return nil, ErrMultipleIDMatches + } + } + } + + if match != nil { + return match, nil + } + + return nil, ErrNoIDPrefixFound +} + +// FindSnapshot takes a string and tries to find a snapshot whose ID matches +// the string as closely as possible. +func FindSnapshot(be Server, s string) (ID, error) { + // parse ID directly + if id, err := ParseID(s); err == nil { + return id, nil + } + + // find snapshot id with prefix + id, err := Find(be, Snapshot, s) + if err != nil { + return nil, err + } + + return id, nil +} diff --git a/cmd/khepri/cmd_cat.go b/cmd/khepri/cmd_cat.go index 19d925900..47befd91a 100644 --- a/cmd/khepri/cmd_cat.go +++ b/cmd/khepri/cmd_cat.go @@ -19,7 +19,17 @@ func commandCat(be backend.Server, key *khepri.Key, args []string) error { id, err := backend.ParseID(args[1]) if err != nil { - return err + id = nil + + if tpe != "snapshot" { + return err + } + + // find snapshot id with prefix + id, err = backend.Find(be, backend.Snapshot, args[1]) + if err != nil { + return err + } } ch, err := khepri.NewContentHandler(be, key) @@ -105,7 +115,8 @@ func commandCat(be backend.Server, key *khepri.Key, args []string) error { return nil case "snapshot": var sn khepri.Snapshot - err := ch.LoadJSONRaw(backend.Snapshot, id, &sn) + + err = ch.LoadJSONRaw(backend.Snapshot, id, &sn) if err != nil { return err } diff --git a/cmd/khepri/cmd_ls.go b/cmd/khepri/cmd_ls.go index 53ee53ded..5ab98ace7 100644 --- a/cmd/khepri/cmd_ls.go +++ b/cmd/khepri/cmd_ls.go @@ -53,7 +53,7 @@ func commandLs(be backend.Server, key *khepri.Key, args []string) error { return errors.New("usage: ls SNAPSHOT_ID [dir]") } - id, err := backend.ParseID(args[0]) + id, err := backend.FindSnapshot(be, args[0]) if err != nil { return err } diff --git a/cmd/khepri/cmd_restore.go b/cmd/khepri/cmd_restore.go index 4672b2cce..de747c8e2 100644 --- a/cmd/khepri/cmd_restore.go +++ b/cmd/khepri/cmd_restore.go @@ -14,7 +14,7 @@ func commandRestore(be backend.Server, key *khepri.Key, args []string) error { return errors.New("usage: restore ID dir") } - id, err := backend.ParseID(args[0]) + id, err := backend.FindSnapshot(be, args[0]) if err != nil { errx(1, "invalid id %q: %v", args[0], err) } diff --git a/cmd/khepri/cmd_snapshots.go b/cmd/khepri/cmd_snapshots.go index 84af0dbe9..43a5dffe5 100644 --- a/cmd/khepri/cmd_snapshots.go +++ b/cmd/khepri/cmd_snapshots.go @@ -3,34 +3,79 @@ package main import ( "errors" "fmt" + "os" + "sort" + "strings" + "time" "github.com/fd0/khepri" "github.com/fd0/khepri/backend" ) -const TimeFormat = "02.01.2006 15:04:05 -0700" +const ( + minute = 60 + hour = 60 * minute + day = 24 * hour + week = 7 * day +) + +const TimeFormat = "2006-01-02 15:04:05" + +func reltime(t time.Time) string { + sec := uint64(time.Since(t).Seconds()) + + switch { + case sec > week: + return t.Format(TimeFormat) + case sec > day: + return fmt.Sprintf("%d days ago", sec/day) + case sec > hour: + return fmt.Sprintf("%d hours ago", sec/hour) + case sec > minute: + return fmt.Sprintf("%d minutes ago", sec/minute) + default: + return fmt.Sprintf("%d seconds ago", sec) + } +} func commandSnapshots(be backend.Server, key *khepri.Key, args []string) error { if len(args) != 0 { return errors.New("usage: snapshots") } - // ch, err := khepri.NewContentHandler(be, key) - // if err != nil { - // return err - // } + ch, err := khepri.NewContentHandler(be, key) + if err != nil { + return err + } + + fmt.Printf("%-8s %-19s %-10s %s\n", "ID", "Date", "Source", "Directory") + fmt.Printf("%s\n", strings.Repeat("-", 80)) + + list := []*khepri.Snapshot{} backend.EachID(be, backend.Snapshot, func(id backend.ID) { - // sn, err := ch.LoadSnapshot(id) - // if err != nil { - // fmt.Fprintf(os.Stderr, "error loading snapshot %s: %v\n", id, err) - // return - // } + sn, err := ch.LoadSnapshot(id) + if err != nil { + fmt.Fprintf(os.Stderr, "error loading snapshot %s: %v\n", id, err) + return + } - // fmt.Printf("snapshot %s\n %s at %s by %s\n", - // id, sn.Dir, sn.Time, sn.Username) - fmt.Println(id) + pos := sort.Search(len(list), func(i int) bool { + return list[i].Time.After(sn.Time) + }) + + if pos < len(list) { + list = append(list, nil) + copy(list[pos+1:], list[pos:]) + list[pos] = sn + } else { + list = append(list, sn) + } }) + for _, sn := range list { + fmt.Printf("%-8s %-19s %-10s %s\n", sn.ID().String()[:8], sn.Time.Format(TimeFormat), sn.Hostname, sn.Dir) + } + return nil } diff --git a/snapshot.go b/snapshot.go index a92dd19ff..db6f81f7d 100644 --- a/snapshot.go +++ b/snapshot.go @@ -51,7 +51,7 @@ func NewSnapshot(dir string) *Snapshot { } func LoadSnapshot(ch *ContentHandler, id backend.ID) (*Snapshot, error) { - sn := &Snapshot{} + sn := &Snapshot{id: id} err := ch.LoadJSON(backend.Snapshot, id, sn) if err != nil { return nil, err @@ -60,6 +60,10 @@ func LoadSnapshot(ch *ContentHandler, id backend.ID) (*Snapshot, error) { return sn, nil } -func (sn *Snapshot) String() string { +func (sn Snapshot) String() string { return fmt.Sprintf("", sn.Dir, sn.Time) } + +func (sn Snapshot) ID() backend.ID { + return sn.id +}