✨ Add initial share commandline handling
This commit is contained in:
parent
f39d8c3e48
commit
b48b7f58e1
|
@ -0,0 +1,123 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
uuid "github.com/satori/go.uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CmdShare struct {
|
||||||
|
CmdList CmdShareList `cmd:"" name:"list" help:"List all shares."`
|
||||||
|
CmdCreate CmdShareCreate `cmd:"" name:"create" help:"Create a new share."`
|
||||||
|
CmdDelete CmdShareDelete `cmd:"" name:"delete" help:"Delete a share."`
|
||||||
|
CmdAddUser CmdShareAddUser `cmd:"" name:"add-user" help:"Add user to share."`
|
||||||
|
CmdAddLogin CmdShareAddLogin `cmd:"" name:"add-login" help:"Add login to share."`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CmdShareList struct{}
|
||||||
|
|
||||||
|
func (cmd *CmdShareList) Run(app *app) error {
|
||||||
|
shares, err := app.shareStore.GetShares()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, share := range shares {
|
||||||
|
fmt.Printf("* %s (%s)\n", share.UUID.String(), share.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CmdShareCreate struct {
|
||||||
|
Name string `name:"name" help:"Name of the share." required:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *CmdShareCreate) Run(app *app) error {
|
||||||
|
share, err := app.shareStore.CreateShare()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
share.Name = cmd.Name
|
||||||
|
if err := app.shareStore.UpdateShareAttributes(share); err != nil {
|
||||||
|
// Best effort cleanup.
|
||||||
|
_ = app.shareStore.RemoveShare(share.UUID)
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot set share attributes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(path.Join(app.DataDirectory, share.UUID.String()), 0750); err != nil {
|
||||||
|
// Best effort cleanup.
|
||||||
|
_ = app.shareStore.RemoveShare(share.UUID)
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot create data dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Share created: %s\n", share.UUID.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShareIdentifier struct {
|
||||||
|
UUID uuid.UUID `arg:"" name:"id" help:"ID of the share to be deleted." required:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CmdShareDelete struct {
|
||||||
|
ShareIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *CmdShareDelete) Run(app *app) error {
|
||||||
|
if err := os.RemoveAll(path.Join(app.DataDirectory, cmd.UUID.String())); err != nil {
|
||||||
|
return fmt.Errorf("cannot remove data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.shareStore.RemoveShare(cmd.UUID); err != nil {
|
||||||
|
return fmt.Errorf("cannot remove share: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CmdShareAddUser struct {
|
||||||
|
ShareIdentifier
|
||||||
|
Username string `arg:"" name:"username" help:"Username of the user to add."`
|
||||||
|
Role ShareRole `name:"role" help:"Role of the user (reader/writer/admin)" default:"writer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *CmdShareAddUser) Run(app *app) error {
|
||||||
|
if _, err := app.userStore.GetUser(cmd.Username); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
share := Share{UUID: cmd.UUID}
|
||||||
|
return app.shareStore.AddUserToShare(share, cmd.Username, cmd.Role)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CmdShareAddLogin struct {
|
||||||
|
ShareIdentifier
|
||||||
|
Username string `arg:"" name:"username" help:"Username of the user to add the login for."`
|
||||||
|
LoginName string `arg:"" name:"loginname" help:"Name f the login. Must be unique."`
|
||||||
|
ReadOnly bool `name:"readonly" help:"If set, the login can only read."`
|
||||||
|
PasswordParam
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *CmdShareAddLogin) Run(app *app) error {
|
||||||
|
if _, err := app.userStore.GetUser(cmd.Username); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := cmd.acquirePassword()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot acquire password for login: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
share := Share{UUID: cmd.UUID}
|
||||||
|
login := Login{
|
||||||
|
LoginName: cmd.LoginName,
|
||||||
|
Password: password,
|
||||||
|
ReadOnly: cmd.ReadOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.shareStore.AddLogin(share, cmd.Username, login)
|
||||||
|
}
|
1
main.go
1
main.go
|
@ -37,6 +37,7 @@ import (
|
||||||
type app struct {
|
type app struct {
|
||||||
config
|
config
|
||||||
CmdUser CmdUser `cmd:"" name:"user" help:"Manage users."`
|
CmdUser CmdUser `cmd:"" name:"user" help:"Manage users."`
|
||||||
|
CmdShare CmdShare `cmd:"" name:"share" help:"Manage shares."`
|
||||||
|
|
||||||
userStore UserStore
|
userStore UserStore
|
||||||
shareStore ShareStore
|
shareStore ShareStore
|
||||||
|
|
85
store.go
85
store.go
|
@ -96,6 +96,7 @@ type ShareUser struct {
|
||||||
type Login struct {
|
type Login struct {
|
||||||
LoginName string
|
LoginName string
|
||||||
Password string
|
Password string
|
||||||
|
ReadOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// View on a share from the perspective of a user of that share.
|
// View on a share from the perspective of a user of that share.
|
||||||
|
@ -114,6 +115,8 @@ type LoginShare struct {
|
||||||
const userPrefix = "user:"
|
const userPrefix = "user:"
|
||||||
const sharePrefix = "share:"
|
const sharePrefix = "share:"
|
||||||
const loginSharePrefix = "loginshare:"
|
const loginSharePrefix = "loginshare:"
|
||||||
|
const shareuserPrefix = "shareuser:"
|
||||||
|
const shareloginPrefix = "sharelogin:"
|
||||||
|
|
||||||
type DBStore struct {
|
type DBStore struct {
|
||||||
db *buntdb.DB
|
db *buntdb.DB
|
||||||
|
@ -127,6 +130,14 @@ func (s Share) key() string {
|
||||||
return sharePrefix + s.UUID.String()
|
return sharePrefix + s.UUID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Share) userKey(username string) string {
|
||||||
|
return fmt.Sprintf("%s%s:%s", shareuserPrefix, s.UUID.String(), username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Share) loginKey(username, loginName string) string {
|
||||||
|
return fmt.Sprintf("%s%s:%s:%s", shareloginPrefix, s.UUID.String(), username, loginName)
|
||||||
|
}
|
||||||
|
|
||||||
func NewDBStore(filename string) (*DBStore, error) {
|
func NewDBStore(filename string) (*DBStore, error) {
|
||||||
db, err := buntdb.Open(filename)
|
db, err := buntdb.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -333,7 +344,7 @@ func (store *DBStore) RemoveShare(id uuid.UUID) error {
|
||||||
share := Share{UUID: id}
|
share := Share{UUID: id}
|
||||||
|
|
||||||
var userNames []string
|
var userNames []string
|
||||||
usersPrefix := share.key() + ":user:"
|
usersPrefix := share.userKey("")
|
||||||
if err := tx.AscendKeys(usersPrefix+"*", func(key, value string) bool {
|
if err := tx.AscendKeys(usersPrefix+"*", func(key, value string) bool {
|
||||||
userNames = append(userNames, strings.TrimPrefix(key, usersPrefix))
|
userNames = append(userNames, strings.TrimPrefix(key, usersPrefix))
|
||||||
return true
|
return true
|
||||||
|
@ -343,7 +354,7 @@ func (store *DBStore) RemoveShare(id uuid.UUID) error {
|
||||||
|
|
||||||
for _, username := range userNames {
|
for _, username := range userNames {
|
||||||
if err := store.removeUserFromShare(tx, share, username); err != nil {
|
if err := store.removeUserFromShare(tx, share, username); err != nil {
|
||||||
return fmt.Errorf("cannot remove user %q from share: %v", username, err)
|
return fmt.Errorf("cannot remove user %q from share: %w", username, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,8 +374,8 @@ func (store *DBStore) AddUserToShare(share Share, username string, role ShareRol
|
||||||
if _, err := tx.Get(share.key()); err != nil {
|
if _, err := tx.Get(share.key()); err != nil {
|
||||||
return ErrShareNotFound
|
return ErrShareNotFound
|
||||||
}
|
}
|
||||||
if _, _, err := tx.Set(share.key()+":user:"+username, string(role), nil); err != nil {
|
if _, _, err := tx.Set(share.userKey(username), string(role), nil); err != nil {
|
||||||
return fmt.Errorf("cannot set user: %v", err)
|
return fmt.Errorf("cannot set user: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -372,7 +383,7 @@ func (store *DBStore) AddUserToShare(share Share, username string, role ShareRol
|
||||||
|
|
||||||
func (store *DBStore) removeUserFromShare(tx *buntdb.Tx, share Share, username string) error {
|
func (store *DBStore) removeUserFromShare(tx *buntdb.Tx, share Share, username string) error {
|
||||||
var logins []string
|
var logins []string
|
||||||
loginsPrefix := share.key() + ":login:" + username + ":"
|
loginsPrefix := share.loginKey(username, "")
|
||||||
if err := tx.AscendKeys(loginsPrefix+"*", func(key, value string) bool {
|
if err := tx.AscendKeys(loginsPrefix+"*", func(key, value string) bool {
|
||||||
logins = append(logins, strings.TrimPrefix(key, loginsPrefix))
|
logins = append(logins, strings.TrimPrefix(key, loginsPrefix))
|
||||||
return true
|
return true
|
||||||
|
@ -383,18 +394,18 @@ func (store *DBStore) removeUserFromShare(tx *buntdb.Tx, share Share, username s
|
||||||
for _, loginName := range logins {
|
for _, loginName := range logins {
|
||||||
shareIdString, err := tx.Delete(loginSharePrefix + username + ":" + loginName)
|
shareIdString, err := tx.Delete(loginSharePrefix + username + ":" + loginName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot remove login ref: %v", err)
|
return fmt.Errorf("cannot remove login ref: %w", err)
|
||||||
} else if shareIdString != share.UUID.String() {
|
} else if shareIdString != share.UUID.String() {
|
||||||
return fmt.Errorf("inconsistent login ref %q", username+":"+loginName)
|
return fmt.Errorf("inconsistent login ref %q", username+":"+loginName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tx.Delete(share.key() + ":login:" + username + ":" + loginName); err != nil {
|
if _, err := tx.Delete(share.loginKey(username, loginName)); err != nil {
|
||||||
return fmt.Errorf("cannot remove login %q: %v", loginName, err)
|
return fmt.Errorf("cannot remove login %q: %w", loginName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tx.Delete(share.key() + ":user:" + username); err != nil {
|
if _, err := tx.Delete(share.userKey(username)); err != nil {
|
||||||
return fmt.Errorf("cannot remove user: %v", err)
|
return fmt.Errorf("cannot remove user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -415,7 +426,7 @@ func (store *DBStore) AddLogin(share Share, username string, login Login) error
|
||||||
if _, err := tx.Get(share.key()); err != nil {
|
if _, err := tx.Get(share.key()); err != nil {
|
||||||
return ErrShareNotFound
|
return ErrShareNotFound
|
||||||
}
|
}
|
||||||
if _, err := tx.Get(share.key() + ":user:" + username); err != nil {
|
if _, err := tx.Get(share.userKey(username)); err != nil {
|
||||||
return ErrUserNotFound
|
return ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,14 +437,18 @@ func (store *DBStore) AddLogin(share Share, username string, login Login) error
|
||||||
// If the existing value happens to be our shareId already, we assume we are just
|
// If the existing value happens to be our shareId already, we assume we are just
|
||||||
// updating the existing model. No harm there.
|
// updating the existing model. No harm there.
|
||||||
if existingValue, exists, err := tx.Set(loginSharePrefix+username+":"+login.LoginName, share.UUID.String(), nil); err != nil {
|
if existingValue, exists, err := tx.Set(loginSharePrefix+username+":"+login.LoginName, share.UUID.String(), nil); err != nil {
|
||||||
return fmt.Errorf("cannot set login reference: %v", err)
|
return fmt.Errorf("cannot set login reference: %w", err)
|
||||||
} else if exists && existingValue != share.UUID.String() {
|
} else if exists && existingValue != share.UUID.String() {
|
||||||
return ErrLoginDuplicate
|
return ErrLoginDuplicate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now simply update the current login information.
|
// Now simply update the current login information.
|
||||||
if _, _, err := tx.Set(share.key()+":login:"+username+":"+login.LoginName, login.Password, nil); err != nil {
|
b, err := json.Marshal(login)
|
||||||
return fmt.Errorf("cannot set login: %v", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot marshal login: %w", err)
|
||||||
|
}
|
||||||
|
if _, _, err := tx.Set(share.loginKey(username, login.LoginName), string(b), nil); err != nil {
|
||||||
|
return fmt.Errorf("cannot set login: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -451,7 +466,7 @@ func (store *DBStore) RemoveLogin(share Share, username string, loginName string
|
||||||
return ErrLoginNotFound
|
return ErrLoginNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tx.Delete(share.key() + ":login:" + username + ":" + loginName); err != nil {
|
if _, err := tx.Delete(share.loginKey(username, loginName)); err != nil {
|
||||||
return ErrLoginNotFound
|
return ErrLoginNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,37 +514,35 @@ func (store *DBStore) FindShareByLogin(username, loginName string) (loginShare L
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
userRole, err := tx.Get(sharePrefix + shareIdString + ":user:" + username)
|
|
||||||
if err == buntdb.ErrNotFound {
|
|
||||||
// TODO warn about inconsistency
|
|
||||||
return ErrShareNotFound
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
loginPassword, err := tx.Get(sharePrefix + shareIdString + ":login:" + username + ":" + loginName)
|
|
||||||
if err == buntdb.ErrNotFound {
|
|
||||||
// TODO warn about inconsistency
|
|
||||||
return ErrShareNotFound
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
share, err := unmarshalShare(shareIdString, sharePayload)
|
share, err := unmarshalShare(shareIdString, sharePayload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userRole, err := tx.Get(share.userKey(username))
|
||||||
|
if err == buntdb.ErrNotFound {
|
||||||
|
// TODO warn about inconsistency
|
||||||
|
return ErrShareNotFound
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loginPayload, err := tx.Get(share.loginKey(username, loginName))
|
||||||
|
if err == buntdb.ErrNotFound {
|
||||||
|
// TODO warn about inconsistency
|
||||||
|
return ErrShareNotFound
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
loginShare.Share = share
|
loginShare.Share = share
|
||||||
loginShare.ShareUser = ShareUser{
|
loginShare.ShareUser = ShareUser{
|
||||||
Username: username,
|
Username: username,
|
||||||
Role: ShareRole(userRole),
|
Role: ShareRole(userRole),
|
||||||
}
|
}
|
||||||
loginShare.Login = Login{
|
if err := json.Unmarshal([]byte(loginPayload), &loginShare.Login); err != nil {
|
||||||
LoginName: loginName,
|
return fmt.Errorf("cannot unmarshal login: %w", err)
|
||||||
Password: loginPassword,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -543,7 +556,7 @@ func (store *DBStore) FindSharesByUser(username string) ([]UserShare, error) {
|
||||||
func unmarshalShare(idString, payload string) (Share, error) {
|
func unmarshalShare(idString, payload string) (Share, error) {
|
||||||
var share Share
|
var share Share
|
||||||
if err := json.Unmarshal([]byte(payload), &share); err != nil {
|
if err := json.Unmarshal([]byte(payload), &share); err != nil {
|
||||||
return Share{}, err
|
return Share{}, fmt.Errorf("cannot unmarshal share: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just in case ...
|
// Just in case ...
|
||||||
|
|
|
@ -220,7 +220,7 @@ func TestStoreShareHandling(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("cannot add login if user doesn't exist", func(t *testing.T) {
|
t.Run("cannot add login if user doesn't exist", func(t *testing.T) {
|
||||||
if err := store.AddLogin(share3, user1.Username, Login{"foo", ""}); err == nil {
|
if err := store.AddLogin(share3, user1.Username, Login{"foo", "", false}); err == nil {
|
||||||
t.Errorf("an error should have been returned")
|
t.Errorf("an error should have been returned")
|
||||||
} else if err != ErrUserNotFound {
|
} else if err != ErrUserNotFound {
|
||||||
t.Errorf("wrong error has been returned: %v", err)
|
t.Errorf("wrong error has been returned: %v", err)
|
||||||
|
|
Loading…
Reference in New Issue