diff --git a/src/db/common.go b/src/db/common.go index 90b2f0f..63b0b7e 100644 --- a/src/db/common.go +++ b/src/db/common.go @@ -23,18 +23,38 @@ const ( AdminRole = "admin" UserRole = "user" VisitorRole = "visitor" + + VisitorID = uint64(1) + VisitorName = "visitor" ) var ( + // users related errors + ErrReachedLimit = errors.New("reached space limit") + ErrUserNotFound = errors.New("user not found") + ErrNegtiveUsedSpace = errors.New("used space can not be negative") ErrInvalidFileInfo = errors.New("invalid fileInfo") ErrInvalidUser = errors.New("invalid user") ErrInvalidQuota = errors.New("invalid quota") ErrInvalidPreferences = errors.New("invalid preferences") - ErrBucketNotFound = errors.New("bucket not found") - ErrKeyNotFound = errors.New("key not found") - ErrKeyExisting = errors.New("key is existing") - ErrCreateExisting = errors.New("create upload info which already exists") - ErrQuota = errors.New("quota limit reached") + // files related errors + ErrEmpty = errors.New("can not hash empty string") + ErrFileInfoNotFound = errors.New("file info not found") + ErrSharingNotFound = errors.New("sharing id not found") + ErrConflicted = errors.New("conflict found in hashing") + ErrVerNotFound = errors.New("file info schema version not found") + // uploadings + ErrGreaterThanSize = errors.New("uploaded is greater than file size") + ErrUploadNotFound = errors.New("upload info not found") + + // site + ErrConfigNotFound = errors.New("site config not found") + + ErrBucketNotFound = errors.New("bucket not found") + ErrKeyNotFound = errors.New("key not found") + ErrKeyExisting = errors.New("key is existing") + ErrCreateExisting = errors.New("create upload info which already exists") + ErrQuota = errors.New("quota limit reached") DefaultSiteName = "Quickshare" DefaultSiteDesc = "Quickshare" @@ -93,11 +113,11 @@ var ( ) type FileInfo struct { - IsDir bool `json:"isDir" yaml:"isDir"` - Shared bool `json:"shared" yaml:"shared"` - ShareID string `json:"shareID" yaml:"shareID"` + IsDir bool `json:"isDir" yaml:"isDir"` // deprecated + Shared bool `json:"shared" yaml:"shared"` // deprecated + ShareID string `json:"shareID" yaml:"shareID"` // deprecated Sha1 string `json:"sha1" yaml:"sha1"` - Size int64 `json:"size" yaml:"size"` + Size int64 `json:"size" yaml:"size"` // deprecated } type UserCfg struct { @@ -177,6 +197,30 @@ type IUserStore interface { ListRoles() (map[string]bool, error) } +type IFileInfoStore interface { + AddSharing(ctx context.Context, dirPath string) error + DelSharing(ctx context.Context, dirPath string) error + GetSharing(ctx context.Context, dirPath string) (bool, bool) + ListSharings(ctx context.Context, prefix string) (map[string]string, error) + GetFileInfo(ctx context.Context, itemPath string) (*FileInfo, error) + SetFileInfo(ctx context.Context, itemPath string, info *FileInfo) error + DelFileInfo(ctx context.Context, itemPath string) error + ListFileInfos(ctx context.Context, itemPaths []string) (map[string]*FileInfo, error) + SetSha1(ctx context.Context, itemPath, sign string) error + GetSharingDir(ctx context.Context, hashID string) (string, error) + // upload info + AddUploadInfo(ctx context.Context, user, filePath, tmpPath string, fileSize int64) error + SetUploadInfo(ctx context.Context, user, filePath string, newUploaded int64) error + GetUploadInfo(ctx context.Context, user, filePath string) (string, int64, int64, error) + DelUploadInfo(ctx context.Context, user, filePath string) error + ListUploadInfo(ctx context.Context, user string) ([]*UploadInfo, error) +} + +type ISiteStore interface { + SetClientCfg(ctx context.Context, cfg *ClientConfig) error + GetCfg(ctx context.Context) (*SiteConfig, error) +} + func ComparePreferences(p1, p2 *Preferences) bool { return p1.CSSURL == p2.CSSURL && p1.LanPackURL == p2.LanPackURL && diff --git a/src/db/fileinfostore/file_info_store.go b/src/db/fileinfostore/file_info_store.go index 3d7da20..93633b7 100644 --- a/src/db/fileinfostore/file_info_store.go +++ b/src/db/fileinfostore/file_info_store.go @@ -1,6 +1,7 @@ package fileinfostore import ( + "context" "crypto/sha1" "encoding/json" "errors" @@ -21,12 +22,12 @@ const ( ) var ( - ErrEmpty = errors.New("can not hash empty string") - ErrNotFound = errors.New("file info not found") - ErrSharingNotFound = errors.New("sharing id not found") - ErrConflicted = errors.New("conflict found in hashing") - ErrVerNotFound = errors.New("file info schema version not found") - maxHashingTime = 10 + // db.ErrEmpty = errors.New("can not hash empty string") + // db.ErrFileInfoNotFound = errors.New("file info not found") + // db.ErrSharingNotFound = errors.New("sharing id not found") + // db.ErrConflicted = errors.New("conflict found in hashing") + // db.ErrVerNotFound = errors.New("file info schema version not found") + maxHashingTime = 10 ) type IFileInfoStore interface { @@ -77,7 +78,7 @@ func NewFileInfoStore(store kvstore.IKVStore) (*FileInfoStore, error) { func (fi *FileInfoStore) getInfo(itemPath string) (*db.FileInfo, error) { infoStr, ok := fi.store.GetStringIn(db.FileInfoNs, itemPath) if !ok { - return nil, ErrNotFound + return nil, db.ErrFileInfoNotFound } info := &db.FileInfo{} @@ -92,16 +93,16 @@ func (fi *FileInfoStore) getInfo(itemPath string) (*db.FileInfo, error) { return info, nil } -func (fi *FileInfoStore) GetInfo(itemPath string) (*db.FileInfo, error) { +func (fi *FileInfoStore) GetFileInfo(ctx context.Context, itemPath string) (*db.FileInfo, error) { return fi.getInfo(itemPath) } -func (fi *FileInfoStore) GetInfos(itemPaths []string) (map[string]*db.FileInfo, error) { +func (fi *FileInfoStore) ListFileInfos(ctx context.Context, itemPaths []string) (map[string]*db.FileInfo, error) { infos := map[string]*db.FileInfo{} for _, itemPath := range itemPaths { info, err := fi.getInfo(itemPath) if err != nil { - if !errors.Is(err, ErrNotFound) { + if !errors.Is(err, db.ErrFileInfoNotFound) { // TODO: try to make info data consistent with fs return nil, err } @@ -133,23 +134,23 @@ func (fi *FileInfoStore) setInfo(itemPath string, info *db.FileInfo) error { return nil } -func (fi *FileInfoStore) SetInfo(itemPath string, info *db.FileInfo) error { +func (fi *FileInfoStore) SetFileInfo(ctx context.Context, itemPath string, info *db.FileInfo) error { return fi.setInfo(itemPath, info) } -func (fi *FileInfoStore) DelInfo(itemPath string) error { +func (fi *FileInfoStore) DelFileInfo(ctx context.Context, itemPath string) error { return fi.store.DelStringIn(db.FileInfoNs, itemPath) } // sharings -func (fi *FileInfoStore) SetSha1(itemPath, sign string) error { +func (fi *FileInfoStore) SetSha1(ctx context.Context, itemPath, sign string) error { fi.mtx.Lock() defer fi.mtx.Unlock() info, err := fi.getInfo(itemPath) if err != nil { - if !errors.Is(err, ErrNotFound) { + if !errors.Is(err, db.ErrFileInfoNotFound) { return err } info = &db.FileInfo{ @@ -163,7 +164,7 @@ func (fi *FileInfoStore) SetSha1(itemPath, sign string) error { func (fi *FileInfoStore) getShareID(payload string) (string, error) { if len(payload) == 0 { - return "", ErrEmpty + return "", db.ErrEmpty } for i := 0; i < maxHashingTime; i++ { @@ -183,24 +184,24 @@ func (fi *FileInfoStore) getShareID(payload string) (string, error) { } } - return "", ErrConflicted + return "", db.ErrConflicted } -func (fi *FileInfoStore) GetSharingDir(hashID string) (string, error) { +func (fi *FileInfoStore) GetSharingDir(ctx context.Context, hashID string) (string, error) { dirPath, ok := fi.store.GetStringIn(db.ShareIDNs, hashID) if !ok { - return "", ErrSharingNotFound + return "", db.ErrSharingNotFound } return dirPath, nil } -func (fi *FileInfoStore) AddSharing(dirPath string) error { +func (fi *FileInfoStore) AddSharing(ctx context.Context, dirPath string) error { fi.mtx.Lock() defer fi.mtx.Unlock() info, err := fi.getInfo(dirPath) if err != nil { - if !errors.Is(err, ErrNotFound) { + if !errors.Is(err, db.ErrFileInfoNotFound) { return err } info = &db.FileInfo{ @@ -223,7 +224,7 @@ func (fi *FileInfoStore) AddSharing(dirPath string) error { return fi.setInfo(dirPath, info) } -func (fi *FileInfoStore) DelSharing(dirPath string) error { +func (fi *FileInfoStore) DelSharing(ctx context.Context, dirPath string) error { fi.mtx.Lock() defer fi.mtx.Unlock() @@ -257,7 +258,7 @@ func (fi *FileInfoStore) DelSharing(dirPath string) error { return fi.setInfo(dirPath, info) } -func (fi *FileInfoStore) GetSharing(dirPath string) (bool, bool) { +func (fi *FileInfoStore) GetSharing(ctx context.Context, dirPath string) (bool, bool) { fi.mtx.Lock() defer fi.mtx.Unlock() @@ -269,7 +270,7 @@ func (fi *FileInfoStore) GetSharing(dirPath string) (bool, bool) { return info.IsDir && info.Shared, true } -func (fi *FileInfoStore) ListSharings(prefix string) (map[string]string, error) { +func (fi *FileInfoStore) ListSharings(ctx context.Context, prefix string) (map[string]string, error) { infoStrs, err := fi.store.ListStringsByPrefixIn(prefix, db.FileInfoNs) if err != nil { return nil, err diff --git a/src/db/fileinfostore/upload_info_store.go b/src/db/fileinfostore/upload_info_store.go index a66768c..3c815bb 100644 --- a/src/db/fileinfostore/upload_info_store.go +++ b/src/db/fileinfostore/upload_info_store.go @@ -1,6 +1,7 @@ package fileinfostore import ( + "context" "encoding/json" "errors" @@ -36,7 +37,7 @@ func (fi *FileInfoStore) setUploadInfo(user, filePath string, info *db.UploadInf return fi.store.SetStringIn(db.UploadNS(user), filePath, string(newInfoBytes)) } -func (fi *FileInfoStore) AddUploadInfo(user, filePath, tmpPath string, fileSize int64) error { +func (fi *FileInfoStore) AddUploadInfo(ctx context.Context, user, filePath, tmpPath string, fileSize int64) error { fi.mtx.Lock() defer fi.mtx.Unlock() @@ -58,7 +59,7 @@ func (fi *FileInfoStore) AddUploadInfo(user, filePath, tmpPath string, fileSize }) } -func (fi *FileInfoStore) SetUploadInfo(user, filePath string, newUploaded int64) error { +func (fi *FileInfoStore) SetUploadInfo(ctx context.Context, user, filePath string, newUploaded int64) error { fi.mtx.Lock() defer fi.mtx.Unlock() @@ -76,18 +77,18 @@ func (fi *FileInfoStore) SetUploadInfo(user, filePath string, newUploaded int64) }) } -func (fi *FileInfoStore) GetUploadInfo(user, filePath string) (string, int64, int64, error) { +func (fi *FileInfoStore) GetUploadInfo(ctx context.Context, user, filePath string) (string, int64, int64, error) { fi.mtx.Lock() defer fi.mtx.Unlock() return fi.getUploadInfo(user, filePath) } -func (fi *FileInfoStore) DelUploadInfo(user, filePath string) error { +func (fi *FileInfoStore) DelUploadInfo(ctx context.Context, user, filePath string) error { return fi.store.DelInt64In(db.UploadNS(user), filePath) } -func (fi *FileInfoStore) ListUploadInfo(user string) ([]*db.UploadInfo, error) { +func (fi *FileInfoStore) ListUploadInfo(ctx context.Context, user string) ([]*db.UploadInfo, error) { ns := db.UploadNS(user) if !fi.store.HasNamespace(ns) { return nil, nil diff --git a/src/db/interface.go b/src/db/interface.go new file mode 100644 index 0000000..0898a5e --- /dev/null +++ b/src/db/interface.go @@ -0,0 +1,92 @@ +package db + +import ( + "context" + "database/sql" + + _ "github.com/mattn/go-sqlite3" +) + +// TODO: expose more APIs if needed +type IDB interface { + BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) + Close() error + PingContext(ctx context.Context) error + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row + // Conn(ctx context.Context) (*Conn, error) + // Driver() driver.Driver + // SetConnMaxIdleTime(d time.Duration) + // SetConnMaxLifetime(d time.Duration) + // SetMaxIdleConns(n int) + // SetMaxOpenConns(n int) + // Stats() DBStats +} + +type IDBFunctions interface { + Init(ctx context.Context, adminName, adminPwd string, config *SiteConfig) error + IDBLockable + IUserDB + IFileDB + IUploadDB + ISharingDB + IConfigDB +} + +type IDBLockable interface { + Lock() + Unlock() + RLock() + RUnlock() +} + +type IUserDB interface { + 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) + SetPwd(ctx context.Context, id uint64, pwd string) error + SetInfo(ctx context.Context, id uint64, user *User) error + SetPreferences(ctx context.Context, id uint64, prefers *Preferences) error + SetUsed(ctx context.Context, id uint64, incr bool, capacity int64) error + ResetUsed(ctx context.Context, id uint64, used int64) error + ListUsers(ctx context.Context) ([]*User, error) + ListUserIDs(ctx context.Context) (map[string]string, error) + AddRole(role string) error + DelRole(role string) error + ListRoles() (map[string]bool, error) +} + +type IFileDB interface { + AddFileInfo(ctx context.Context, userId uint64, itemPath string, info *FileInfo) error + // DelFileInfo(ctx context.Context, itemPath string) error + DelFileInfo(ctx context.Context, userId uint64, itemPath string) error + GetFileInfo(ctx context.Context, userId uint64, itemPath string) (*FileInfo, error) + SetSha1(ctx context.Context, userId uint64, itemPath, sign string) error + MoveFileInfos(ctx context.Context, userID uint64, oldPath, newPath string, isDir bool) error + ListFileInfos(ctx context.Context, itemPaths []string) (map[string]*FileInfo, error) +} +type IUploadDB interface { + AddUploadInfos(ctx context.Context, userId uint64, tmpPath, filePath string, info *FileInfo) error + DelUploadingInfos(ctx context.Context, userId uint64, realPath string) error + // MoveUploadingInfos(ctx context.Context, userId uint64, uploadPath, itemPath string) error + SetUploadInfo(ctx context.Context, user uint64, filePath string, newUploaded int64) error + GetUploadInfo(ctx context.Context, userId uint64, filePath string) (string, int64, int64, error) + ListUploadInfos(ctx context.Context, user uint64) ([]*UploadInfo, error) +} + +type ISharingDB interface { + IsSharing(ctx context.Context, userId uint64, dirPath string) bool + GetSharingDir(ctx context.Context, hashID string) (string, error) + AddSharing(ctx context.Context, userId uint64, dirPath string) error + DelSharing(ctx context.Context, userId uint64, dirPath string) error + ListUserSharings(ctx context.Context, userId uint64) (map[string]string, error) +} + +type IConfigDB interface { + SetClientCfg(ctx context.Context, cfg *ClientConfig) error + GetCfg(ctx context.Context) (*SiteConfig, error) +} diff --git a/src/db/rdb/interface.go b/src/db/rdb/interface.go deleted file mode 100644 index 63793cd..0000000 --- a/src/db/rdb/interface.go +++ /dev/null @@ -1,25 +0,0 @@ -package rdb - -import ( - "context" - "database/sql" - _ "github.com/mattn/go-sqlite3" -) - -// TODO: expose more APIs if needed -type IDB interface { - BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) - Close() error - PingContext(ctx context.Context) error - PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) - ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) - QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) - QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row - // Conn(ctx context.Context) (*Conn, error) - // Driver() driver.Driver - // SetConnMaxIdleTime(d time.Duration) - // SetConnMaxLifetime(d time.Duration) - // SetMaxIdleConns(n int) - // SetMaxOpenConns(n int) - // Stats() DBStats -} diff --git a/src/db/rdb/sqlite/configs.go b/src/db/rdb/sqlite/configs.go new file mode 100644 index 0000000..1d69f64 --- /dev/null +++ b/src/db/rdb/sqlite/configs.go @@ -0,0 +1,72 @@ +package sqlite + +import ( + "context" + "encoding/json" + + "github.com/ihexxa/quickshare/src/db" +) + +func (st *SQLiteStore) getCfg(ctx context.Context) (*db.SiteConfig, error) { + var configStr string + err := st.db.QueryRowContext( + ctx, + `select config + from t_config + where id=0`, + ).Scan(&configStr) + if err != nil { + return nil, err + } + + config := &db.SiteConfig{} + err = json.Unmarshal([]byte(configStr), config) + if err != nil { + return nil, err + } + + if err = db.CheckSiteCfg(config, true); err != nil { + return nil, err + } + return config, nil +} + +func (st *SQLiteStore) setCfg(ctx context.Context, cfg *db.SiteConfig) error { + if err := db.CheckSiteCfg(cfg, false); err != nil { + return err + } + + cfgBytes, err := json.Marshal(cfg) + if err != nil { + return err + } + + _, err = st.db.ExecContext( + ctx, + `update t_config + set config=? + where id=0`, + string(cfgBytes), + ) + return err +} + +func (st *SQLiteStore) SetClientCfg(ctx context.Context, cfg *db.ClientConfig) error { + st.Lock() + defer st.Unlock() + + siteCfg, err := st.getCfg(ctx) + if err != nil { + return err + } + siteCfg.ClientCfg = cfg + + return st.setCfg(ctx, siteCfg) +} + +func (st *SQLiteStore) GetCfg(ctx context.Context) (*db.SiteConfig, error) { + st.RLock() + defer st.RUnlock() + + return st.getCfg(ctx) +} diff --git a/src/db/rdb/sqlite/files.go b/src/db/rdb/sqlite/files.go new file mode 100644 index 0000000..ff29ea3 --- /dev/null +++ b/src/db/rdb/sqlite/files.go @@ -0,0 +1,276 @@ +package sqlite + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "path" + "strings" + + "github.com/ihexxa/quickshare/src/db" +) + +const ( + InitNs = "Init" + InitTimeKey = "initTime" + SchemaVerKey = "SchemaVersion" + SchemaV1 = "v1" +) + +var ( + maxHashingTime = 10 +) + +func (st *SQLiteStore) getFileInfo(ctx context.Context, userId uint64, itemPath string) (*db.FileInfo, error) { + var infoStr string + fInfo := &db.FileInfo{} + var isDir bool + var size int64 + var shareId string + err := st.db.QueryRowContext( + ctx, + `select is_dir, size, share_id, info + from t_file_info + where path=? and user=? + `, + itemPath, + userId, + ).Scan( + &isDir, + &size, + &shareId, + &infoStr, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, db.ErrFileInfoNotFound + } + return nil, err + } + + err = json.Unmarshal([]byte(infoStr), &fInfo) + if err != nil { + return nil, err + } + fInfo.IsDir = isDir + fInfo.Size = size + fInfo.ShareID = shareId + fInfo.Shared = shareId != "" + return fInfo, nil +} + +func (st *SQLiteStore) GetFileInfo(ctx context.Context, userId uint64, itemPath string) (*db.FileInfo, error) { + st.RLock() + defer st.RUnlock() + + return st.getFileInfo(ctx, userId, itemPath) +} + +func (st *SQLiteStore) ListFileInfos(ctx context.Context, itemPaths []string) (map[string]*db.FileInfo, error) { + st.RLock() + defer st.RUnlock() + + // TODO: add pagination + placeholders := []string{} + values := []any{} + for i := 0; i < len(itemPaths); i++ { + placeholders = append(placeholders, "?") + values = append(values, itemPaths[i]) + } + rows, err := st.db.QueryContext( + ctx, + fmt.Sprintf( + `select path, is_dir, size, share_id, info + from t_file_info + where path in (%s) + `, + strings.Join(placeholders, ","), + ), + values..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var fInfoStr, itemPath, shareId string + var isDir bool + var size int64 + fInfos := map[string]*db.FileInfo{} + for rows.Next() { + fInfo := &db.FileInfo{} + + err = rows.Scan(&itemPath, &isDir, &size, &shareId, &fInfoStr) + if err != nil { + return nil, err + } + + err = json.Unmarshal([]byte(fInfoStr), fInfo) + if err != nil { + return nil, err + } + fInfo.IsDir = isDir + fInfo.Size = size + fInfo.ShareID = shareId + fInfo.Shared = shareId != "" + fInfos[itemPath] = fInfo + } + if rows.Err() != nil { + return nil, rows.Err() + } + + return fInfos, nil +} + +func (st *SQLiteStore) addFileInfo(ctx context.Context, userId uint64, itemPath string, info *db.FileInfo) error { + infoStr, err := json.Marshal(info) + if err != nil { + return err + } + + dirPath, itemName := path.Split(itemPath) + _, err = st.db.ExecContext( + ctx, + `insert into t_file_info + (path, user, parent, name, is_dir, size, share_id, info) values (?, ?, ?, ?, ?, ?, ?, ?)`, + itemPath, + userId, + dirPath, + itemName, + info.IsDir, + info.Size, + info.ShareID, + infoStr, + ) + return err +} + +func (st *SQLiteStore) AddFileInfo(ctx context.Context, userId uint64, itemPath string, info *db.FileInfo) error { + st.Lock() + defer st.Unlock() + + err := st.addFileInfo(ctx, userId, itemPath, info) + if err != nil { + return err + } + + // increase used space + return st.setUsed(ctx, userId, true, info.Size) +} + +func (st *SQLiteStore) delFileInfo(ctx context.Context, userId uint64, itemPath string) error { + _, err := st.db.ExecContext( + ctx, + `delete from t_file_info + where path=? and user=? + `, + itemPath, + userId, + ) + return err +} + +// func (st *SQLiteStore) DelFileInfo(ctx context.Context, itemPath string) error { +// st.Lock() +// defer st.Unlock() +// return st.delFileInfo(ctx, itemPath) +// } + +// sharings + +func (st *SQLiteStore) SetSha1(ctx context.Context, userId uint64, itemPath, sign string) error { + st.Lock() + defer st.Unlock() + + info, err := st.getFileInfo(ctx, userId, itemPath) + if err != nil { + return err + } + info.Sha1 = sign + + infoStr, err := json.Marshal(info) + if err != nil { + return err + } + + _, err = st.db.ExecContext( + ctx, + `update t_file_info + set info=? + where path=? and user=?`, + infoStr, + itemPath, + userId, + ) + return err +} + +func (st *SQLiteStore) DelFileInfo(ctx context.Context, userID uint64, itemPath string) error { + st.Lock() + defer st.Unlock() + + // get all children and size + rows, err := st.db.QueryContext( + ctx, + `select path, size + from t_file_info + where path = ? or path like ? + `, + itemPath, + fmt.Sprintf("%s/%%", itemPath), + ) + if err != nil { + return err + } + defer rows.Close() + + var childrenPath string + var itemSize int64 + placeholders := []string{} + values := []any{} + decrSize := int64(0) + for rows.Next() { + err = rows.Scan(&childrenPath, &itemSize) + if err != nil { + return err + } + placeholders = append(placeholders, "?") + values = append(values, childrenPath) + decrSize += itemSize + } + + // decrease used space + err = st.setUsed(ctx, userID, false, decrSize) + if err != nil { + return err + } + + // delete file info entries + _, err = st.db.ExecContext( + ctx, + fmt.Sprintf( + `delete from t_file_info + where path in (%s)`, + strings.Join(placeholders, ","), + ), + values..., + ) + return err +} + +func (st *SQLiteStore) MoveFileInfos(ctx context.Context, userId uint64, oldPath, newPath string, isDir bool) error { + st.Lock() + defer st.Unlock() + + info, err := st.getFileInfo(ctx, userId, oldPath) + if err != nil { + return err + } + err = st.delFileInfo(ctx, userId, oldPath) + if err != nil { + return err + } + return st.addFileInfo(ctx, userId, newPath, info) +} diff --git a/src/db/rdb/sqlite/files_sharings.go b/src/db/rdb/sqlite/files_sharings.go new file mode 100644 index 0000000..5e8e691 --- /dev/null +++ b/src/db/rdb/sqlite/files_sharings.go @@ -0,0 +1,156 @@ +package sqlite + +import ( + "context" + "crypto/sha1" + "database/sql" + "encoding/json" + "errors" + "fmt" + "io" + "path" + "time" + + "github.com/ihexxa/quickshare/src/db" +) + +func (st *SQLiteStore) generateShareID(payload string) (string, error) { + if len(payload) == 0 { + return "", db.ErrEmpty + } + + msg := fmt.Sprintf("%s-%d", payload, time.Now().Unix()) + h := sha1.New() + _, err := io.WriteString(h, msg) + if err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil))[:7], nil +} + +func (st *SQLiteStore) IsSharing(ctx context.Context, userId uint64, dirPath string) bool { + st.RLock() + defer st.RUnlock() + + // TODO: differentiate error and not exist + info, err := st.getFileInfo(ctx, userId, dirPath) + if err != nil { + return false + } + return info.ShareID != "" +} + +func (st *SQLiteStore) GetSharingDir(ctx context.Context, hashID string) (string, error) { + st.RLock() + defer st.RUnlock() + + var sharedPath string + err := st.db.QueryRowContext( + ctx, + `select path + from t_file_info + where share_id=? + `, + hashID, + ).Scan( + &sharedPath, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return "", db.ErrSharingNotFound + } + return "", err + } + + return sharedPath, nil +} + +func (st *SQLiteStore) AddSharing(ctx context.Context, userId uint64, dirPath string) error { + st.Lock() + defer st.Unlock() + + shareID, err := st.generateShareID(dirPath) + if err != nil { + return err + } + + _, err = st.getFileInfo(ctx, userId, dirPath) + if err != nil && !errors.Is(err, db.ErrFileInfoNotFound) { + return err + } + + if errors.Is(err, db.ErrFileInfoNotFound) { + // insert new + parentPath, name := path.Split(dirPath) + info := &db.FileInfo{Shared: true} // TODO: deprecate shared in info + infoStr, err := json.Marshal(info) + if err != nil { + return err + } + + _, err = st.db.ExecContext( + ctx, + `insert into t_file_info + (path, user, parent, name, is_dir, size, share_id, info) values (?, ?, ?, ?, ?, ?, ?, ?)`, + dirPath, userId, parentPath, name, true, 0, shareID, infoStr, + ) + return err + } + + _, err = st.db.ExecContext( + ctx, + `update t_file_info + set share_id=? + where path=? and user=?`, + shareID, dirPath, userId, + ) + return err +} + +func (st *SQLiteStore) DelSharing(ctx context.Context, userId uint64, dirPath string) error { + st.Lock() + defer st.Unlock() + + _, err := st.db.ExecContext( + ctx, + `update t_file_info + set share_id='' + where path=? and user=?`, + dirPath, + userId, + ) + return err +} + +func (st *SQLiteStore) ListUserSharings(ctx context.Context, userId uint64) (map[string]string, error) { + st.RLock() + defer st.RUnlock() + + rows, err := st.db.QueryContext( + ctx, + `select path, share_id + from t_file_info + where user=? and share_id <> ''`, + userId, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var pathname, shareId string + pathToShareId := map[string]string{} + for rows.Next() { + err = rows.Scan(&pathname, &shareId) + if err != nil { + return nil, err + } + pathToShareId[pathname] = shareId + } + if rows.Err() != nil { + return nil, rows.Err() + } + + return pathToShareId, nil +} diff --git a/src/db/rdb/sqlite/files_uploadings.go b/src/db/rdb/sqlite/files_uploadings.go new file mode 100644 index 0000000..6bf3db7 --- /dev/null +++ b/src/db/rdb/sqlite/files_uploadings.go @@ -0,0 +1,192 @@ +package sqlite + +import ( + "context" + "database/sql" + "errors" + + "github.com/ihexxa/quickshare/src/db" +) + +func (st *SQLiteStore) addUploadInfoOnly(ctx context.Context, userId uint64, filePath, tmpPath string, fileSize int64) error { + _, err := st.db.ExecContext( + ctx, + `insert into t_file_uploading + (real_path, tmp_path, user, size, uploaded) values (?, ?, ?, ?, ?)`, + filePath, tmpPath, userId, fileSize, 0, + ) + return err +} + +func (st *SQLiteStore) AddUploadInfos(ctx context.Context, userId uint64, tmpPath, filePath string, info *db.FileInfo) error { + st.Lock() + defer st.Unlock() + + userInfo, err := st.getUser(ctx, userId) + if err != nil { + return err + } else if userInfo.UsedSpace+info.Size > int64(userInfo.Quota.SpaceLimit) { + return db.ErrQuota + } + + _, _, _, err = st.getUploadInfo(ctx, userId, filePath) + if err == nil { + return db.ErrKeyExisting + } else if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + userInfo.UsedSpace += info.Size + err = st.setUser(ctx, userInfo) + if err != nil { + return err + } + + return st.addUploadInfoOnly(ctx, userId, filePath, tmpPath, info.Size) +} + +func (st *SQLiteStore) DelUploadingInfos(ctx context.Context, userId uint64, realPath string) error { + st.Lock() + defer st.Unlock() + + return st.delUploadingInfos(ctx, userId, realPath) +} + +func (st *SQLiteStore) delUploadingInfos(ctx context.Context, userId uint64, realPath string) error { + _, size, _, err := st.getUploadInfo(ctx, userId, realPath) + if err != nil { + // info may not exist + return err + } + + err = st.delUploadInfoOnly(ctx, userId, realPath) + if err != nil { + return err + } + + userInfo, err := st.getUser(ctx, userId) + if err != nil { + return err + } + userInfo.UsedSpace -= size + return st.setUser(ctx, userInfo) +} + +func (st *SQLiteStore) delUploadInfoOnly(ctx context.Context, userId uint64, filePath string) error { + _, err := st.db.ExecContext( + ctx, + `delete from t_file_uploading + where real_path=? and user=?`, + filePath, userId, + ) + return err +} + +// func (st *SQLiteStore) MoveUploadingInfos(ctx context.Context, userId uint64, uploadPath, itemPath string) error { +// st.Lock() +// defer st.Unlock() + +// _, size, _, err := st.getUploadInfo(ctx, userId, itemPath) +// if err != nil { +// return err +// } +// err = st.delUploadInfoOnly(ctx, userId, itemPath) +// if err != nil { +// return err +// } +// return st.addFileInfo(ctx, userId, itemPath, &db.FileInfo{ +// Size: size, +// }) +// } + +func (st *SQLiteStore) SetUploadInfo(ctx context.Context, userId uint64, filePath string, newUploaded int64) error { + st.Lock() + defer st.Unlock() + + var size, uploaded int64 + err := st.db.QueryRowContext( + ctx, + `select size, uploaded + from t_file_uploading + where real_path=? and user=?`, + filePath, userId, + ).Scan(&size, &uploaded) + if err != nil { + return err + } else if newUploaded > size { + return db.ErrGreaterThanSize + } + + _, err = st.db.ExecContext( + ctx, + `update t_file_uploading + set uploaded=? + where real_path=? and user=?`, + newUploaded, filePath, userId, + ) + return err +} + +func (st *SQLiteStore) getUploadInfo(ctx context.Context, userId uint64, filePath string) (string, int64, int64, error) { + var size, uploaded int64 + err := st.db.QueryRowContext( + ctx, + `select size, uploaded + from t_file_uploading + where user=? and real_path=?`, + userId, filePath, + ).Scan(&size, &uploaded) + if err != nil { + return "", 0, 0, err + } + + return filePath, size, uploaded, nil +} + +func (st *SQLiteStore) GetUploadInfo(ctx context.Context, userId uint64, filePath string) (string, int64, int64, error) { + st.RLock() + defer st.RUnlock() + return st.getUploadInfo(ctx, userId, filePath) +} + +func (st *SQLiteStore) ListUploadInfos(ctx context.Context, userId uint64) ([]*db.UploadInfo, error) { + st.RLock() + defer st.RUnlock() + + rows, err := st.db.QueryContext( + ctx, + `select real_path, size, uploaded + from t_file_uploading + where user=?`, + userId, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var pathname string + var size, uploaded int64 + infos := []*db.UploadInfo{} + for rows.Next() { + err = rows.Scan( + &pathname, + &size, + &uploaded, + ) + if err != nil { + return nil, err + } + + infos = append(infos, &db.UploadInfo{ + RealFilePath: pathname, + Size: size, + Uploaded: uploaded, + }) + } + if rows.Err() != nil { + return nil, rows.Err() + } + + return infos, nil +} diff --git a/src/db/rdb/sqlite/sqlite.go b/src/db/rdb/sqlite/sqlite.go index 82bccfa..6c033f1 100644 --- a/src/db/rdb/sqlite/sqlite.go +++ b/src/db/rdb/sqlite/sqlite.go @@ -1,16 +1,21 @@ package sqlite import ( + "context" "database/sql" + "encoding/json" "fmt" - _ "github.com/mattn/go-sqlite3" + "sync" + "time" - "github.com/ihexxa/quickshare/src/db/rdb" + "github.com/ihexxa/quickshare/src/db" + _ "github.com/mattn/go-sqlite3" ) type SQLite struct { - rdb.IDB + db.IDB dbPath string + mtx *sync.RWMutex } func NewSQLite(dbPath string) (*SQLite, error) { @@ -24,3 +29,159 @@ func NewSQLite(dbPath string) (*SQLite, error) { dbPath: dbPath, }, nil } + +func NewSQLiteStore(db db.IDB) (*SQLiteStore, error) { + return &SQLiteStore{ + db: db, + mtx: &sync.RWMutex{}, + }, nil +} + +func (st *SQLiteStore) Lock() { + st.mtx.Lock() +} + +func (st *SQLiteStore) Unlock() { + st.mtx.Unlock() +} + +func (st *SQLiteStore) RLock() { + st.mtx.RLock() +} + +func (st *SQLiteStore) RUnlock() { + st.mtx.RUnlock() +} + +func (st *SQLiteStore) IsInited() bool { + // always try to init the db + return false +} + +func (st *SQLiteStore) Init(ctx context.Context, rootName, rootPwd string, cfg *db.SiteConfig) error { + err := st.InitUserTable(ctx, rootName, rootPwd) + if err != nil { + return err + } + + if err = st.InitFileTables(ctx); err != nil { + return err + } + + return st.InitConfigTable(ctx, cfg) +} + +func (st *SQLiteStore) InitUserTable(ctx context.Context, rootName, rootPwd string) error { + _, err := st.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: db.VisitorID, + Name: db.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 = st.AddUser(ctx, user) + if err != nil { + return err + } + } + + return nil +} + +func (st *SQLiteStore) InitFileTables(ctx context.Context) error { + _, err := st.db.ExecContext( + ctx, + `create table if not exists t_file_info ( + path varchar not null, + user bigint not null, + parent varchar not null, + name varchar not null, + is_dir boolean not null, + size bigint not null, + share_id varchar not null, + info varchar not null, + primary key(path) + )`, + ) + if err != nil { + return err + } + + _, err = st.db.ExecContext( + ctx, + `create table if not exists t_file_uploading ( + real_path varchar not null, + tmp_path varchar not null, + user bigint not null, + size bigint not null, + uploaded bigint not null, + primary key(real_path) + )`, + ) + return err +} + +func (st *SQLiteStore) InitConfigTable(ctx context.Context, cfg *db.SiteConfig) error { + st.Lock() + defer st.Unlock() + + _, err := st.db.ExecContext( + ctx, + `create table if not exists t_config ( + id bigint not null, + config varchar not null, + modified datetime not null, + primary key(id) + )`, + ) + if err != nil { + return err + } + + cfgStr, err := json.Marshal(cfg) + if err != nil { + return err + } + _, err = st.db.ExecContext( + ctx, + `insert into t_config + (id, config, modified) values (?, ?, ?)`, + 0, cfgStr, time.Now(), + ) + return err +} diff --git a/src/db/rdb/sqlite/users.go b/src/db/rdb/sqlite/users.go index b757722..6a68dd1 100644 --- a/src/db/rdb/sqlite/users.go +++ b/src/db/rdb/sqlite/users.go @@ -5,97 +5,18 @@ import ( "database/sql" "encoding/json" "errors" - // "errors" "fmt" - // "sync" - // "time" + "sync" "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 +type SQLiteStore struct { + db db.IDB + mtx *sync.RWMutex } -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 { +func (st *SQLiteStore) setUser(ctx context.Context, user *db.User) error { var err error if err = db.CheckUser(user, false); err != nil { return err @@ -109,7 +30,7 @@ func (u *SQLiteUsers) setUser(ctx context.Context, tx *sql.Tx, user *db.User) er if err != nil { return err } - _, err = tx.ExecContext( + _, err = st.db.ExecContext( ctx, `update t_user set name=?, pwd=?, role=?, used_space=?, quota=?, preference=? @@ -120,16 +41,15 @@ func (u *SQLiteUsers) setUser(ctx context.Context, tx *sql.Tx, user *db.User) er user.UsedSpace, quotaStr, preferencesStr, + user.ID, ) return err } -func (u *SQLiteUsers) getUser(ctx context.Context, tx *sql.Tx, id uint64) (*db.User, error) { - var err error - +func (st *SQLiteStore) getUser(ctx context.Context, id uint64) (*db.User, error) { user := &db.User{} var quotaStr, preferenceStr string - err = tx.QueryRowContext( + err := st.db.QueryRowContext( ctx, `select id, name, pwd, role, used_space, quota, preference from t_user @@ -146,7 +66,7 @@ func (u *SQLiteUsers) getUser(ctx context.Context, tx *sql.Tx, id uint64) (*db.U ) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, ErrUserNotFound + return nil, db.ErrUserNotFound } return nil, err } @@ -162,7 +82,10 @@ func (u *SQLiteUsers) getUser(ctx context.Context, tx *sql.Tx, id uint64) (*db.U return user, nil } -func (u *SQLiteUsers) AddUser(ctx context.Context, user *db.User) error { +func (st *SQLiteStore) AddUser(ctx context.Context, user *db.User) error { + st.Lock() + defer st.Unlock() + quotaStr, err := json.Marshal(user.Quota) if err != nil { return err @@ -171,7 +94,7 @@ func (u *SQLiteUsers) AddUser(ctx context.Context, user *db.User) error { if err != nil { return err } - _, err = u.db.ExecContext( + _, err = st.db.ExecContext( ctx, `insert into t_user (id, name, pwd, role, used_space, quota, preference) values (?, ?, ?, ?, ?, ?, ?)`, user.ID, @@ -185,8 +108,11 @@ func (u *SQLiteUsers) AddUser(ctx context.Context, user *db.User) error { return err } -func (u *SQLiteUsers) DelUser(ctx context.Context, id uint64) error { - _, err := u.db.ExecContext( +func (st *SQLiteStore) DelUser(ctx context.Context, id uint64) error { + st.Lock() + defer st.Unlock() + + _, err := st.db.ExecContext( ctx, `delete from t_user where id=?`, id, @@ -194,36 +120,34 @@ func (u *SQLiteUsers) DelUser(ctx context.Context, id uint64) error { return err } -func (u *SQLiteUsers) GetUser(ctx context.Context, id uint64) (*db.User, error) { - tx, err := u.db.BeginTx(ctx, &sql.TxOptions{}) +func (st *SQLiteStore) GetUser(ctx context.Context, id uint64) (*db.User, error) { + st.RLock() + defer st.RUnlock() + + user, err := st.getUser(ctx, id) 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) { +func (st *SQLiteStore) GetUserByName(ctx context.Context, name string) (*db.User, error) { + st.RLock() + defer st.RUnlock() + user := &db.User{} var quotaStr, preferenceStr string - err := u.db.QueryRowContext( + err := st.db.QueryRowContext( ctx, - `select id, name, role, used_space, quota, preference + `select id, name, pwd, role, used_space, quota, preference from t_user where name=?`, name, ).Scan( &user.ID, &user.Name, + &user.Pwd, &user.Role, &user.UsedSpace, "aStr, @@ -231,7 +155,7 @@ func (u *SQLiteUsers) GetUserByName(ctx context.Context, name string) (*db.User, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, ErrUserNotFound + return nil, db.ErrUserNotFound } return nil, err } @@ -247,8 +171,11 @@ func (u *SQLiteUsers) GetUserByName(ctx context.Context, name string) (*db.User, return user, nil } -func (u *SQLiteUsers) SetPwd(ctx context.Context, id uint64, pwd string) error { - _, err := u.db.ExecContext( +func (st *SQLiteStore) SetPwd(ctx context.Context, id uint64, pwd string) error { + st.Lock() + defer st.Unlock() + + _, err := st.db.ExecContext( ctx, `update t_user set pwd=? @@ -260,13 +187,16 @@ func (u *SQLiteUsers) SetPwd(ctx context.Context, id uint64, pwd string) error { } // role + quota -func (u *SQLiteUsers) SetInfo(ctx context.Context, id uint64, user *db.User) error { +func (st *SQLiteStore) SetInfo(ctx context.Context, id uint64, user *db.User) error { + st.Lock() + defer st.Unlock() + quotaStr, err := json.Marshal(user.Quota) if err != nil { return err } - _, err = u.db.ExecContext( + _, err = st.db.ExecContext( ctx, `update t_user set role=?, quota=? @@ -277,13 +207,16 @@ func (u *SQLiteUsers) SetInfo(ctx context.Context, id uint64, user *db.User) err return err } -func (u *SQLiteUsers) SetPreferences(ctx context.Context, id uint64, prefers *db.Preferences) error { +func (st *SQLiteStore) SetPreferences(ctx context.Context, id uint64, prefers *db.Preferences) error { + st.Lock() + defer st.Unlock() + preferenceStr, err := json.Marshal(prefers) if err != nil { return err } - _, err = u.db.ExecContext( + _, err = st.db.ExecContext( ctx, `update t_user set preference=? @@ -294,31 +227,32 @@ func (u *SQLiteUsers) SetPreferences(ctx context.Context, id uint64, prefers *db 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 - } +func (st *SQLiteStore) SetUsed(ctx context.Context, id uint64, incr bool, capacity int64) error { + st.Lock() + defer st.Unlock() + return st.setUsed(ctx, id, incr, capacity) +} - gotUser, err := u.getUser(ctx, tx, id) +func (st *SQLiteStore) setUsed(ctx context.Context, id uint64, incr bool, capacity int64) error { + gotUser, err := st.getUser(ctx, id) if err != nil { return err } if incr && gotUser.UsedSpace+capacity > int64(gotUser.Quota.SpaceLimit) { - return ErrReachedLimit + return db.ErrReachedLimit } if incr { gotUser.UsedSpace = gotUser.UsedSpace + capacity } else { if gotUser.UsedSpace-capacity < 0 { - return ErrNegtiveUsedSpace + return db.ErrNegtiveUsedSpace } gotUser.UsedSpace = gotUser.UsedSpace - capacity } - _, err = tx.ExecContext( + _, err = st.db.ExecContext( ctx, `update t_user set used_space=? @@ -330,11 +264,14 @@ func (u *SQLiteUsers) SetUsed(ctx context.Context, id uint64, incr bool, capacit return err } - return tx.Commit() + return nil } -func (u *SQLiteUsers) ResetUsed(ctx context.Context, id uint64, used int64) error { - _, err := u.db.ExecContext( +func (st *SQLiteStore) ResetUsed(ctx context.Context, id uint64, used int64) error { + st.Lock() + defer st.Unlock() + + _, err := st.db.ExecContext( ctx, `update t_user set used_space=? @@ -345,16 +282,19 @@ func (u *SQLiteUsers) ResetUsed(ctx context.Context, id uint64, used int64) erro return err } -func (u *SQLiteUsers) ListUsers(ctx context.Context) ([]*db.User, error) { +func (st *SQLiteStore) ListUsers(ctx context.Context) ([]*db.User, error) { + st.RLock() + defer st.RUnlock() + // TODO: support pagination - rows, err := u.db.QueryContext( + rows, err := st.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, db.ErrUserNotFound } return nil, err } @@ -389,8 +329,11 @@ func (u *SQLiteUsers) ListUsers(ctx context.Context) ([]*db.User, error) { return users, nil } -func (u *SQLiteUsers) ListUserIDs(ctx context.Context) (map[string]string, error) { - users, err := u.ListUsers(ctx) +func (st *SQLiteStore) ListUserIDs(ctx context.Context) (map[string]string, error) { + st.RLock() + defer st.RUnlock() + + users, err := st.ListUsers(ctx) if err != nil { return nil, err } @@ -402,17 +345,17 @@ func (u *SQLiteUsers) ListUserIDs(ctx context.Context) (map[string]string, error return nameToId, nil } -func (u *SQLiteUsers) AddRole(role string) error { +func (st *SQLiteStore) AddRole(role string) error { // TODO: implement this after adding grant/revoke panic("not implemented") } -func (u *SQLiteUsers) DelRole(role string) error { +func (st *SQLiteStore) DelRole(role string) error { // TODO: implement this after adding grant/revoke panic("not implemented") } -func (u *SQLiteUsers) ListRoles() (map[string]bool, error) { +func (st *SQLiteStore) ListRoles() (map[string]bool, error) { // TODO: implement this after adding grant/revoke panic("not implemented") } diff --git a/src/db/sitestore/site_store.go b/src/db/sitestore/site_store.go index 2654146..d8812db 100644 --- a/src/db/sitestore/site_store.go +++ b/src/db/sitestore/site_store.go @@ -1,6 +1,7 @@ package sitestore import ( + "context" "encoding/json" "errors" "sync" @@ -79,7 +80,7 @@ func (st *SiteStore) setCfg(cfg *db.SiteConfig) error { return st.store.SetStringIn(NsSite, KeySiteCfg, string(cfgBytes)) } -func (st *SiteStore) SetClientCfg(cfg *db.ClientConfig) error { +func (st *SiteStore) SetClientCfg(ctx context.Context, cfg *db.ClientConfig) error { st.mtx.Lock() defer st.mtx.Unlock() @@ -92,7 +93,7 @@ func (st *SiteStore) SetClientCfg(cfg *db.ClientConfig) error { return st.setCfg(siteCfg) } -func (st *SiteStore) GetCfg() (*db.SiteConfig, error) { +func (st *SiteStore) GetCfg(ctx context.Context) (*db.SiteConfig, error) { st.mtx.RLock() defer st.mtx.RUnlock() diff --git a/src/db/tests/config_test.go b/src/db/tests/config_test.go new file mode 100644 index 0000000..d453dd1 --- /dev/null +++ b/src/db/tests/config_test.go @@ -0,0 +1,126 @@ +package tests + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/ihexxa/quickshare/src/db" + "github.com/ihexxa/quickshare/src/db/rdb/sqlite" + "github.com/ihexxa/quickshare/src/db/sitestore" + "github.com/ihexxa/quickshare/src/kvstore/boltdbpvd" +) + +var testSiteConfig = &db.SiteConfig{ + ClientCfg: &db.ClientConfig{ + SiteName: "", + SiteDesc: "", + AllowSetBg: true, + AutoTheme: false, + Bg: &db.BgConfig{ + Url: "/imgs/bg.jpg", + Repeat: "repeat", + Position: "top", + Align: "scroll", + BgColor: "#000", + }, + }, +} + +func TestSiteStore(t *testing.T) { + testConfigMethods := func(t *testing.T, store db.IConfigDB) { + siteCfg := &db.SiteConfig{ + ClientCfg: &db.ClientConfig{ + SiteName: "quickshare", + SiteDesc: "simpel file sharing", + AllowSetBg: true, + AutoTheme: true, + Bg: &db.BgConfig{ + Url: "/imgs/bg.jpg", + Repeat: "no-repeat", + Position: "center", + Align: "fixed", + BgColor: "#ccc", + }, + }, + } + + ctx := context.TODO() + err := store.SetClientCfg(ctx, siteCfg.ClientCfg) + if err != nil { + t.Fatal(err) + } + newSiteCfg, err := store.GetCfg(ctx) + if err != nil { + t.Fatal(err) + } else if !reflect.DeepEqual(newSiteCfg, siteCfg) { + t.Fatalf("not equal new(%v) original(%v)", newSiteCfg, siteCfg) + } + } + + t.Run("config methods basic tests - boltdb", func(t *testing.T) { + rootPath, err := ioutil.TempDir("./", "quickshare_sitestore_test_") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rootPath) + + dbPath := filepath.Join(rootPath, "quickshare.db") + kvstore := boltdbpvd.New(dbPath, 1024) + defer kvstore.Close() + + store, err := sitestore.NewSiteStore(kvstore) + if err != nil { + t.Fatal("fail to new kvstore", err) + } + err = store.Init(&db.SiteConfig{ + ClientCfg: &db.ClientConfig{ + SiteName: "", + SiteDesc: "", + AllowSetBg: true, + AutoTheme: false, + Bg: &db.BgConfig{ + Url: "/imgs/bg.jpg", + Repeat: "repeat", + Position: "top", + Align: "scroll", + BgColor: "#000", + }, + }, + }) + if err != nil { + panic(err) + } + + testConfigMethods(t, store) + }) + + t.Run("config methods basic tests - sqlite", func(t *testing.T) { + rootPath, err := ioutil.TempDir("./", "qs_sqlite_config_") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rootPath) + + dbPath := filepath.Join(rootPath, "quickshare.sqlite") + sqliteDB, err := sqlite.NewSQLite(dbPath) + if err != nil { + t.Fatal(err) + } + defer sqliteDB.Close() + + store, err := sqlite.NewSQLiteStore(sqliteDB) + if err != nil { + t.Fatal("fail to new sqlite store", err) + } + err = store.Init(context.TODO(), "admin", "adminPwd", testSiteConfig) + if err != nil { + panic(err) + } + + testConfigMethods(t, store) + }) +} diff --git a/src/db/tests/files_test.go b/src/db/tests/files_test.go new file mode 100644 index 0000000..ed4f301 --- /dev/null +++ b/src/db/tests/files_test.go @@ -0,0 +1,446 @@ +package tests + +import ( + "context" + "database/sql" + "errors" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/ihexxa/quickshare/src/db" + "github.com/ihexxa/quickshare/src/db/rdb/sqlite" +) + +type IFilesFunctions interface { + db.IFileDB + db.IUploadDB + db.ISharingDB +} + +func TestFileStore(t *testing.T) { + testSharingMethods := func(t *testing.T, store db.IDBFunctions) { + dirPaths := []string{"admin/path1", "admin/path1/path2"} + var err error + + ctx := context.TODO() + adminId := uint64(0) + + // add some of paths... + err = store.AddFileInfo(ctx, adminId, "admin/path1", &db.FileInfo{ + // Shared: false, // deprecated + IsDir: false, + Size: int64(0), + ShareID: "", + Sha1: "", + }) + if err != nil { + t.Fatal(err) + } + + // add sharings + for _, dirPath := range dirPaths { + err = store.AddSharing(ctx, adminId, dirPath) + if err != nil { + t.Fatal(err) + } + } + + // list sharings + dirToID, err := store.ListUserSharings(ctx, adminId) + if err != nil { + t.Fatal(err) + } else if len(dirToID) != len(dirPaths) { + t.Fatal("share size not match") + } + + for _, sharingDir := range dirPaths { + if _, ok := dirToID[sharingDir]; !ok { + t.Fatalf("sharing(%s) not found", sharingDir) + } + mustTrue := store.IsSharing(ctx, adminId, sharingDir) + if !mustTrue { + t.Fatalf("get sharing(%t) should exist", mustTrue) + } + + info, err := store.GetFileInfo(ctx, adminId, sharingDir) + if err != nil { + t.Fatal(err) + } else if len(info.ShareID) != 7 { + t.Fatalf("incorrect ShareID %s", info.ShareID) + } + + gotSharingDir, err := store.GetSharingDir(ctx, info.ShareID) + if err != nil { + t.Fatal(err) + } else if sharingDir != gotSharingDir { + t.Fatalf("sharing dir not consist: (%s) (%s)", sharingDir, gotSharingDir) + } + } + + // del sharings + for _, dirPath := range dirPaths { + err = store.DelSharing(ctx, adminId, dirPath) + if err != nil { + t.Fatal(err) + } + } + + // list sharings + dirToIDAfterDel, err := store.ListUserSharings(ctx, adminId) + if err != nil { + t.Fatal(err) + } else if len(dirToIDAfterDel) != 0 { + t.Fatalf("share size not match (%+v)", dirToIDAfterDel) + } + + for _, dirPath := range dirPaths { + if _, ok := dirToIDAfterDel[dirPath]; ok { + t.Fatalf("sharing(%s) should not exist", dirPath) + } + shared := store.IsSharing(ctx, adminId, dirPath) + if shared { + t.Fatalf("get sharing(%t) should not shared but exist", shared) + } + + info, err := store.GetFileInfo(ctx, adminId, dirPath) + if err != nil { + t.Fatal(err) + } else if len(info.ShareID) != 0 { + t.Fatalf("ShareID should be empty %s", info.ShareID) + } + + // shareIDs are removed, use original dirToID to get shareID + originalShareID, ok := dirToID[dirPath] + if !ok { + t.Fatalf("dir (%s) should exist in originalShareID", dirPath) + } + + _, err = store.GetSharingDir(ctx, originalShareID) + if !errors.Is(err, db.ErrSharingNotFound) { + t.Fatal("should return ErrSharingNotFound") + } + } + } + + testFileInfoMethods := func(t *testing.T, store db.IDBFunctions) { + pathInfos := map[string]*db.FileInfo{ + "admin/origin/item1": &db.FileInfo{ + // Shared: false, // deprecated + IsDir: false, + Size: int64(7), + ShareID: "", + Sha1: "item1_sha", + }, + "admin/origin/item2": &db.FileInfo{ + // Shared: false, // deprecated + IsDir: false, + Size: int64(3), + ShareID: "", + Sha1: "item2_sha", + }, + "admin/origin/dir": &db.FileInfo{ + // Shared: true, // deprecated + IsDir: true, + Size: int64(0), + ShareID: "mockedShareID", + Sha1: "", + }, + } + var err error + + adminId := uint64(0) + ctx := context.TODO() + // add infos + usedSpace := int64(0) + itemPaths := []string{} + for itemPath, info := range pathInfos { + err = store.AddFileInfo(ctx, adminId, itemPath, info) + if err != nil { + t.Fatal(err) + } + usedSpace += info.Size + itemPaths = append(itemPaths, itemPath) + } + + // get infos + for itemPath, expected := range pathInfos { + info, err := store.GetFileInfo(ctx, adminId, itemPath) + if err != nil { + t.Fatal(err) + } + if info.ShareID != expected.ShareID || + info.IsDir != expected.IsDir || + info.Sha1 != expected.Sha1 || + info.Size != expected.Size { + t.Fatalf("info not equaled (%v) (%v)", expected, info) + } + } + + // list infos + pathToInfo, err := store.ListFileInfos(ctx, itemPaths) + if err != nil { + t.Fatal(err) + } else if len(pathToInfo) != len(pathInfos) { + t.Fatalf("list result size not match (%v) (%d)", pathToInfo, len(pathInfos)) + } + for pathname, info := range pathInfos { + gotInfo, ok := pathToInfo[pathname] + if !ok { + t.Fatalf("path not found (%s)", pathname) + } + if info.ShareID != gotInfo.ShareID || + info.IsDir != gotInfo.IsDir || + info.Sha1 != gotInfo.Sha1 || + info.Size != gotInfo.Size { + t.Fatalf("info not equaled (%v) (%v)", gotInfo, info) + } + } + + // set sha1 + testSha1 := "sha1" + for itemPath := range pathInfos { + err := store.SetSha1(ctx, adminId, itemPath, testSha1) + if err != nil { + t.Fatal(err) + } + info, err := store.GetFileInfo(ctx, adminId, itemPath) + if err != nil { + t.Fatal(err) + } + if info.Sha1 != testSha1 { + t.Fatalf("sha not equaled (%v) (%v)", info.Sha1, testSha1) + } + } + + // move paths + newPaths := []string{} + for itemPath, info := range pathInfos { + newItemPath := strings.ReplaceAll(itemPath, "origin", "new") + err = store.MoveFileInfos(ctx, adminId, itemPath, newItemPath, info.IsDir) + if err != nil { + t.Fatal(err) + } + newPaths = append(newPaths, newItemPath) + } + + // list infos + pathToInfo, err = store.ListFileInfos(ctx, newPaths) + if err != nil { + t.Fatal(err) + } else if len(pathToInfo) != len(pathInfos) { + t.Fatalf("list result size not match (%v) (%d)", pathToInfo, len(pathInfos)) + } + + // check used space + adminInfo, err := store.GetUser(ctx, adminId) + if err != nil { + t.Fatal(err) + } else if adminInfo.UsedSpace != usedSpace { + t.Fatalf("used space not match (%d) (%d)", adminInfo.UsedSpace, usedSpace) + } + + // del info + for _, itemPath := range newPaths { + err = store.DelFileInfo(ctx, adminId, itemPath) + if err != nil { + t.Fatal(err) + } + } + + // check used space + adminInfo, err = store.GetUser(ctx, adminId) + if err != nil { + t.Fatal(err) + } else if adminInfo.UsedSpace != int64(0) { + t.Fatalf("used space not match (%d) (%d)", adminInfo.UsedSpace, int64(0)) + } + + // list infos + pathToInfo, err = store.ListFileInfos(ctx, itemPaths) + if err != nil { + t.Fatal(err) + } else if len(pathToInfo) != 0 { + t.Fatalf("list result should be empty (%v)", pathToInfo) + } + + for itemPath := range pathInfos { + _, err := store.GetFileInfo(ctx, adminId, itemPath) + if !errors.Is(err, db.ErrFileInfoNotFound) { + t.Fatal(err) + } + } + } + + testUploadingMethods := func(t *testing.T, store db.IDBFunctions) { + pathInfos := map[string]*db.FileInfo{ + "admin/origin/item1": &db.FileInfo{ + // Shared: false, // deprecated + IsDir: false, + Size: int64(7), + ShareID: "", + Sha1: "", + }, + "admin/origin/item2": &db.FileInfo{ + // Shared: false, // deprecated + IsDir: false, + Size: int64(3), + ShareID: "", + Sha1: "", + }, + "admin/origin/to_delete/item3": &db.FileInfo{ + // Shared: false, // deprecated + IsDir: false, + Size: int64(11), + ShareID: "", + Sha1: "", + }, + } + var err error + + adminId := uint64(0) + ctx := context.TODO() + + // add infos + usedSpace := int64(0) + usedSpaceAfterDeleting := int64(0) + itemPaths := []string{} + pathToTmpPath := map[string]string{} + for itemPath, info := range pathInfos { + tmpPath := strings.ReplaceAll(itemPath, "origin", "uploads") + pathToTmpPath[itemPath] = tmpPath + err = store.AddUploadInfos(ctx, adminId, tmpPath, itemPath, info) + if err != nil { + t.Fatal(err) + } + usedSpace += info.Size + if !strings.Contains(itemPath, "delete") { + usedSpaceAfterDeleting += info.Size + } + itemPaths = append(itemPaths, itemPath) + } + + // get infos + for itemPath, info := range pathInfos { + gotPath, size, uploaded, err := store.GetUploadInfo(ctx, adminId, itemPath) + if err != nil { + t.Fatal(err) + } + if size != info.Size || + uploaded != int64(0) || + gotPath != itemPath { + t.Fatalf("info not equaled (%v)", info) + } + } + + // list infos + uploadInfos, err := store.ListUploadInfos(ctx, adminId) + if err != nil { + t.Fatal(err) + } else if len(uploadInfos) != len(pathInfos) { + t.Fatalf("list result size not match (%v) (%d)", uploadInfos, len(pathInfos)) + } + for _, uploadInfo := range uploadInfos { + expected, ok := pathInfos[uploadInfo.RealFilePath] + if !ok { + t.Fatalf("path not found (%s)", uploadInfo.RealFilePath) + } + if uploadInfo.Uploaded != int64(0) || + expected.Size != uploadInfo.Size { + t.Fatalf("info not equaled (%d) (%d)", uploadInfo.Uploaded, uploadInfo.Size) + } + } + + // check used space + adminInfo, err := store.GetUser(ctx, adminId) + if err != nil { + t.Fatal(err) + } else if adminInfo.UsedSpace != usedSpace { + t.Fatalf("used space not match (%d) (%d)", adminInfo.UsedSpace, usedSpace) + } + + // set uploading + for itemPath, info := range pathInfos { + err := store.SetUploadInfo(ctx, adminId, itemPath, int64(info.Size/2)) + if err != nil { + t.Fatal(err) + } + gotPath, size, uploaded, err := store.GetUploadInfo(ctx, adminId, itemPath) + if err != nil { + t.Fatal(err) + } + if gotPath != itemPath || size != info.Size || uploaded != info.Size/2 { + t.Fatal("uploaded info not match") + } + } + + // check used space + adminInfo, err = store.GetUser(ctx, adminId) + if err != nil { + t.Fatal(err) + } else if adminInfo.UsedSpace != usedSpace { + t.Fatalf("used space not match (%d) (%d)", adminInfo.UsedSpace, usedSpace) + } + + // del info + for itemPath := range pathInfos { + err = store.DelUploadingInfos(ctx, adminId, itemPath) + if err != nil { + t.Fatal(err) + } + } + + // check used space + adminInfo, err = store.GetUser(ctx, adminId) + if err != nil { + t.Fatal(err) + } else if adminInfo.UsedSpace != int64(0) { + t.Fatalf("used space not match (%d) (%d)", adminInfo.UsedSpace, int64(0)) + } + + // list infos + uploadInfos, err = store.ListUploadInfos(ctx, adminId) + if err != nil { + t.Fatal(err) + } else if len(uploadInfos) != 0 { + t.Fatalf("list result size not match (%v) (%d)", uploadInfos, 0) + } + + for itemPath := range pathInfos { + _, _, _, err := store.GetUploadInfo(ctx, adminId, itemPath) + if !errors.Is(err, sql.ErrNoRows) { + t.Fatal(err) + } + } + } + + t.Run("testing file info store - sqlite", func(t *testing.T) { + rootPath, err := ioutil.TempDir("./", "qs_sqlite_test_") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(rootPath) + + dbPath := filepath.Join(rootPath, "files.sqlite") + sqliteDB, err := sqlite.NewSQLite(dbPath) + if err != nil { + t.Fatal(err) + } + defer sqliteDB.Close() + + store, err := sqlite.NewSQLiteStore(sqliteDB) + if err != nil { + t.Fatal("fail to new sqlite store", err) + } + err = store.Init(context.TODO(), "admin", "1234", testSiteConfig) + if err != nil { + t.Fatal("fail to init", err) + } + + testSharingMethods(t, store) + testFileInfoMethods(t, store) + testUploadingMethods(t, store) + }) +} diff --git a/src/db/rdb/sqlite/users_test.go b/src/db/tests/users_test.go similarity index 81% rename from src/db/rdb/sqlite/users_test.go rename to src/db/tests/users_test.go index bf65407..aa172a0 100644 --- a/src/db/rdb/sqlite/users_test.go +++ b/src/db/tests/users_test.go @@ -1,4 +1,4 @@ -package sqlite +package tests import ( "context" @@ -7,16 +7,20 @@ import ( "path/filepath" "strings" "testing" - "time" "github.com/ihexxa/quickshare/src/db" + "github.com/ihexxa/quickshare/src/db/rdb/sqlite" + // "github.com/ihexxa/quickshare/src/db/userstore" + // "github.com/ihexxa/quickshare/src/kvstore/boltdbpvd" ) -func TestUserStores(t *testing.T) { +func TestUserStore(t *testing.T) { rootName, rootPwd := "root", "rootPwd" - testUserMethods := func(t *testing.T, store db.IUserStore) { + testUserMethods := func(t *testing.T, store db.IUserDB) { ctx := context.TODO() + + // check root root, err := store.GetUser(ctx, 0) if err != nil { t.Fatal(err) @@ -43,11 +47,12 @@ func TestUserStores(t *testing.T) { t.Fatalf("incorrect preference %v %v", root.Preferences, db.DefaultPreferences) } + // check visitor visitor, err := store.GetUser(ctx, 1) if err != nil { t.Fatal(err) } - if visitor.Name != VisitorName { + if visitor.Name != db.VisitorName { t.Fatal("visitor not found") } if visitor.Pwd != rootPwd { @@ -69,6 +74,7 @@ func TestUserStores(t *testing.T) { t.Fatalf("incorrect preference") } + // add users id, name1 := uint64(2), "test_user1" pwd1, pwd2 := "666", "888" role1, role2 := db.UserRole, db.AdminRole @@ -91,6 +97,7 @@ func TestUserStores(t *testing.T) { t.Fatal("there should be no error") } + // compare users user, err := store.GetUser(ctx, id) if err != nil { t.Fatal(err) @@ -114,6 +121,7 @@ func TestUserStores(t *testing.T) { t.Fatalf("down limit not matched %d %d", downLimit1, user.Quota.DownloadSpeedLimit) } + // check listing users, err := store.ListUsers(ctx) if err != nil { t.Fatal(err) @@ -190,7 +198,6 @@ func TestUserStores(t *testing.T) { 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", @@ -219,7 +226,7 @@ func TestUserStores(t *testing.T) { t.Fatalf("ids not matched %d %d", id, user.ID) } if user.Pwd != pwd2 { - t.Fatalf("passwords not match %s", err) + t.Fatalf("passwords not match %s %s", user.Pwd, pwd2) } if user.Role != role2 { t.Fatalf("roles not matched %s %s", role2, user.Role) @@ -252,7 +259,7 @@ func TestUserStores(t *testing.T) { 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 { + if user.ID == db.VisitorID && user.Name != db.VisitorName && user.Role != db.VisitorRole { t.Fatalf("incorrect visitor info %v", user) } } @@ -266,7 +273,49 @@ func TestUserStores(t *testing.T) { } } - t.Run("testing UserStore sqlite", func(t *testing.T) { + // TODO: role functions are disabled temporarily + // testRoleMethods := func(t *testing.T, store db.IUserDB) { + // roles := []string{"role1", "role2"} + // var err error + // for _, role := range roles { + // err = store.AddRole(role) + // if err != nil { + // t.Fatal(err) + // } + // } + + // roleMap, err := store.ListRoles() + // if err != nil { + // t.Fatal(err) + // } + + // for _, role := range append(roles, []string{ + // db.AdminRole, db.UserRole, db.VisitorRole, + // }...) { + // if !roleMap[role] { + // t.Fatalf("role(%s) not found", role) + // } + // } + + // for _, role := range roles { + // err = store.DelRole(role) + // if err != nil { + // t.Fatal(err) + // } + // } + + // roleMap, err = store.ListRoles() + // if err != nil { + // t.Fatal(err) + // } + // for _, role := range roles { + // if roleMap[role] { + // t.Fatalf("role(%s) should not exist", role) + // } + // } + // } + + t.Run("user store crud - sqlite", func(t *testing.T) { rootPath, err := ioutil.TempDir("./", "quickshare_userstore_test_") if err != nil { t.Fatal(err) @@ -274,17 +323,17 @@ func TestUserStores(t *testing.T) { defer os.RemoveAll(rootPath) dbPath := filepath.Join(rootPath, "quickshare.sqlite") - sqliteDB, err := NewSQLite(dbPath) + sqliteDB, err := sqlite.NewSQLite(dbPath) if err != nil { t.Fatal(err) } defer sqliteDB.Close() - store, err := NewSQLiteUsers(sqliteDB) + store, err := sqlite.NewSQLiteStore(sqliteDB) if err != nil { t.Fatal("fail to new user store", err) } - if err = store.Init(context.TODO(), rootName, rootPwd); err != nil { + if err = store.Init(context.TODO(), rootName, rootPwd, testSiteConfig); err != nil { t.Fatal("fail to init user store", err) } diff --git a/src/db/userstore/user_store_test.go b/src/db/userstore/user_store_test.go deleted file mode 100644 index 373814d..0000000 --- a/src/db/userstore/user_store_test.go +++ /dev/null @@ -1,329 +0,0 @@ -package userstore - -import ( - "io/ioutil" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/ihexxa/quickshare/src/db" - "github.com/ihexxa/quickshare/src/kvstore/boltdbpvd" -) - -func TestUserStores(t *testing.T) { - rootName, rootPwd := "root", "rootPwd" - - testUserMethods := func(t *testing.T, store IUserStore) { - root, err := store.GetUser(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", err) - } - 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(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(&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(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() - 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(id, pwd2) - if err != nil { - t.Fatal(err) - } - store.SetInfo(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(id, true, usedIncr) - if err != nil { - t.Fatal(err) - } - err = store.SetUsed(id, false, usedDecr) - if err != nil { - t.Fatal(err) - } - err = store.SetUsed(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(id) - if err != nil { - t.Fatal(err) - } - 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 user.UsedSpace != usedIncr-usedDecr { - t.Fatalf("used space not matched %d %d", user.UsedSpace, usedIncr-usedDecr) - } - - 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(id, newPrefer) - if err != nil { - t.Fatal(err) - } - - user, err = store.GetUserByName(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(id) - if err != nil { - t.Fatal(err) - } - users, err = store.ListUsers() - 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() - 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)) - } - } - - testRoleMethods := func(t *testing.T, store IUserStore) { - roles := []string{"role1", "role2"} - var err error - for _, role := range roles { - err = store.AddRole(role) - if err != nil { - t.Fatal(err) - } - } - - roleMap, err := store.ListRoles() - if err != nil { - t.Fatal(err) - } - - for _, role := range append(roles, []string{ - db.AdminRole, db.UserRole, db.VisitorRole, - }...) { - if !roleMap[role] { - t.Fatalf("role(%s) not found", role) - } - } - - for _, role := range roles { - err = store.DelRole(role) - if err != nil { - t.Fatal(err) - } - } - - roleMap, err = store.ListRoles() - if err != nil { - t.Fatal(err) - } - for _, role := range roles { - if roleMap[role] { - t.Fatalf("role(%s) should not exist", role) - } - } - } - - t.Run("testing KVUserStore", 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.db") - kvstore := boltdbpvd.New(dbPath, 1024) - defer kvstore.Close() - - store, err := NewKVUserStore(kvstore) - if err != nil { - t.Fatal("fail to new kvstore", err) - } - if err = store.Init(rootName, rootPwd); err != nil { - t.Fatal("fail to init kvstore", err) - } - - testUserMethods(t, store) - testRoleMethods(t, store) - }) -}