🚧 Implement basic share handling
This commit is contained in:
		
							parent
							
								
									e6a62bfeb1
								
							
						
					
					
						commit
						66c525aa7b
					
				
							
								
								
									
										160
									
								
								store.go
									
									
									
									
									
								
							
							
						
						
									
										160
									
								
								store.go
									
									
									
									
									
								
							| @ -45,9 +45,22 @@ type UserStore interface { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ShareStore 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 | type GlobalRole string | ||||||
| 
 | 
 | ||||||
| @ -73,29 +86,46 @@ type User struct { | |||||||
| 
 | 
 | ||||||
| type Share struct { | type Share struct { | ||||||
| 	UUID        uuid.UUID | 	UUID        uuid.UUID | ||||||
| 	Directory string | 	Name        string | ||||||
| 	Users     []ShareUser | 	Description string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ShareUser struct { | type ShareUser struct { | ||||||
| 	Username string | 	Username string | ||||||
| 	Role     ShareRole | 	Role     ShareRole | ||||||
| 	Logins   []Login |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type Login struct { | type Login struct { | ||||||
| 	Loginname string | 	LoginName string | ||||||
| 	Password  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 { | type DBStore struct { | ||||||
| 	db *buntdb.DB | 	db *buntdb.DB | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (u User) key() string { | 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) { | func NewDBStore(filename string) (*DBStore, error) { | ||||||
| @ -135,6 +165,8 @@ func (u User) merge(updates User) (User, error) { | |||||||
| 	return merged, nil | 	return merged, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | var ErrShareNotFound = errors.New("share not found") | ||||||
|  | 
 | ||||||
| func (store *DBStore) AddUser(user User) (err error) { | func (store *DBStore) AddUser(user User) (err error) { | ||||||
| 	if strings.Contains(user.Username, ":") { | 	if strings.Contains(user.Username, ":") { | ||||||
| 		return ErrInvalidUsername | 		return ErrInvalidUsername | ||||||
| @ -260,3 +292,117 @@ func hashPassword(password string) (string, error) { | |||||||
| 	} | 	} | ||||||
| 	return string(hash), nil | 	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") | ||||||
|  | } | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ package main | |||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
|  | 	uuid "github.com/satori/go.uuid" | ||||||
| 	"github.com/tidwall/buntdb" | 	"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) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user