From 66c525aa7b9d12658a1168a0a6b9a74daf505bff Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Mon, 12 Oct 2020 19:51:44 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Implement=20basic=20share=20hand?= =?UTF-8?q?ling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- store.go | 162 +++++++++++++++++++++++++++++++++++++++++++++++--- store_test.go | 97 ++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 8 deletions(-) diff --git a/store.go b/store.go index 19661a3..b1016b1 100644 --- a/store.go +++ b/store.go @@ -45,9 +45,22 @@ type UserStore interface { } type ShareStore interface { + CreateShare() (Share, error) + UpdateShareAttributes(share Share) error + RemoveShare(id uuid.UUID) error + AddUserToShare(share Share, username string, role ShareRole) error + RemoveUserFromShare(share Share, username string) error + AddLogin(share Share, username string, login Login) error + RemoveLogin(share Share, username string, loginName string) error + + GetShares() ([]Share, error) + + FindByLogin(username, loginName string) (LoginShare, error) + FindByUser(username string) ([]UserShare, error) } -var _ UserStore = &DBStore{} +var _ UserStore = (*DBStore)(nil) +var _ ShareStore = (*DBStore)(nil) type GlobalRole string @@ -72,30 +85,47 @@ type User struct { } type Share struct { - UUID uuid.UUID - Directory string - Users []ShareUser + UUID uuid.UUID + Name string + Description string } type ShareUser struct { Username string Role ShareRole - Logins []Login } type Login struct { - Loginname string + LoginName string Password string } -const usersPrefix = "users:" +// View on a share from the perspective of a user of that share. +type UserShare struct { + Share + Role ShareRole +} + +// View on a share from the perspective of a specific login (with associated user). +type LoginShare struct { + Share + ShareUser + Login +} + +const userPrefix = "user:" +const sharePrefix = "share:" type DBStore struct { db *buntdb.DB } func (u User) key() string { - return usersPrefix + u.Username + return userPrefix + u.Username +} + +func (s Share) key() string { + return sharePrefix + s.UUID.String() } func NewDBStore(filename string) (*DBStore, error) { @@ -135,6 +165,8 @@ func (u User) merge(updates User) (User, error) { return merged, nil } +var ErrShareNotFound = errors.New("share not found") + func (store *DBStore) AddUser(user User) (err error) { if strings.Contains(user.Username, ":") { return ErrInvalidUsername @@ -260,3 +292,117 @@ func hashPassword(password string) (string, error) { } return string(hash), nil } + +func (store *DBStore) CreateShare() (Share, error) { + share := Share{ + UUID: uuid.NewV4(), + } + + if err := store.db.Update(func(tx *buntdb.Tx) error { + b, err := json.Marshal(share) + if err != nil { + return fmt.Errorf("cannot marshal share: %w", err) + } + if _, _, err := tx.Set(share.key(), string(b), nil); err != nil { + return err + } + return nil + }); err != nil { + return Share{}, err + } + + return share, nil +} + +func (store *DBStore) UpdateShareAttributes(share Share) error { + return store.db.Update(func(tx *buntdb.Tx) error { + var existingShare Share + if val, err := tx.Get(share.key()); err == buntdb.ErrNotFound { + return ErrShareNotFound + } else if err != nil { + return err + } else if err := json.Unmarshal([]byte(val), &existingShare); err != nil { + return fmt.Errorf("cannot unmarshal share %s: %w", share.UUID, err) + } + + existingShare.UUID = share.UUID + existingShare.Name = share.Name + existingShare.Description = share.Description + + if b, err := json.Marshal(existingShare); err != nil { + return fmt.Errorf("cannot marshal share %s: %w", share.UUID, err) + } else if _, _, err := tx.Set(existingShare.key(), string(b), nil); err != nil { + return err + } + + return nil + }) +} + +func (store *DBStore) RemoveShare(id uuid.UUID) error { + return store.db.Update(func(tx *buntdb.Tx) error { + share := Share{UUID: id} + if _, err := tx.Delete(share.key()); err == buntdb.ErrNotFound { + return ErrShareNotFound + } else { + return err + } + }) +} + +func (store *DBStore) AddUserToShare(share Share, username string, role ShareRole) error { + panic("implement me") +} + +func (store *DBStore) RemoveUserFromShare(share Share, username string) error { + panic("implement me") +} + +func (store *DBStore) AddLogin(share Share, username string, login Login) error { + panic("implement me") +} + +func (store *DBStore) RemoveLogin(share Share, username string, loginName string) error { + panic("implement me") +} + +func (store *DBStore) GetShares() (shares []Share, err error) { + err = store.db.View(func(tx *buntdb.Tx) error { + var processingError error + + if err := tx.AscendKeys(sharePrefix+"*", func(key, value string) bool { + var share Share + + if err := json.Unmarshal([]byte(value), &share); err != nil { + processingError = err + return false + } + + // Just in case ... + idString := strings.TrimPrefix(key, sharePrefix) + if id, err := uuid.FromString(idString); err != nil { + processingError = fmt.Errorf("invalid uuid in db: %q: %w", idString, err) + return false + } else { + share.UUID = id + } + + shares = append(shares, share) + return true + }); err != nil { + return err + } + + return processingError + }) + + return shares, err +} + +func (store *DBStore) FindByLogin(username, loginName string) (LoginShare, error) { + panic("implement me") +} + +func (store *DBStore) FindByUser(username string) ([]UserShare, error) { + panic("implement me") +} diff --git a/store_test.go b/store_test.go index b33b2af..a32b861 100644 --- a/store_test.go +++ b/store_test.go @@ -3,6 +3,7 @@ package main import ( "testing" + uuid "github.com/satori/go.uuid" "github.com/tidwall/buntdb" ) @@ -101,3 +102,99 @@ func TestStoreUserHandling(t *testing.T) { } }) } + +func TestStoreShareHandling(t *testing.T) { + store, err := NewDBStore(":memory:") + if err != nil { + t.Fatalf("cannot create store: %v", err) + } + defer store.Close() + + t.Run("store should be empty initially", func(t *testing.T) { + shares, err := store.GetShares() + if err != nil { + t.Errorf("no error should have been returned: %v", err) + } + if len(shares) != 0 { + t.Errorf("there should be no shares") + } + }) + + t.Run("creating a share should work", func(t *testing.T) { + share, err := store.CreateShare() + if err != nil { + t.Errorf("error creating share: %v", err) + return + } + var emptyUUID uuid.UUID + if share.UUID == emptyUUID { + t.Errorf("UUID is empty") + return + } + if share.Name != "" || share.Description != "" { + t.Errorf("share should not have attributes set (yet)") + } + + t.Run("share attributes can be set", func(t *testing.T) { + share.Name = "a name" + share.Description = "some desc" + if err := store.UpdateShareAttributes(share); err != nil { + t.Errorf("cannot set attributes of share: %v", err) + } + }) + + t.Run("cannot set attributes of unknown share", func(t *testing.T) { + otherShare := Share{UUID: uuid.NewV4()} + otherShare.Name = "foo" + otherShare.Description = "bar" + + err := store.UpdateShareAttributes(otherShare) + if err == nil { + t.Errorf("an error should have been returned") + } else if err != ErrShareNotFound { + t.Errorf("wrong error has been returned: %v", err) + } + }) + + t.Run("share can be listed", func(t *testing.T) { + shares, err := store.GetShares() + if err != nil { + t.Errorf("error getting shares: %v", err) + return + } + + if len(shares) != 1 { + t.Errorf("invalid number of shares: %d", len(shares)) + return + } + + if shares[0].UUID != share.UUID { + t.Errorf("unexpected uuid") + } + if shares[0].Name != "a name" { + t.Errorf("unexpected name") + } + if shares[0].Description != "some desc" { + t.Errorf("unexpected description") + } + }) + + t.Run("share can be removed", func(t *testing.T) { + if err := store.RemoveShare(share.UUID); err != nil { + t.Errorf("removing share failed: %v", err) + } + }) + }) + + t.Run("database should be empty now", func(t *testing.T) { + // checks that we properly deleted all keys + if err := store.db.View(func(tx *buntdb.Tx) error { + return tx.Ascend("", func(key, value string) bool { + t.Errorf("there should be no keys left") + return false + }) + }); err != nil { + t.Errorf("iterating keys failed: %v", err) + } + }) +}