From 59a39efc4afde0482b997555381a26b5f5177aae Mon Sep 17 00:00:00 2001 From: hexxa Date: Wed, 31 Aug 2022 22:30:24 +0800 Subject: [PATCH] feat(db): enable sqlite for storing users --- configs/dev.yml | 1 + src/db/common.go | 20 ++ src/db/rdb/sqlite/sqlite.go | 3 +- src/db/rdb/sqlite/users.go | 418 ++++++++++++++++++++++++++++ src/db/rdb/sqlite/users_test.go | 293 +++++++++++++++++++ src/db/userstore/user_store.go | 46 +-- src/depidx/deps.go | 7 +- src/handlers/multiusers/handlers.go | 28 +- src/server/config.go | 6 +- src/server/server.go | 28 +- 10 files changed, 791 insertions(+), 59 deletions(-) create mode 100644 src/db/rdb/sqlite/users.go create mode 100644 src/db/rdb/sqlite/users_test.go diff --git a/configs/dev.yml b/configs/dev.yml index 1cf6590..e7713f3 100644 --- a/configs/dev.yml +++ b/configs/dev.yml @@ -50,3 +50,4 @@ site: align: "fixed" db: dbPath: "tmp/quickshare.db" + dbType: "rdb" \ No newline at end of file diff --git a/src/db/common.go b/src/db/common.go index 0c0db59..90b2f0f 100644 --- a/src/db/common.go +++ b/src/db/common.go @@ -1,6 +1,7 @@ package db import ( + "context" "errors" "fmt" "reflect" @@ -157,6 +158,25 @@ type UploadInfo struct { Uploaded int64 `json:"uploaded" yaml:"uploaded"` } +type IUserStore interface { + Init(ctx context.Context, rootName, rootPwd string) error + IsInited() bool + AddUser(ctx context.Context, user *User) error + DelUser(ctx context.Context, id uint64) error + GetUser(ctx context.Context, id uint64) (*User, error) + GetUserByName(ctx context.Context, name string) (*User, error) + SetInfo(ctx context.Context, id uint64, user *User) error + SetUsed(ctx context.Context, id uint64, incr bool, capacity int64) error + ResetUsed(ctx context.Context, id uint64, used int64) error + SetPwd(ctx context.Context, id uint64, pwd string) error + SetPreferences(ctx context.Context, id uint64, settings *Preferences) error + ListUsers(context.Context) ([]*User, error) + ListUserIDs(context.Context) (map[string]string, error) + AddRole(role string) error + DelRole(role string) error + ListRoles() (map[string]bool, error) +} + func ComparePreferences(p1, p2 *Preferences) bool { return p1.CSSURL == p2.CSSURL && p1.LanPackURL == p2.LanPackURL && diff --git a/src/db/rdb/sqlite/sqlite.go b/src/db/rdb/sqlite/sqlite.go index 4175461..82bccfa 100644 --- a/src/db/rdb/sqlite/sqlite.go +++ b/src/db/rdb/sqlite/sqlite.go @@ -2,6 +2,7 @@ package sqlite import ( "database/sql" + "fmt" _ "github.com/mattn/go-sqlite3" "github.com/ihexxa/quickshare/src/db/rdb" @@ -13,7 +14,7 @@ type SQLite struct { } func NewSQLite(dbPath string) (*SQLite, error) { - db, err := sql.Open("sqlite3", dbPath) + db, err := sql.Open("sqlite3", fmt.Sprintf("%s?_synchronous=FULL", dbPath)) if err != nil { return nil, err } diff --git a/src/db/rdb/sqlite/users.go b/src/db/rdb/sqlite/users.go new file mode 100644 index 0000000..b757722 --- /dev/null +++ b/src/db/rdb/sqlite/users.go @@ -0,0 +1,418 @@ +package sqlite + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + // "errors" + "fmt" + // "sync" + // "time" + + "github.com/ihexxa/quickshare/src/db" + "github.com/ihexxa/quickshare/src/db/rdb" + // "github.com/ihexxa/quickshare/src/kvstore" +) + +// TODO: use sync.Pool instead + +const ( + VisitorID = uint64(1) + VisitorName = "visitor" +) + +var ( + ErrReachedLimit = errors.New("reached space limit") + ErrUserNotFound = errors.New("user not found") + ErrNegtiveUsedSpace = errors.New("used space can not be negative") +) + +type SQLiteUsers struct { + db rdb.IDB +} + +func NewSQLiteUsers(db rdb.IDB) (*SQLiteUsers, error) { + return &SQLiteUsers{db: db}, nil +} + +func (u *SQLiteUsers) Init(ctx context.Context, rootName, rootPwd string) error { + _, err := u.db.ExecContext( + ctx, + `create table if not exists t_user ( + id bigint not null, + name varchar not null, + pwd varchar not null, + role integer not null, + used_space bigint not null, + quota varchar not null, + preference varchar not null, + primary key(id) + )`, + ) + if err != nil { + return err + } + + admin := &db.User{ + ID: 0, + Name: rootName, + Pwd: rootPwd, + Role: db.AdminRole, + Quota: &db.Quota{ + SpaceLimit: db.DefaultSpaceLimit, + UploadSpeedLimit: db.DefaultUploadSpeedLimit, + DownloadSpeedLimit: db.DefaultDownloadSpeedLimit, + }, + Preferences: &db.DefaultPreferences, + } + visitor := &db.User{ + ID: VisitorID, + Name: VisitorName, + Pwd: rootPwd, + Role: db.VisitorRole, + Quota: &db.Quota{ + SpaceLimit: 0, + UploadSpeedLimit: db.VisitorUploadSpeedLimit, + DownloadSpeedLimit: db.VisitorDownloadSpeedLimit, + }, + Preferences: &db.DefaultPreferences, + } + for _, user := range []*db.User{admin, visitor} { + err = u.AddUser(ctx, user) + if err != nil { + return err + } + } + + return nil +} + +func (u *SQLiteUsers) IsInited() bool { + // always try to init the db + return false +} + +// t_users +// id, name, pwd, role, used_space, config +func (u *SQLiteUsers) setUser(ctx context.Context, tx *sql.Tx, user *db.User) error { + var err error + if err = db.CheckUser(user, false); err != nil { + return err + } + + quotaStr, err := json.Marshal(user.Quota) + if err != nil { + return err + } + preferencesStr, err := json.Marshal(user.Preferences) + if err != nil { + return err + } + _, err = tx.ExecContext( + ctx, + `update t_user + set name=?, pwd=?, role=?, used_space=?, quota=?, preference=? + where id=?`, + user.Name, + user.Pwd, + user.Role, + user.UsedSpace, + quotaStr, + preferencesStr, + ) + return err +} + +func (u *SQLiteUsers) getUser(ctx context.Context, tx *sql.Tx, id uint64) (*db.User, error) { + var err error + + user := &db.User{} + var quotaStr, preferenceStr string + err = tx.QueryRowContext( + ctx, + `select id, name, pwd, role, used_space, quota, preference + from t_user + where id=?`, + id, + ).Scan( + &user.ID, + &user.Name, + &user.Pwd, + &user.Role, + &user.UsedSpace, + "aStr, + &preferenceStr, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrUserNotFound + } + return nil, err + } + + err = json.Unmarshal([]byte(quotaStr), &user.Quota) + if err != nil { + return nil, err + } + err = json.Unmarshal([]byte(preferenceStr), &user.Preferences) + if err != nil { + return nil, err + } + return user, nil +} + +func (u *SQLiteUsers) AddUser(ctx context.Context, user *db.User) error { + quotaStr, err := json.Marshal(user.Quota) + if err != nil { + return err + } + preferenceStr, err := json.Marshal(user.Preferences) + if err != nil { + return err + } + _, err = u.db.ExecContext( + ctx, + `insert into t_user (id, name, pwd, role, used_space, quota, preference) values (?, ?, ?, ?, ?, ?, ?)`, + user.ID, + user.Name, + user.Pwd, + user.Role, + user.UsedSpace, + quotaStr, + preferenceStr, + ) + return err +} + +func (u *SQLiteUsers) DelUser(ctx context.Context, id uint64) error { + _, err := u.db.ExecContext( + ctx, + `delete from t_user where id=?`, + id, + ) + return err +} + +func (u *SQLiteUsers) GetUser(ctx context.Context, id uint64) (*db.User, error) { + tx, err := u.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return nil, err + } + + user, err := u.getUser(ctx, tx, id) + if err != nil { + return nil, err + } + + err = tx.Commit() + if err != nil { + return nil, err + } + return user, err +} + +func (u *SQLiteUsers) GetUserByName(ctx context.Context, name string) (*db.User, error) { + user := &db.User{} + var quotaStr, preferenceStr string + err := u.db.QueryRowContext( + ctx, + `select id, name, role, used_space, quota, preference + from t_user + where name=?`, + name, + ).Scan( + &user.ID, + &user.Name, + &user.Role, + &user.UsedSpace, + "aStr, + &preferenceStr, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrUserNotFound + } + return nil, err + } + + err = json.Unmarshal([]byte(quotaStr), &user.Quota) + if err != nil { + return nil, err + } + err = json.Unmarshal([]byte(preferenceStr), &user.Preferences) + if err != nil { + return nil, err + } + return user, nil +} + +func (u *SQLiteUsers) SetPwd(ctx context.Context, id uint64, pwd string) error { + _, err := u.db.ExecContext( + ctx, + `update t_user + set pwd=? + where id=?`, + pwd, + id, + ) + return err +} + +// role + quota +func (u *SQLiteUsers) SetInfo(ctx context.Context, id uint64, user *db.User) error { + quotaStr, err := json.Marshal(user.Quota) + if err != nil { + return err + } + + _, err = u.db.ExecContext( + ctx, + `update t_user + set role=?, quota=? + where id=?`, + user.Role, quotaStr, + id, + ) + return err +} + +func (u *SQLiteUsers) SetPreferences(ctx context.Context, id uint64, prefers *db.Preferences) error { + preferenceStr, err := json.Marshal(prefers) + if err != nil { + return err + } + + _, err = u.db.ExecContext( + ctx, + `update t_user + set preference=? + where id=?`, + preferenceStr, + id, + ) + return err +} + +func (u *SQLiteUsers) SetUsed(ctx context.Context, id uint64, incr bool, capacity int64) error { + tx, err := u.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return err + } + + gotUser, err := u.getUser(ctx, tx, id) + if err != nil { + return err + } + + if incr && gotUser.UsedSpace+capacity > int64(gotUser.Quota.SpaceLimit) { + return ErrReachedLimit + } + + if incr { + gotUser.UsedSpace = gotUser.UsedSpace + capacity + } else { + if gotUser.UsedSpace-capacity < 0 { + return ErrNegtiveUsedSpace + } + gotUser.UsedSpace = gotUser.UsedSpace - capacity + } + + _, err = tx.ExecContext( + ctx, + `update t_user + set used_space=? + where id=?`, + gotUser.UsedSpace, + gotUser.ID, + ) + if err != nil { + return err + } + + return tx.Commit() +} + +func (u *SQLiteUsers) ResetUsed(ctx context.Context, id uint64, used int64) error { + _, err := u.db.ExecContext( + ctx, + `update t_user + set used_space=? + where id=?`, + used, + id, + ) + return err +} + +func (u *SQLiteUsers) ListUsers(ctx context.Context) ([]*db.User, error) { + // TODO: support pagination + rows, err := u.db.QueryContext( + ctx, + `select id, name, role, used_space, quota, preference + from t_user`, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrUserNotFound + } + return nil, err + } + defer rows.Close() // TODO: check error + + users := []*db.User{} + for rows.Next() { + user := &db.User{} + var quotaStr, preferenceStr string + err = rows.Scan( + &user.ID, + &user.Name, + &user.Role, + &user.UsedSpace, + "aStr, + &preferenceStr, + ) + err = json.Unmarshal([]byte(quotaStr), &user.Quota) + if err != nil { + return nil, err + } + err = json.Unmarshal([]byte(preferenceStr), &user.Preferences) + if err != nil { + return nil, err + } + + users = append(users, user) + } + if rows.Err() != nil { + return nil, rows.Err() + } + return users, nil +} + +func (u *SQLiteUsers) ListUserIDs(ctx context.Context) (map[string]string, error) { + users, err := u.ListUsers(ctx) + if err != nil { + return nil, err + } + + nameToId := map[string]string{} + for _, user := range users { + nameToId[user.Name] = fmt.Sprint(user.ID) + } + return nameToId, nil +} + +func (u *SQLiteUsers) AddRole(role string) error { + // TODO: implement this after adding grant/revoke + panic("not implemented") +} + +func (u *SQLiteUsers) DelRole(role string) error { + // TODO: implement this after adding grant/revoke + panic("not implemented") +} + +func (u *SQLiteUsers) ListRoles() (map[string]bool, error) { + // TODO: implement this after adding grant/revoke + panic("not implemented") +} diff --git a/src/db/rdb/sqlite/users_test.go b/src/db/rdb/sqlite/users_test.go new file mode 100644 index 0000000..bf65407 --- /dev/null +++ b/src/db/rdb/sqlite/users_test.go @@ -0,0 +1,293 @@ +package sqlite + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/ihexxa/quickshare/src/db" +) + +func TestUserStores(t *testing.T) { + rootName, rootPwd := "root", "rootPwd" + + testUserMethods := func(t *testing.T, store db.IUserStore) { + ctx := context.TODO() + root, err := store.GetUser(ctx, 0) + if err != nil { + t.Fatal(err) + } + if root.Name != rootName { + t.Fatal("root user not found") + } + if root.Pwd != rootPwd { + t.Fatalf("passwords not match (%s) (%s)", root.Pwd, rootPwd) + } + if root.Role != db.AdminRole { + t.Fatalf("incorrect root role") + } + if root.Quota.SpaceLimit != db.DefaultSpaceLimit { + t.Fatalf("incorrect root SpaceLimit") + } + if root.Quota.UploadSpeedLimit != db.DefaultUploadSpeedLimit { + t.Fatalf("incorrect root UploadSpeedLimit") + } + if root.Quota.DownloadSpeedLimit != db.DefaultDownloadSpeedLimit { + t.Fatalf("incorrect root DownloadSpeedLimit") + } + if !db.ComparePreferences(root.Preferences, &db.DefaultPreferences) { + t.Fatalf("incorrect preference %v %v", root.Preferences, db.DefaultPreferences) + } + + visitor, err := store.GetUser(ctx, 1) + if err != nil { + t.Fatal(err) + } + if visitor.Name != VisitorName { + t.Fatal("visitor not found") + } + if visitor.Pwd != rootPwd { + t.Fatalf("passwords not match %s", err) + } + if visitor.Role != db.VisitorRole { + t.Fatalf("incorrect visitor role") + } + if visitor.Quota.SpaceLimit != 0 { + t.Fatalf("incorrect visitor SpaceLimit") + } + if visitor.Quota.UploadSpeedLimit != db.VisitorUploadSpeedLimit { + t.Fatalf("incorrect visitor UploadSpeedLimit") + } + if visitor.Quota.DownloadSpeedLimit != db.VisitorDownloadSpeedLimit { + t.Fatalf("incorrect visitor DownloadSpeedLimit") + } + if !db.ComparePreferences(visitor.Preferences, &db.DefaultPreferences) { + t.Fatalf("incorrect preference") + } + + id, name1 := uint64(2), "test_user1" + pwd1, pwd2 := "666", "888" + role1, role2 := db.UserRole, db.AdminRole + spaceLimit1, upLimit1, downLimit1 := int64(17), 5, 7 + spaceLimit2, upLimit2, downLimit2 := int64(19), 13, 17 + + err = store.AddUser(ctx, &db.User{ + ID: id, + Name: name1, + Pwd: pwd1, + Role: role1, + Quota: &db.Quota{ + SpaceLimit: spaceLimit1, + UploadSpeedLimit: upLimit1, + DownloadSpeedLimit: downLimit1, + }, + Preferences: &db.DefaultPreferences, + }) + if err != nil { + t.Fatal("there should be no error") + } + + user, err := store.GetUser(ctx, id) + if err != nil { + t.Fatal(err) + } + if user.Name != name1 { + t.Fatalf("names not matched %s %s", name1, user.Name) + } + if user.Pwd != pwd1 { + t.Fatalf("passwords not match %s", err) + } + if user.Role != role1 { + t.Fatalf("roles not matched %s %s", role1, user.Role) + } + if user.Quota.SpaceLimit != spaceLimit1 { + t.Fatalf("space limit not matched %d %d", spaceLimit1, user.Quota.SpaceLimit) + } + if user.Quota.UploadSpeedLimit != upLimit1 { + t.Fatalf("up limit not matched %d %d", upLimit1, user.Quota.UploadSpeedLimit) + } + if user.Quota.DownloadSpeedLimit != downLimit1 { + t.Fatalf("down limit not matched %d %d", downLimit1, user.Quota.DownloadSpeedLimit) + } + + users, err := store.ListUsers(ctx) + if err != nil { + t.Fatal(err) + } + if len(users) != 3 { + t.Fatalf("users size should be 3 (%d)", len(users)) + } + for _, user := range users { + if user.ID == 0 { + if user.Name != rootName || user.Role != db.AdminRole { + t.Fatalf("incorrect root info %v", user) + } + } + if user.ID == id { + if user.Name != name1 || user.Role != role1 { + t.Fatalf("incorrect user info %v", user) + } + } + if user.Pwd != "" { + t.Fatalf("password must be empty") + } + } + + err = store.SetPwd(ctx, id, pwd2) + if err != nil { + t.Fatal(err) + } + store.SetInfo(ctx, id, &db.User{ + ID: id, + Role: role2, + Quota: &db.Quota{ + SpaceLimit: spaceLimit2, + UploadSpeedLimit: upLimit2, + DownloadSpeedLimit: downLimit2, + }, + }) + + usedIncr, usedDecr := int64(spaceLimit2), int64(7) + err = store.SetUsed(ctx, id, true, usedIncr) + if err != nil { + t.Fatal(err) + } + err = store.SetUsed(ctx, id, false, usedDecr) + if err != nil { + t.Fatal(err) + } + err = store.SetUsed(ctx, id, true, int64(spaceLimit2)-(usedIncr-usedDecr)+1) + if err == nil || !strings.Contains(err.Error(), "reached space limit") { + t.Fatal("should reject big file") + } else { + err = nil + } + + user, err = store.GetUser(ctx, id) + if err != nil { + t.Fatal(err) + } + if user.Pwd != pwd2 { + t.Fatalf("passwords not match %s %s", user.Pwd, pwd2) + } + if user.Role != role2 { + t.Fatalf("roles not matched %s %s", role2, user.Role) + } + if user.Quota.SpaceLimit != spaceLimit2 { + t.Fatalf("space limit not matched %d %d", spaceLimit2, user.Quota.SpaceLimit) + } + if user.Quota.UploadSpeedLimit != upLimit2 { + t.Fatalf("up limit not matched %d %d", upLimit2, user.Quota.UploadSpeedLimit) + } + if user.Quota.DownloadSpeedLimit != downLimit2 { + t.Fatalf("down limit not matched %d %d", downLimit2, user.Quota.DownloadSpeedLimit) + } + if user.UsedSpace != usedIncr-usedDecr { + t.Fatalf("used space not matched %d %d", user.UsedSpace, usedIncr-usedDecr) + } + + time.Sleep(5 * time.Second) + newPrefer := &db.Preferences{ + Bg: &db.BgConfig{ + Url: "/url", + Repeat: "repeat", + Position: "center", + Align: "fixed", + BgColor: "#333", + }, + CSSURL: "/cssurl", + LanPackURL: "lanPackURL", + Lan: "zhCN", + Theme: "dark", + Avatar: "/avatar", + Email: "foo@gmail.com", + } + err = store.SetPreferences(ctx, id, newPrefer) + if err != nil { + t.Fatal(err) + } + + user, err = store.GetUserByName(ctx, name1) + if err != nil { + t.Fatal(err) + } + if user.ID != id { + t.Fatalf("ids not matched %d %d", id, user.ID) + } + if user.Pwd != pwd2 { + t.Fatalf("passwords not match %s", err) + } + if user.Role != role2 { + t.Fatalf("roles not matched %s %s", role2, user.Role) + } + if user.Quota.SpaceLimit != spaceLimit2 { + t.Fatalf("space limit not matched %d %d", spaceLimit2, user.Quota.SpaceLimit) + } + if user.Quota.UploadSpeedLimit != upLimit2 { + t.Fatalf("up limit not matched %d %d", upLimit2, user.Quota.UploadSpeedLimit) + } + if user.Quota.DownloadSpeedLimit != downLimit2 { + t.Fatalf("down limit not matched %d %d", downLimit2, user.Quota.DownloadSpeedLimit) + } + if !db.ComparePreferences(user.Preferences, newPrefer) { + t.Fatalf("preferences not matched %v %v", user.Preferences, newPrefer) + } + + err = store.DelUser(ctx, id) + if err != nil { + t.Fatal(err) + } + users, err = store.ListUsers(ctx) + if err != nil { + t.Fatal(err) + } + if len(users) != 2 { + t.Fatalf("users size should be 2 (%d)", len(users)) + } + for _, user := range users { + if user.ID == 0 && user.Name != rootName && user.Role != db.AdminRole { + t.Fatalf("incorrect root info %v", user) + } + if user.ID == VisitorID && user.Name != VisitorName && user.Role != db.VisitorRole { + t.Fatalf("incorrect visitor info %v", user) + } + } + + nameToID, err := store.ListUserIDs(ctx) + if err != nil { + t.Fatal(err) + } + if len(nameToID) != len(users) { + t.Fatalf("nameToID size (%d) should be same as (%d)", len(nameToID), len(users)) + } + } + + t.Run("testing UserStore sqlite", func(t *testing.T) { + rootPath, err := ioutil.TempDir("./", "quickshare_userstore_test_") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rootPath) + + dbPath := filepath.Join(rootPath, "quickshare.sqlite") + sqliteDB, err := NewSQLite(dbPath) + if err != nil { + t.Fatal(err) + } + defer sqliteDB.Close() + + store, err := NewSQLiteUsers(sqliteDB) + if err != nil { + t.Fatal("fail to new user store", err) + } + if err = store.Init(context.TODO(), rootName, rootPwd); err != nil { + t.Fatal("fail to init user store", err) + } + + testUserMethods(t, store) + }) +} diff --git a/src/db/userstore/user_store.go b/src/db/userstore/user_store.go index 7271f6b..b2bc5b7 100644 --- a/src/db/userstore/user_store.go +++ b/src/db/userstore/user_store.go @@ -1,6 +1,7 @@ package userstore import ( + "context" "encoding/json" "errors" "fmt" @@ -24,25 +25,6 @@ var ( ErrNegtiveUsedSpace = errors.New("used space can not be negative") ) -type IUserStore interface { - Init(rootName, rootPwd string) error - IsInited() bool - AddUser(user *db.User) error - DelUser(id uint64) error - GetUser(id uint64) (*db.User, error) - GetUserByName(name string) (*db.User, error) - SetInfo(id uint64, user *db.User) error - SetUsed(id uint64, incr bool, capacity int64) error - ResetUsed(id uint64, used int64) error - SetPwd(id uint64, pwd string) error - SetPreferences(id uint64, settings *db.Preferences) error - ListUsers() ([]*db.User, error) - ListUserIDs() (map[string]string, error) - AddRole(role string) error - DelRole(role string) error - ListRoles() (map[string]bool, error) -} - type KVUserStore struct { store kvstore.IKVStore mtx *sync.RWMutex @@ -55,7 +37,7 @@ func NewKVUserStore(store kvstore.IKVStore) (*KVUserStore, error) { }, nil } -func (us *KVUserStore) Init(rootName, rootPwd string) error { +func (us *KVUserStore) Init(ctx context.Context, rootName, rootPwd string) error { var err error for _, namespace := range []string{ @@ -99,7 +81,7 @@ func (us *KVUserStore) Init(rootName, rootPwd string) error { } for _, user := range []*db.User{admin, visitor} { - err = us.AddUser(user) + err = us.AddUser(context.TODO(), user) if err != nil { return err } @@ -181,14 +163,14 @@ func (us *KVUserStore) getUserByName(name string) (*db.User, error) { return user, nil } -func (us *KVUserStore) AddUser(user *db.User) error { +func (us *KVUserStore) AddUser(ctx context.Context, user *db.User) error { us.mtx.Lock() defer us.mtx.Unlock() return us.setUser(user) } -func (us *KVUserStore) DelUser(id uint64) error { +func (us *KVUserStore) DelUser(ctx context.Context, id uint64) error { us.mtx.Lock() defer us.mtx.Unlock() @@ -206,21 +188,21 @@ func (us *KVUserStore) DelUser(id uint64) error { return nil } -func (us *KVUserStore) GetUser(id uint64) (*db.User, error) { +func (us *KVUserStore) GetUser(ctx context.Context, id uint64) (*db.User, error) { us.mtx.RLock() defer us.mtx.RUnlock() return us.getUser(id) } -func (us *KVUserStore) GetUserByName(name string) (*db.User, error) { +func (us *KVUserStore) GetUserByName(ctx context.Context, name string) (*db.User, error) { us.mtx.RLock() defer us.mtx.RUnlock() return us.getUserByName(name) } -func (us *KVUserStore) SetPwd(id uint64, pwd string) error { +func (us *KVUserStore) SetPwd(ctx context.Context, id uint64, pwd string) error { us.mtx.Lock() defer us.mtx.Unlock() @@ -233,7 +215,7 @@ func (us *KVUserStore) SetPwd(id uint64, pwd string) error { return us.setUser(user) } -func (us *KVUserStore) SetInfo(id uint64, user *db.User) error { +func (us *KVUserStore) SetInfo(ctx context.Context, id uint64, user *db.User) error { us.mtx.Lock() defer us.mtx.Unlock() @@ -248,7 +230,7 @@ func (us *KVUserStore) SetInfo(id uint64, user *db.User) error { return us.setUser(gotUser) } -func (us *KVUserStore) SetPreferences(id uint64, prefers *db.Preferences) error { +func (us *KVUserStore) SetPreferences(ctx context.Context, id uint64, prefers *db.Preferences) error { us.mtx.Lock() defer us.mtx.Unlock() @@ -261,7 +243,7 @@ func (us *KVUserStore) SetPreferences(id uint64, prefers *db.Preferences) error return us.setUser(user) } -func (us *KVUserStore) SetUsed(id uint64, incr bool, capacity int64) error { +func (us *KVUserStore) SetUsed(ctx context.Context, id uint64, incr bool, capacity int64) error { us.mtx.Lock() defer us.mtx.Unlock() @@ -286,7 +268,7 @@ func (us *KVUserStore) SetUsed(id uint64, incr bool, capacity int64) error { return us.setUser(gotUser) } -func (us *KVUserStore) ResetUsed(id uint64, used int64) error { +func (us *KVUserStore) ResetUsed(ctx context.Context, id uint64, used int64) error { us.mtx.Lock() defer us.mtx.Unlock() @@ -299,7 +281,7 @@ func (us *KVUserStore) ResetUsed(id uint64, used int64) error { return us.setUser(gotUser) } -func (us *KVUserStore) ListUsers() ([]*db.User, error) { +func (us *KVUserStore) ListUsers(ctx context.Context) ([]*db.User, error) { us.mtx.RLock() defer us.mtx.RUnlock() @@ -355,7 +337,7 @@ func (us *KVUserStore) ListUsers() ([]*db.User, error) { return users, nil } -func (us *KVUserStore) ListUserIDs() (map[string]string, error) { +func (us *KVUserStore) ListUserIDs(ctx context.Context) (map[string]string, error) { us.mtx.RLock() defer us.mtx.RUnlock() diff --git a/src/depidx/deps.go b/src/depidx/deps.go index cdc663a..bd0a655 100644 --- a/src/depidx/deps.go +++ b/src/depidx/deps.go @@ -10,7 +10,6 @@ import ( "github.com/ihexxa/quickshare/src/db/fileinfostore" "github.com/ihexxa/quickshare/src/db/rdb" "github.com/ihexxa/quickshare/src/db/sitestore" - "github.com/ihexxa/quickshare/src/db/userstore" "github.com/ihexxa/quickshare/src/fs" "github.com/ihexxa/quickshare/src/idgen" "github.com/ihexxa/quickshare/src/iolimiter" @@ -31,7 +30,7 @@ type Deps struct { fs fs.ISimpleFS token cryptoutil.ITokenEncDec kv kvstore.IKVStore - users userstore.IUserStore + users db.IUserStore fileInfos fileinfostore.IFileInfoStore siteStore sitestore.ISiteStore id idgen.IIDGen @@ -88,11 +87,11 @@ func (deps *Deps) SetLog(logger *zap.SugaredLogger) { deps.logger = logger } -func (deps *Deps) Users() userstore.IUserStore { +func (deps *Deps) Users() db.IUserStore { return deps.users } -func (deps *Deps) SetUsers(users userstore.IUserStore) { +func (deps *Deps) SetUsers(users db.IUserStore) { deps.users = users } diff --git a/src/handlers/multiusers/handlers.go b/src/handlers/multiusers/handlers.go index b274307..68c36f7 100644 --- a/src/handlers/multiusers/handlers.go +++ b/src/handlers/multiusers/handlers.go @@ -157,7 +157,7 @@ func (h *MultiUsersSvc) Init(adminName, adminPwd string) (string, error) { } // TODO: return "" for being compatible with singleuser service, should remove this - err = h.deps.Users().Init(adminName, adminPwd) + err = h.deps.Users().Init(c, adminName, adminPwd) if err != nil { return "", err } @@ -205,7 +205,7 @@ func (h *MultiUsersSvc) Init(adminName, adminPwd string) (string, error) { Preferences: &preferences, } - err = h.deps.Users().AddUser(user) + err = h.deps.Users().AddUser(c, user) if err != nil { h.deps.Log().Warn("warning: failed to add user(%s): %s", user, err) return "", err @@ -243,7 +243,7 @@ func (h *MultiUsersSvc) Login(c *gin.Context) { } } - user, err := h.deps.Users().GetUserByName(req.User) + user, err := h.deps.Users().GetUserByName(c, req.User) if err != nil { if errors.Is(err, userstore.ErrUserNotFound) { c.JSON(q.ErrResp(c, 403, err)) @@ -320,7 +320,7 @@ func (h *MultiUsersSvc) SetPwd(c *gin.Context) { return } - user, err := h.deps.Users().GetUser(uid) + user, err := h.deps.Users().GetUser(c, uid) if err != nil { c.JSON(q.ErrResp(c, 402, err)) return @@ -338,7 +338,7 @@ func (h *MultiUsersSvc) SetPwd(c *gin.Context) { return } - err = h.deps.Users().SetPwd(uid, string(newHash)) + err = h.deps.Users().SetPwd(c, uid, string(newHash)) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return @@ -364,7 +364,7 @@ func (h *MultiUsersSvc) ForceSetPwd(c *gin.Context) { c.JSON(q.ErrResp(c, 500, err)) return } - targetUser, err := h.deps.Users().GetUser(targetUID) + targetUser, err := h.deps.Users().GetUser(c, targetUID) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return @@ -380,7 +380,7 @@ func (h *MultiUsersSvc) ForceSetPwd(c *gin.Context) { return } - err = h.deps.Users().SetPwd(targetUser.ID, string(newHash)) + err = h.deps.Users().SetPwd(c, targetUser.ID, string(newHash)) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return @@ -437,7 +437,7 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) { } newPreferences := db.DefaultPreferences - err = h.deps.Users().AddUser(&db.User{ + err = h.deps.Users().AddUser(c, &db.User{ ID: uid, Name: req.Name, Pwd: string(pwdHash), @@ -483,7 +483,7 @@ func (h *MultiUsersSvc) DelUser(c *gin.Context) { } // TODO: try to make following atomic - err = h.deps.Users().DelUser(userID) + err = h.deps.Users().DelUser(c, userID) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return @@ -514,7 +514,7 @@ func (h *MultiUsersSvc) ListUsers(c *gin.Context) { // } // } - users, err := h.deps.Users().ListUsers() + users, err := h.deps.Users().ListUsers(c) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return @@ -651,7 +651,7 @@ func (h *MultiUsersSvc) Self(c *gin.Context) { return } - user, err := h.deps.Users().GetUserByName(claims[q.UserParam]) + user, err := h.deps.Users().GetUserByName(c, claims[q.UserParam]) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return @@ -686,7 +686,7 @@ func (h *MultiUsersSvc) SetUser(c *gin.Context) { return } - err := h.deps.Users().SetInfo(req.ID, &db.User{ + err := h.deps.Users().SetInfo(c, req.ID, &db.User{ Role: req.Role, Quota: req.Quota, }) @@ -721,7 +721,7 @@ func (h *MultiUsersSvc) SetPreferences(c *gin.Context) { req.Preferences.Bg = db.DefaultBgConfig } - err = h.deps.Users().SetPreferences(uid, req.Preferences) + err = h.deps.Users().SetPreferences(c, uid, req.Preferences) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return @@ -740,7 +740,7 @@ func (h *MultiUsersSvc) ResetUsedSpace(c *gin.Context) { return } - userInfo, err := h.deps.Users().GetUser(req.UserID) + userInfo, err := h.deps.Users().GetUser(c, req.UserID) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return diff --git a/src/server/config.go b/src/server/config.go index 2d5e9f1..6db8e2d 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -9,7 +9,8 @@ import ( const fileIndexPath = "/fileindex.jsonl" type DbConfig struct { - DbPath string `json:"dbPath" yaml:"dbPath"` + DbPath string `json:"dbPath" yaml:"dbPath"` + RdbPath string `json:"rdbPath" yaml:"rdbPath"` // valid values: rdb, kv } type FSConfig struct { @@ -140,7 +141,8 @@ func DefaultConfigStruct() *Config { }, }, Db: &DbConfig{ - DbPath: "quickshare.db", + DbPath: "quickshare.db", + RdbPath: "quickshare.sqlite", }, } } diff --git a/src/server/server.go b/src/server/server.go index cc023db..3a936b3 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -27,6 +27,7 @@ import ( "github.com/ihexxa/quickshare/src/db" "github.com/ihexxa/quickshare/src/db/boltstore" "github.com/ihexxa/quickshare/src/db/fileinfostore" + "github.com/ihexxa/quickshare/src/db/rdb/sqlite" "github.com/ihexxa/quickshare/src/db/sitestore" "github.com/ihexxa/quickshare/src/db/userstore" "github.com/ihexxa/quickshare/src/depidx" @@ -65,7 +66,7 @@ func NewServer(cfg gocfg.ICfg) (*Server, error) { err = checkCompatibility(deps) if err != nil { - return nil, fmt.Errorf("fail to check compatibility: %w", err) + return nil, fmt.Errorf("failed to check compatibility: %w", err) } port := cfg.GrabInt("Server.Port") @@ -152,19 +153,33 @@ func initDeps(cfg gocfg.ICfg) *depidx.Deps { kv := boltdbpvd.New(dbPath, 1024) users, err := userstore.NewKVUserStore(kv) if err != nil { - panic(fmt.Sprintf("fail to init user store: %s", err)) + panic(fmt.Sprintf("failed to init user store: %s", err)) } fileInfos, err := fileinfostore.NewFileInfoStore(kv) if err != nil { - panic(fmt.Sprintf("fail to init file info store: %s", err)) + panic(fmt.Sprintf("failed to init file info store: %s", err)) } siteStore, err := sitestore.NewSiteStore(kv) if err != nil { - panic(fmt.Sprintf("fail to init site config store: %s", err)) + panic(fmt.Sprintf("failed to init site config store: %s", err)) } boltDB, err := boltstore.NewBoltStore(kv.Bolt()) if err != nil { - panic(fmt.Sprintf("fail to init bolt store: %s", err)) + panic(fmt.Sprintf("failed to init bolt store: %s", err)) + } + + rdbPath := cfg.GrabString("Db.RdbPath") + if rdbPath == "" { + panic("rdbPath is blank") + } + rdbDir := filepath.Dir(rdbPath) + if err = filesystem.MkdirAll(rdbDir); err != nil { + panic(fmt.Sprintf("failed to create path for rdb: %s", err)) + } + + rdb, err := sqlite.NewSQLite(rdbPath) + if err != nil { + panic(fmt.Sprintf("failed to open sqlite: %s", err)) } err = siteStore.Init(&db.SiteConfig{ @@ -181,7 +196,7 @@ func initDeps(cfg gocfg.ICfg) *depidx.Deps { }, }) if err != nil { - panic(fmt.Sprintf("fail to init site config store: %s", err)) + panic(fmt.Sprintf("failed to init site config store: %s", err)) } limiterCap := cfg.IntOr("Users.LimiterCapacity", 10000) @@ -199,6 +214,7 @@ func initDeps(cfg gocfg.ICfg) *depidx.Deps { deps.SetID(ider) deps.SetLog(logger) deps.SetLimiter(limiter) + deps.SetDB(rdb) queueSize := cfg.GrabInt("Workers.QueueSize") sleepCyc := cfg.GrabInt("Workers.SleepCyc")