From 1bef4d027243a5474af9802b95beaf18eb70fefb Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Thu, 4 Nov 2021 12:51:49 +0800 Subject: [PATCH 001/104] Add: docs/problem_description.md --- docs/problem_description.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/problem_description.md diff --git a/docs/problem_description.md b/docs/problem_description.md new file mode 100644 index 0000000..8346d51 --- /dev/null +++ b/docs/problem_description.md @@ -0,0 +1,34 @@ +# DBMS Group Project Problem Description + +- Group 1 + +The Internet infrastructure construction has made the network speed development faster. With the fast Internet, people are gradually migrating various data and services to the cloud. For example, NetEase Cloud Music, Spotify, and Apple Music, we call them streaming media platforms. The definition of streaming media platform is that users purchase the digital copyright of music and then play the music online on the platform. + +Generally speaking, users cannot buy music that is not available on the platform. The user cannot download the digital file of the music (the user purchases the right to play instead of the right to copy). Users cannot upload their music to the platform. + +However, in the era of digital copyright, there are still many advantages to getting original music files, such as no need to install a dedicated player; free copying to other devices (without violating copyright); no risk of music unavailable from the platform; no play records and privacy will be tracked by the platform. + +Some people don't like streaming platforms. They like to collect music (download or buy CDs) and save it on their computers. But as more and more music is collected (over 70,000 songs and in total size of 800GB), it becomes very difficult to manage files. It is difficult for them to find where the songs they want to listen to are saved. Also, lossless music files are large and difficult to play online. + +So we decided to develop a project based on database knowledge to help people who have collected a lot of music to enjoy their music simply. + +The features of the project we designed are as follows: + +- Open. Independent front-end (GUI) and back-end (server program), using API to communicate. +- Easy to use. Minimize dependencies, allowing users to configure quickly and simply. +- Lightweight. The program is small in size and quick to install. +- High performance. Only do what should be done, no features that will lead to poor performance. +- Cross-platform. The project can run on computers, mobile phones, Linux, Windows, macOS, and X86 and ARM processor architectures. +- Extensibility. Access to cloud OSS (Object Storage Service), reverse proxy, or other external software. + +Our project has the following functions: + +- Index file. Index local files into the database. +- Search. Search for music based on name/album/tag/comment, sorted by rating or other columns. +- Play. Play music online, play music randomly and play music at a low bit rate on a bad network. +- User management. Users can register and log in. +- Comment. Users can give a like or comment on the music. +- Management. The administrator can upload music, update or delete the database. +- Share. Generate a link to share the music with others. + +After research and discussion, in order to meet the above requirements, we decided to use the Golang programming language on the backend. SQLite as a database program. Vue as the front-end GUI interface. \ No newline at end of file From 2358335d4e475172f289c7cdfc491777d6b67050 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Thu, 4 Nov 2021 12:56:27 +0800 Subject: [PATCH 002/104] Add: README.md DBMS TODO --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index ef0fef7..bed34c5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,53 @@ A light weight personal music streaming platform. [toc] +## TODO + +- Restructure,为多人协作做好准备 + +### 前端部分更改 + +- 修复页面 CSS 溢出问题 +- 显示操作执行世界 + +页面数量至少 10 个(目前 5 个),预计添加如下页面 + +- 文件详情页,可以修改单个文件的信息 +- 文件评论页,可对文件进行评论 +- 最新动态页,查看最近播放的曲目、最近的评论 +- 登录/注册页,取代现有的 token 逻辑 +- FfmpegConfigs 配置页面 +- 意见反馈的查看页面 + +### 后端部分更改 + +- 返回操作执行时间 + +- 修复 Prepare 模式转码不完整但仍然被 tmpfs 记录为成功转码的问题 + +- FfmpegConfigs 由目前的字典格式改为列表格式 +- 为 sqlite3 添加数据库单线程锁 +- 添加外键约束 +- Update 功能自动检查重复的项目并忽略,只添加新的项目 +- Token 验证方法改为 暱称 + 密码 的方法,管理员使用 admin 保留关键字作为暱称。 + +需要 8 个 entities 和 6 个 relationship,目前有 3 个 entities,和 1 个 relationship + +目前有 + +- files 文件表,数量 50,000 +- folders 文件夹表,数量 3,000 +- feedbacks 反馈留言表 + +计划添加 + +- users 用户表 +- comments 管理表 +- playbacks 播放记录表 +- likes 点赞记录表 + +## 编译 & 构建 + ## How to build ### Build the back-end server From be2515231c00c3cf1ee34869f8d53a0d0cd48f6a Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Thu, 4 Nov 2021 20:07:52 +0800 Subject: [PATCH 003/104] Update: docs/problem_description add extra description --- docs/problem_description.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/problem_description.md b/docs/problem_description.md index 8346d51..0eb2c37 100644 --- a/docs/problem_description.md +++ b/docs/problem_description.md @@ -10,7 +10,9 @@ However, in the era of digital copyright, there are still many advantages to get Some people don't like streaming platforms. They like to collect music (download or buy CDs) and save it on their computers. But as more and more music is collected (over 70,000 songs and in total size of 800GB), it becomes very difficult to manage files. It is difficult for them to find where the songs they want to listen to are saved. Also, lossless music files are large and difficult to play online. -So we decided to develop a project based on database knowledge to help people who have collected a lot of music to enjoy their music simply. +As long as there no such "Self-hosted music streaming platform" software available, we decided to develop a project based on database knowledge to help people who have collected a lot of music to enjoy their music simply. + +We will handle various relevant types of data in our database. Including song name, album name, file size, update date, rating, comment, user information, etc. They are highly relevant, so using a relational database will be a good choice. The features of the project we designed are as follows: From 258bf9869fc3c2fe69f490836aafcb371d920b1d Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Tue, 7 Dec 2021 00:19:05 +0800 Subject: [PATCH 004/104] Re-struct pkg/api, pkg/database --- internal/pkg/api/api.go | 750 ------------------ internal/pkg/database/database.go | 441 ---------- main.go | 2 +- pkg/api/api.go | 84 ++ {internal/pkg => pkg}/api/api_test.go | 0 pkg/api/check.go | 17 + pkg/api/handle_common.go | 26 + pkg/api/handle_database_manage.go | 81 ++ pkg/api/handle_error.go | 28 + pkg/api/handle_feedback.go | 45 ++ pkg/api/handle_ffmpeg_config.go | 74 ++ pkg/api/handle_get_file_info.go | 117 +++ pkg/api/handle_get_files_in_folder.go | 50 ++ pkg/api/handle_get_random_files.go | 25 + pkg/api/handle_search_files.go | 48 ++ pkg/api/handle_search_folders.go | 48 ++ pkg/api/handle_stream.go | 191 +++++ pkg/api/handle_token.go | 18 + pkg/database/database.go | 36 + .../pkg => pkg}/database/database_test.go | 0 pkg/database/method.go | 225 ++++++ pkg/database/sql_stmt.go | 206 +++++ pkg/database/struct.go | 30 + {internal/pkg => pkg}/tmpfs/tmpfs.go | 0 {internal/pkg => pkg}/tmpfs/tmpfs_test.go | 0 25 files changed, 1350 insertions(+), 1192 deletions(-) delete mode 100644 internal/pkg/api/api.go delete mode 100644 internal/pkg/database/database.go create mode 100644 pkg/api/api.go rename {internal/pkg => pkg}/api/api_test.go (100%) create mode 100644 pkg/api/check.go create mode 100644 pkg/api/handle_common.go create mode 100644 pkg/api/handle_database_manage.go create mode 100644 pkg/api/handle_error.go create mode 100644 pkg/api/handle_feedback.go create mode 100644 pkg/api/handle_ffmpeg_config.go create mode 100644 pkg/api/handle_get_file_info.go create mode 100644 pkg/api/handle_get_files_in_folder.go create mode 100644 pkg/api/handle_get_random_files.go create mode 100644 pkg/api/handle_search_files.go create mode 100644 pkg/api/handle_search_folders.go create mode 100644 pkg/api/handle_stream.go create mode 100644 pkg/api/handle_token.go create mode 100644 pkg/database/database.go rename {internal/pkg => pkg}/database/database_test.go (100%) create mode 100644 pkg/database/method.go create mode 100644 pkg/database/sql_stmt.go create mode 100644 pkg/database/struct.go rename {internal/pkg => pkg}/tmpfs/tmpfs.go (100%) rename {internal/pkg => pkg}/tmpfs/tmpfs_test.go (100%) diff --git a/internal/pkg/api/api.go b/internal/pkg/api/api.go deleted file mode 100644 index 58cfb60..0000000 --- a/internal/pkg/api/api.go +++ /dev/null @@ -1,750 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "errors" - "io" - "log" - "msw-open-music/internal/pkg/database" - "msw-open-music/internal/pkg/tmpfs" - "net/http" - "os" - "os/exec" - "strconv" - "strings" - "time" -) - -type API struct { - Db *database.Database - Server http.Server - token string - APIConfig APIConfig - Tmpfs *tmpfs.Tmpfs -} - -type FfmpegConfigList struct { - FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"` -} - -type AddFfmpegConfigRequest struct { - Token string `json:"token"` - Name string `json:"name"` - FfmpegConfig FfmpegConfig `json:"ffmpeg_config"` -} - -type FfmpegConfig struct { - Name string `json:"name"` - Args string `json:"args"` -} - -type Status struct { - Status string `json:"status,omitempty"` -} -var ok Status = Status{ - Status: "OK", -} - -type WalkRequest struct { - Token string `json:"token"` - Root string `json:"root"` - Pattern []string `json:"pattern"` -} - -type ResetRequest struct { - Token string `json:"token"` -} - -type SearchFilesRequest struct { - Filename string `json:"filename"` - Limit int64 `json:"limit"` - Offset int64 `json:"offset"` -} - -type SearchFoldersRequest struct { - Foldername string `json:"foldername"` - Limit int64 `json:"limit"` - Offset int64 `json:"offset"` -} - -type SearchFilesResponse struct { - Files []database.File `json:"files"` -} - -type SearchFoldersResponse struct { - Folders []database.Folder `json:"folders"` -} - -type GetFilesInFolderRequest struct { - Folder_id int64 `json:"folder_id"` - Limit int64 `json:"limit"` - Offset int64 `json:"offset"` -} - -type GetFilesInFolderResponse struct { - Files *[]database.File `json:"files"` -} - -type GetRandomFilesResponse struct { - Files *[]database.File `json:"files"` -} - -func (api *API) HandleGetRandomFiles(w http.ResponseWriter, r *http.Request) { - files, err := api.Db.GetRandomFiles(10); - if err != nil { - api.HandleError(w, r, err) - return - } - getRandomFilesResponse := &GetRandomFilesResponse{ - Files: &files, - } - log.Println("[api] Get random files") - json.NewEncoder(w).Encode(getRandomFilesResponse) -} - -func (api *API) HandleGetFilesInFolder(w http.ResponseWriter, r *http.Request) { - getFilesInFolderRequest := &GetFilesInFolderRequest{ - Folder_id: -1, - } - - err := json.NewDecoder(r.Body).Decode(getFilesInFolderRequest) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check empyt - if getFilesInFolderRequest.Folder_id < 0 { - api.HandleErrorString(w, r, `"folder_id" can't be none or negative`) - return - } - - files, err := api.Db.GetFilesInFolder(getFilesInFolderRequest.Folder_id, getFilesInFolderRequest.Limit, getFilesInFolderRequest.Offset) - if err != nil { - api.HandleError(w, r, err) - return - } - - getFilesInFolderResponse := &GetFilesInFolderResponse{ - Files: &files, - } - - log.Println("[api] Get files in folder", getFilesInFolderRequest.Folder_id) - - json.NewEncoder(w).Encode(getFilesInFolderResponse) -} - -func (api *API) CheckToken(w http.ResponseWriter, r *http.Request, token string) (error) { - if token != api.token { - err := errors.New("token not matched") - log.Println("[api] [Warning] Token not matched", token) - api.HandleErrorCode(w, r, err, 403) - return err - } - log.Println("[api] Token passed") - return nil -} - -func (api *API) HandleError(w http.ResponseWriter, r *http.Request, err error) { - api.HandleErrorString(w, r, err.Error()) -} - -func (api *API) HandleErrorCode(w http.ResponseWriter, r *http.Request, err error, code int) { - api.HandleErrorStringCode(w, r, err.Error(), code) -} - -func (api *API) HandleErrorString(w http.ResponseWriter, r *http.Request, errorString string) { - api.HandleErrorStringCode(w, r, errorString, 500) -} - -func (api *API) HandleErrorStringCode(w http.ResponseWriter, r *http.Request, errorString string, code int) { - log.Println("[api] [Error]", code, errorString) - errStatus := &Status{ - Status: errorString, - } - w.WriteHeader(code) - json.NewEncoder(w).Encode(errStatus) -} - -func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) { - resetRequest := &ResetRequest{} - err := json.NewDecoder(r.Body).Decode(resetRequest) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check token - err = api.CheckToken(w, r, resetRequest.Token) - if err != nil { - return - } - - // reset - err = api.Db.ResetFiles() - if err != nil { - api.HandleError(w, r, err) - return - } - err = api.Db.ResetFolder() - if err != nil { - api.HandleError(w, r, err) - return - } - - api.HandleStatus(w, r, "Database reseted") -} - -func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) { - walkRequest := &WalkRequest{} - err := json.NewDecoder(r.Body).Decode(walkRequest) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check token match - err = api.CheckToken(w, r, walkRequest.Token) - if err != nil { - return - } - - // check root empty - if walkRequest.Root == "" { - api.HandleErrorString(w, r, `key "root" can't be empty`) - return - } - - // check pattern empty - if len(walkRequest.Pattern) == 0 { - api.HandleErrorString(w, r, `"[]pattern" can't be empty`) - return - } - - // walk - err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern) - if err != nil { - api.HandleError(w, r, err) - return - } - - api.HandleStatus(w, r, "Database udpated") -} - -func (api *API) HandleOK(w http.ResponseWriter, r *http.Request) { - json.NewEncoder(w).Encode(&ok) -} - -func (api *API) HandleStatus(w http.ResponseWriter, r *http.Request, status string) { - s := &Status{ - Status: status, - } - - json.NewEncoder(w).Encode(s) -} - -func (api *API) HandleSearchFiles(w http.ResponseWriter, r *http.Request) { - searchFilesRequest := &SearchFilesRequest{} - err := json.NewDecoder(r.Body).Decode(searchFilesRequest) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check empty - if searchFilesRequest.Filename == "" { - api.HandleErrorString(w, r, `"filename" can't be empty`) - return - } - if api.CheckLimit(w, r, searchFilesRequest.Limit) != nil { - return - } - - searchFilesResponse := &SearchFilesResponse{} - - searchFilesResponse.Files, err = api.Db.SearchFiles(searchFilesRequest.Filename, searchFilesRequest.Limit, searchFilesRequest.Offset) - if err != nil { - api.HandleError(w, r, err) - return - } - - log.Println("[api] Search files", searchFilesRequest.Filename, searchFilesRequest.Limit, searchFilesRequest.Offset) - - json.NewEncoder(w).Encode(searchFilesResponse) -} - -func (api *API) CheckLimit(w http.ResponseWriter, r *http.Request, limit int64) (error) { - if limit <= 0 || limit > 10 { - log.Println("[api] [Warning] Limit error", limit) - err := errors.New(`"limit" can't be zero or more than 10`) - api.HandleError(w, r, err) - return err - } - return nil -} - -func (api *API) HandleSearchFolders(w http.ResponseWriter, r *http.Request) { - searchFoldersRequest := &SearchFoldersRequest{} - err := json.NewDecoder(r.Body).Decode(searchFoldersRequest) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check empty - if searchFoldersRequest.Foldername == "" { - api.HandleErrorString(w, r, `"foldername" can't be empty`) - return - } - if api.CheckLimit(w, r, searchFoldersRequest.Limit) != nil { - return - } - - searchFoldersResponse := &SearchFoldersResponse{} - - searchFoldersResponse.Folders, err = api.Db.SearchFolders(searchFoldersRequest.Foldername, searchFoldersRequest.Limit, searchFoldersRequest.Offset) - if err != nil { - api.HandleError(w, r, err) - return - } - - log.Println("[api] Search folders", searchFoldersRequest.Foldername, searchFoldersRequest.Limit, searchFoldersRequest.Offset) - - json.NewEncoder(w).Encode(searchFoldersResponse) -} - -type GetFileRequest struct { - ID int64 `json:"id"` -} - -func (api *API) HandleGetFileInfo(w http.ResponseWriter, r *http.Request) { - getFileRequest := &GetFileRequest{ - ID: -1, - } - - err := json.NewDecoder(r.Body).Decode(getFileRequest) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check empty - if getFileRequest.ID < 0 { - api.HandleErrorString(w, r, `"id" can't be none or negative`) - return - } - - file, err := api.Db.GetFile(getFileRequest.ID) - if err != nil { - api.HandleError(w, r, err) - return - } - - err = json.NewEncoder(w).Encode(file) - if err != nil { - api.HandleError(w, r, err) - return - } -} - -func (api *API) CheckGetFileStream(w http.ResponseWriter, r *http.Request) (error) { - var err error - q := r.URL.Query() - ids := q["id"] - if len(ids) == 0 { - err = errors.New(`parameter "id" can't be empty`) - api.HandleError(w, r, err) - return err - } - _, err = strconv.Atoi(ids[0]) - if err != nil { - err = errors.New(`parameter "id" should be an integer`) - api.HandleError(w, r, err) - return err - } - configs := q["config"] - if len(configs) == 0 { - err = errors.New(`parameter "config" can't be empty`) - api.HandleError(w, r, err) - return err - } - return nil -} - -func (api *API) GetFfmpegConfig(configName string) (FfmpegConfig, bool) { - ffmpegConfig := FfmpegConfig{} - for _, f := range api.APIConfig.FfmpegConfigList { - if f.Name == configName { - ffmpegConfig = f - } - } - if ffmpegConfig.Name == "" { - return ffmpegConfig, false - } - return ffmpegConfig, true -} - -func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) { - err := api.CheckGetFileStream(w, r) - if err != nil { - return - } - q := r.URL.Query() - ids := q["id"] - id, err := strconv.Atoi(ids[0]) - configs := q["config"] - configName := configs[0] - file, err := api.Db.GetFile(int64(id)) - if err != nil { - api.HandleError(w, r, err) - return - } - - path, err := file.Path() - if err != nil { - api.HandleError(w, r, err) - return - } - - log.Println("[api] Stream file", path, configName) - - ffmpegConfig, ok := api.GetFfmpegConfig(configName) - if !ok { - api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404) - return - } - args := strings.Split(ffmpegConfig.Args, " ") - startArgs := []string {"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", path} - endArgs := []string {"-vn", "-f", "ogg", "-"} - ffmpegArgs := append(startArgs, args...) - ffmpegArgs = append(ffmpegArgs, endArgs...) - cmd := exec.Command("ffmpeg", ffmpegArgs...) - cmd.Stdout = w - err = cmd.Run() - if err != nil { - api.HandleError(w, r, err) - return - } -} - -type PrepareFileStreamDirectRequest struct { - ID int64 `json:"id"` - ConfigName string `json:"config_name"` -} - -type PrepareFileStreamDirectResponse struct { - Filesize int64 `json:"filesize"` -} - -func (api *API) HandlePrepareFileStreamDirect(w http.ResponseWriter, r *http.Request) { - prepareFileStreamDirectRequst := &PrepareFileStreamDirectRequest{ - ID: -1, - } - err := json.NewDecoder(r.Body).Decode(prepareFileStreamDirectRequst) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check empty - if prepareFileStreamDirectRequst.ID < 0 { - api.HandleErrorString(w, r, `"id" can't be none or negative`) - return - } - if prepareFileStreamDirectRequst.ConfigName == "" { - api.HandleErrorString(w, r, `"config_name" can't be empty`) - return - } - - file, err := api.Db.GetFile(prepareFileStreamDirectRequst.ID) - if err != nil { - api.HandleError(w, r, err) - return - } - srcPath, err := file.Path() - if err != nil { - api.HandleError(w, r, err) - return - } - - log.Println("[api] Prepare stream direct file", srcPath, prepareFileStreamDirectRequst.ConfigName) - ffmpegConfig, ok := api.GetFfmpegConfig(prepareFileStreamDirectRequst.ConfigName) - if !ok { - api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404) - return - } - objPath := api.Tmpfs.GetObjFilePath(prepareFileStreamDirectRequst.ID, prepareFileStreamDirectRequst.ConfigName) - - // check obj file exists - exists := api.Tmpfs.Exits(objPath) - if exists { - fileInfo, err := os.Stat(objPath) - if err != nil { - api.HandleError(w, r, err) - return - } - prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{ - Filesize: fileInfo.Size(), - } - json.NewEncoder(w).Encode(prepareFileStreamDirectResponse) - return - } - - api.Tmpfs.Record(objPath) - args := strings.Split(ffmpegConfig.Args, " ") - startArgs := []string {"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", srcPath} - endArgs := []string {"-vn", "-y", objPath} - ffmpegArgs := append(startArgs, args...) - ffmpegArgs = append(ffmpegArgs, endArgs...) - cmd := exec.Command("ffmpeg", ffmpegArgs...) - err = cmd.Run() - if err != nil { - api.HandleError(w, r, err) - return - } - - fileInfo, err := os.Stat(objPath) - if err != nil { - api.HandleError(w, r, err) - return - } - prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{ - Filesize: fileInfo.Size(), - } - json.NewEncoder(w).Encode(prepareFileStreamDirectResponse) -} - -func (api *API) HandleGetFileStreamDirect(w http.ResponseWriter, r *http.Request) { - err := api.CheckGetFileStream(w, r) - if err != nil { - return - } - q := r.URL.Query() - ids := q["id"] - id, err := strconv.Atoi(ids[0]) - configs := q["config"] - configName := configs[0] - - path := api.Tmpfs.GetObjFilePath(int64(id), configName) - if api.Tmpfs.Exits(path) { - api.Tmpfs.Record(path) - } - - log.Println("[api] Get direct cached file", path) - - http.ServeFile(w, r, path) -} - -func (api *API) HandleGetFileDirect(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - ids := q["id"] - if len(ids) == 0 { - api.HandleErrorString(w, r, `parameter "id" can't be empty`) - return - } - id, err := strconv.Atoi(ids[0]) - if err != nil { - api.HandleErrorString(w, r, `parameter "id" should be an integer`) - return - } - file, err := api.Db.GetFile(int64(id)) - if err != nil { - api.HandleError(w, r, err) - return - } - - path, err := file.Path() - if err != nil { - api.HandleError(w, r, err) - return - } - - log.Println("[api] Get direct raw file", path) - - http.ServeFile(w, r, path) -} - -func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) { - getFileRequest := &GetFileRequest{ - ID: -1, - } - - err := json.NewDecoder(r.Body).Decode(getFileRequest) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check empty - if getFileRequest.ID < 0 { - api.HandleErrorString(w, r, `"id" can't be none or negative`) - return - } - - file, err := api.Db.GetFile(getFileRequest.ID) - if err != nil { - api.HandleError(w, r, err) - return - } - - path, err := file.Path() - if err != nil { - api.HandleError(w, r, err) - return - } - - log.Println("[api] Get pipe raw file", path) - - src, err := os.Open(path) - if err != nil { - api.HandleError(w, r, err) - return - } - defer src.Close() - io.Copy(w, src) -} - -func (api *API) HandleGetFfmpegConfigs(w http.ResponseWriter, r *http.Request) { - log.Println("[api] Get ffmpeg config list") - ffmpegConfigList:= &FfmpegConfigList{ - FfmpegConfigList: api.APIConfig.FfmpegConfigList, - } - json.NewEncoder(w).Encode(&ffmpegConfigList) -} - -func (api *API) HandleAddFfmpegConfig(w http.ResponseWriter, r *http.Request) { - addFfmpegConfigRequest := AddFfmpegConfigRequest{} - err := json.NewDecoder(r.Body).Decode(&addFfmpegConfigRequest) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check token - err = api.CheckToken(w, r, addFfmpegConfigRequest.Token) - if err != nil { - return - } - - // check name and args not null - if addFfmpegConfigRequest.Name == "" { - api.HandleErrorString(w, r, `"ffmpeg_config.name" can't be empty`) - return - } - if addFfmpegConfigRequest.FfmpegConfig.Args == "" { - api.HandleErrorString(w, r, `"ffmpeg_config.args" can't be empty`) - return - } - - log.Println("[api] Add ffmpeg config") - - api.APIConfig.FfmpegConfigList = append(api.APIConfig.FfmpegConfigList, addFfmpegConfigRequest.FfmpegConfig) - - api.HandleOK(w, r) -} - -type FeedbackRequest struct { - Feedback string `json:"feedback"` -} - -func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) { - feedbackRequest := &FeedbackRequest{} - err :=json.NewDecoder(r.Body).Decode(feedbackRequest) - if err != nil { - api.HandleError(w, r, err) - return - } - - // check empty feedback - if feedbackRequest.Feedback == "" { - api.HandleErrorString(w, r, `"feedback" can't be empty`) - return - } - - log.Println("[api] Feedback", feedbackRequest.Feedback) - - headerBuff := &bytes.Buffer{} - err = r.Header.Write(headerBuff) - if err != nil { - api.HandleError(w, r, err) - return - } - header := headerBuff.String() - - err = api.Db.InsertFeedback(time.Now().Unix(), feedbackRequest.Feedback, header) - if err != nil { - api.HandleError(w, r, err) - return - } - api.HandleOK(w, r) -} - -func NewAPIConfig() (APIConfig) { - apiConfig := APIConfig{} - return apiConfig -} - -type APIConfig struct { - DatabaseName string `json:"database_name"` - Addr string `json:"addr"` - Token string `json:"token"` - FfmpegThreads int64 `json:"ffmpeg_threads"` - FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"` -} - -type Config struct { - APIConfig APIConfig `json:"api"` - TmpfsConfig tmpfs.TmpfsConfig `json:"tmpfs"` -} - -func NewAPI(config Config) (*API, error) { - var err error - - apiConfig := config.APIConfig - tmpfsConfig := config.TmpfsConfig - - db, err := database.NewDatabase(apiConfig.DatabaseName) - if err != nil { - return nil, err - } - - mux := http.NewServeMux() - apiMux := http.NewServeMux() - - api := &API{ - Db: db, - Server: http.Server{ - Addr: apiConfig.Addr, - Handler: mux, - }, - APIConfig: apiConfig, - } - api.Tmpfs = tmpfs.NewTmpfs(tmpfsConfig) - - // mount api - apiMux.HandleFunc("/hello", api.HandleOK) - apiMux.HandleFunc("/get_file", api.HandleGetFile) - apiMux.HandleFunc("/get_file_direct", api.HandleGetFileDirect) - apiMux.HandleFunc("/search_files", api.HandleSearchFiles) - apiMux.HandleFunc("/search_folders", api.HandleSearchFolders) - apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder) - apiMux.HandleFunc("/get_random_files", api.HandleGetRandomFiles) - apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream) - apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs) - apiMux.HandleFunc("/feedback", api.HandleFeedback) - apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo) - apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect) - apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect) - // below needs token - apiMux.HandleFunc("/walk", api.HandleWalk) - apiMux.HandleFunc("/reset", api.HandleReset) - apiMux.HandleFunc("/add_ffmpeg_config", api.HandleAddFfmpegConfig) - - mux.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMux)) - mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web/build")))) - - api.token = apiConfig.Token - - return api, nil -} diff --git a/internal/pkg/database/database.go b/internal/pkg/database/database.go deleted file mode 100644 index 5063505..0000000 --- a/internal/pkg/database/database.go +++ /dev/null @@ -1,441 +0,0 @@ -package database - -import ( - "database/sql" - "errors" - "log" - "os" - "path/filepath" - - _ "github.com/mattn/go-sqlite3" -) - -var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files ( - id INTEGER PRIMARY KEY, - folder_id INTEGER NOT NULL, - filename TEXT NOT NULL, - filesize INTEGER NOT NULL -);` -var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders ( - id INTEGER PRIMARY KEY, - folder TEXT NOT NULL, - foldername TEXT NOT NULL -);` -var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks ( - id INTEGER PRIMARY KEY, - time INTEGER NOT NULL, - feedback TEXT NOT NULL, - header TEXT NOT NULL -);` -var insertFolderQuery = `INSERT INTO folders (folder, foldername) VALUES (?, ?);` -var findFolderQuery = `SELECT id FROM folders WHERE folder = ? LIMIT 1;` -var insertFileQuery = `INSERT INTO files (folder_id, filename, filesize) VALUES (?, ?, ?);` -var searchFilesQuery = `SELECT files.id, files.folder_id, files.filename, folders.foldername, files.filesize FROM files JOIN folders ON files.folder_id = folders.id WHERE filename LIKE ? LIMIT ? OFFSET ?;` -var getFolderQuery = `SELECT folder FROM folders WHERE id = ? LIMIT 1;` -var dropFilesQuery = `DROP TABLE files;` -var dropFolderQuery = `DROP TABLE folders;` -var getFileQuery = `SELECT files.id, files.folder_id, files.filename, folders.foldername, files.filesize FROM files JOIN folders ON files.folder_id = folders.id WHERE files.id = ? LIMIT 1;` -var searchFoldersQuery = `SELECT id, folder, foldername FROM folders WHERE foldername LIKE ? LIMIT ? OFFSET ?;` -var getFilesInFolderQuery = `SELECT files.id, files.filename, files.filesize, folders.foldername FROM files JOIN folders ON files.folder_id = folders.id WHERE folder_id = ? LIMIT ? OFFSET ?;` -var getRandomFilesQuery = `SELECT files.id, files.folder_id, files.filename, folders.foldername, files.filesize FROM files JOIN folders on files.folder_id = folders.id ORDER BY RANDOM() LIMIT ?;` -var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header) VALUES (?, ?, ?);` - -type Database struct { - sqlConn *sql.DB - stmt *Stmt -} - -type Stmt struct { - initFilesTable *sql.Stmt - initFoldersTable *sql.Stmt - initFeedbacksTable *sql.Stmt - insertFolder *sql.Stmt - insertFile *sql.Stmt - findFolder *sql.Stmt - searchFiles *sql.Stmt - getFolder *sql.Stmt - dropFiles *sql.Stmt - dropFolder *sql.Stmt - getFile *sql.Stmt - searchFolders *sql.Stmt - getFilesInFolder *sql.Stmt - getRandomFiles *sql.Stmt - insertFeedback *sql.Stmt -} - -type File struct { - Db *Database `json:"-"` - ID int64 `json:"id"` - Folder_id int64 `json:"folder_id"` - Foldername string `json:"foldername"` - Filename string `json:"filename"` - Filesize int64 `json:"filesize"` -} - -type Folder struct { - Db *Database `json:"-"` - ID int64 `json:"id"` - Folder string `json:"-"` - Foldername string `json:"foldername"` -} - -func (database *Database) InsertFeedback(time int64, feedback string, header string) (error) { - _, err := database.stmt.insertFeedback.Exec(time, feedback, header) - if err != nil { - return err - } - return nil -} - -func (database *Database) GetRandomFiles(limit int64) ([]File, error) { - rows, err := database.stmt.getRandomFiles.Query(limit) - if err != nil { - return nil, err - } - defer rows.Close() - files := make([]File, 0) - for rows.Next() { - file := File{ - Db: database, - } - err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize) - if err != nil { - return nil, err - } - files = append(files, file) - } - return files, nil -} - -func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, error) { - rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset) - if err != nil { - return nil, err - } - defer rows.Close() - files := make([]File, 0) - for rows.Next() { - file := File{ - Db: database, - Folder_id: folder_id, - } - err = rows.Scan(&file.ID, &file.Filename, &file.Filesize, &file.Foldername) - if err != nil { - return nil, err - } - files = append(files, file) - } - return files, nil -} - -func (database *Database) SearchFolders(foldername string, limit int64, offset int64) ([]Folder, error) { - rows, err := database.stmt.searchFolders.Query("%"+foldername+"%", limit, offset) - if err != nil { - return nil, errors.New("Error searching folders at query " + err.Error()) - } - defer rows.Close() - folders := make([]Folder, 0) - for rows.Next() { - folder := Folder{ - Db: database, - } - err = rows.Scan(&folder.ID, &folder.Folder, &folder.Foldername) - if err != nil { - return nil, errors.New("Error scanning SearchFolders" + err.Error()) - } - folders = append(folders, folder) - } - return folders, nil -} - -func (database *Database) GetFile(id int64) (*File, error) { - file := &File{ - Db: database, - } - err := database.stmt.getFile.QueryRow(id).Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize) - if err != nil { - return nil, err - } - return file, nil -} - -func (database *Database) ResetFiles() (error) { - log.Println("[db] Reset files") - var err error - _, err = database.stmt.dropFiles.Exec() - if err != nil { - return err - } - _, err = database.stmt.initFilesTable.Exec() - if err != nil { - return err - } - return err -} - -func (database *Database) ResetFolder() (error) { - log.Println("[db] Reset folders") - var err error - _, err = database.stmt.dropFolder.Exec() - if err != nil { - return err - } - _, err = database.stmt.initFoldersTable.Exec() - if err != nil { - return err - } - return err -} - -func (database *Database) Walk(root string, pattern []string) (error) { - patternDict := make(map[string]bool) - for _, v := range pattern { - patternDict[v] = true - } - log.Println("[db] Walk", root, patternDict) - return filepath.Walk(root, func (path string, info os.FileInfo, err error) (error) { - if err != nil { - return err - } - - if info.IsDir() { - return nil - } - - // check pattern - ext := filepath.Ext(info.Name()) - if _, ok := patternDict[ext]; !ok { - return nil - } - - // insert file, folder will aut created - err = database.Insert(path, info.Size()) - if err != nil { - return err - } - return nil - }) -} - -func (f *File) Path() (string, error) { - folder, err := f.Db.GetFolder(f.Folder_id) - if err != nil { - return "", err - } - return filepath.Join(folder.Folder, f.Filename), nil -} - -func (database *Database) GetFolder(folderId int64) (*Folder, error) { - folder := &Folder{ - Db: database, - } - err := database.stmt.getFolder.QueryRow(folderId).Scan(&folder.Folder) - if err != nil { - return nil, err - } - return folder, nil -} - -func (database *Database) SearchFiles(filename string, limit int64, offset int64) ([]File, error) { - rows, err := database.stmt.searchFiles.Query("%"+filename+"%", limit, offset) - if err != nil { - return nil, errors.New("Error searching files at query " + err.Error()) - } - defer rows.Close() - files := make([]File, 0) - for rows.Next() { - var file File = File{ - Db: database, - } - err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize) - if err != nil { - return nil, errors.New("Error scanning SearchFiles " + err.Error()) - } - files = append(files, file) - } - if err = rows.Err(); err != nil { - return nil, errors.New("Error scanning SearchFiles exit without full result" + err.Error()) - } - return files, nil -} - -func (database *Database) FindFolder(folder string) (int64, error) { - var id int64 - err := database.stmt.findFolder.QueryRow(folder).Scan(&id) - if err != nil { - return 0, err - } - return id, nil -} - -func (database *Database) InsertFolder(folder string) (int64, error) { - result, err := database.stmt.insertFolder.Exec(folder, filepath.Base(folder)) - if err != nil { - return 0, err - } - lastInsertId, err := result.LastInsertId() - if err != nil { - return 0, err - } - return lastInsertId, nil -} - -func (database *Database) InsertFile(folderId int64, filename string, filesize int64) (error) { - _, err := database.stmt.insertFile.Exec(folderId, filename, filesize) - if err != nil { - return err - } - return nil -} - -func (database *Database) Insert(path string, filesize int64) (error) { - folder, filename := filepath.Split(path) - folderId, err := database.FindFolder(folder) - if err != nil { - folderId, err = database.InsertFolder(folder) - if err != nil { - return err - } - } - err = database.InsertFile(folderId, filename, filesize) - if err != nil { - return err - } - return nil -} - -func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { - var err error - - stmt := &Stmt{} - - // init files table - stmt.initFilesTable, err = sqlConn.Prepare(initFilesTableQuery) - if err != nil { - return nil, err - } - - // init folders table - stmt.initFoldersTable, err = sqlConn.Prepare(initFoldersTableQuery) - if err != nil { - return nil, err - } - - // init feedbacks tables - stmt.initFeedbacksTable, err = sqlConn.Prepare(initFeedbacksTableQuery) - if err != nil { - return nil, err - } - - // run init statement - _, err = stmt.initFilesTable.Exec() - if err != nil { - return nil, err - } - _, err = stmt.initFoldersTable.Exec() - if err != nil { - return nil, err - } - _, err = stmt.initFeedbacksTable.Exec() - if err != nil { - return nil, err - } - - // init insert folder statement - stmt.insertFolder, err = sqlConn.Prepare(insertFolderQuery) - if err != nil { - return nil, err - } - - // init findFolder statement - stmt.findFolder, err = sqlConn.Prepare(findFolderQuery) - if err != nil { - return nil, err - } - - // init insertFile stmt - stmt.insertFile, err = sqlConn.Prepare(insertFileQuery) - if err != nil { - return nil, err - } - - // init searchFile stmt - stmt.searchFiles, err = sqlConn.Prepare(searchFilesQuery) - if err != nil { - return nil, err - } - - // init getFolder stmt - stmt.getFolder, err = sqlConn.Prepare(getFolderQuery) - if err != nil { - return nil, err - } - - // init dropFolder stmt - stmt.dropFolder, err = sqlConn.Prepare(dropFolderQuery) - if err != nil { - return nil, err - } - - // init dropFiles stmt - stmt.dropFiles, err = sqlConn.Prepare(dropFilesQuery) - if err != nil { - return nil, err - } - - // init getFile stmt - stmt.getFile, err = sqlConn.Prepare(getFileQuery) - if err != nil { - return nil, err - } - - // init searchFolder stmt - stmt.searchFolders, err = sqlConn.Prepare(searchFoldersQuery) - if err != nil { - return nil, err - } - - // init getFilesInFolder stmt - stmt.getFilesInFolder, err = sqlConn.Prepare(getFilesInFolderQuery) - if err != nil { - return nil, err - } - - // init getRandomFiles - stmt.getRandomFiles, err = sqlConn.Prepare(getRandomFilesQuery) - if err != nil { - return nil, err - } - - // init insertFeedback - stmt.insertFeedback, err = sqlConn.Prepare(insertFeedbackQuery) - if err != nil { - return nil, err - } - - return stmt, err -} - -func NewDatabase(dbName string) (*Database, error) { - var err error - - // open database - sqlConn, err := sql.Open("sqlite3", dbName) - if err != nil { - return nil, err - } - - // prepare statement - stmt, err := NewPreparedStatement(sqlConn) - if err != nil { - return nil, err - } - - // new database - database := &Database{ - sqlConn: sqlConn, - stmt: stmt, - } - - return database, nil -} diff --git a/main.go b/main.go index 3657147..cd949f2 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "encoding/json" "flag" "log" - "msw-open-music/internal/pkg/api" + "msw-open-music/pkg/api" "os" ) diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000..0774fd8 --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,84 @@ +package api + +import ( + "msw-open-music/pkg/database" + "msw-open-music/pkg/tmpfs" + "net/http" +) + +type API struct { + Db *database.Database + Server http.Server + token string + APIConfig APIConfig + Tmpfs *tmpfs.Tmpfs +} + +func NewAPIConfig() APIConfig { + apiConfig := APIConfig{} + return apiConfig +} + +type APIConfig struct { + DatabaseName string `json:"database_name"` + Addr string `json:"addr"` + Token string `json:"token"` + FfmpegThreads int64 `json:"ffmpeg_threads"` + FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"` +} + +type Config struct { + APIConfig APIConfig `json:"api"` + TmpfsConfig tmpfs.TmpfsConfig `json:"tmpfs"` +} + +func NewAPI(config Config) (*API, error) { + var err error + + apiConfig := config.APIConfig + tmpfsConfig := config.TmpfsConfig + + db, err := database.NewDatabase(apiConfig.DatabaseName) + if err != nil { + return nil, err + } + + mux := http.NewServeMux() + apiMux := http.NewServeMux() + + api := &API{ + Db: db, + Server: http.Server{ + Addr: apiConfig.Addr, + Handler: mux, + }, + APIConfig: apiConfig, + } + api.Tmpfs = tmpfs.NewTmpfs(tmpfsConfig) + + // mount api + apiMux.HandleFunc("/hello", api.HandleOK) + apiMux.HandleFunc("/get_file", api.HandleGetFile) + apiMux.HandleFunc("/get_file_direct", api.HandleGetFileDirect) + apiMux.HandleFunc("/search_files", api.HandleSearchFiles) + apiMux.HandleFunc("/search_folders", api.HandleSearchFolders) + apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder) + apiMux.HandleFunc("/get_random_files", api.HandleGetRandomFiles) + apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream) + apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs) + apiMux.HandleFunc("/feedback", api.HandleFeedback) + apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo) + apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect) + apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect) + // below needs token + apiMux.HandleFunc("/walk", api.HandleWalk) + apiMux.HandleFunc("/reset", api.HandleReset) + apiMux.HandleFunc("/add_ffmpeg_config", api.HandleAddFfmpegConfig) + + mux.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMux)) + mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web/build")))) + + api.token = apiConfig.Token + + return api, nil +} diff --git a/internal/pkg/api/api_test.go b/pkg/api/api_test.go similarity index 100% rename from internal/pkg/api/api_test.go rename to pkg/api/api_test.go diff --git a/pkg/api/check.go b/pkg/api/check.go new file mode 100644 index 0000000..cf9fa6d --- /dev/null +++ b/pkg/api/check.go @@ -0,0 +1,17 @@ +package api + +import ( + "errors" + "log" + "net/http" +) + +func (api *API) CheckLimit(w http.ResponseWriter, r *http.Request, limit int64) error { + if limit <= 0 || limit > 10 { + log.Println("[api] [Warning] Limit error", limit) + err := errors.New(`"limit" can't be zero or more than 10`) + api.HandleError(w, r, err) + return err + } + return nil +} diff --git a/pkg/api/handle_common.go b/pkg/api/handle_common.go new file mode 100644 index 0000000..6665d46 --- /dev/null +++ b/pkg/api/handle_common.go @@ -0,0 +1,26 @@ +package api + +import ( + "encoding/json" + "net/http" +) + +type Status struct { + Status string `json:"status,omitempty"` +} + +func (api *API) HandleStatus(w http.ResponseWriter, r *http.Request, status string) { + s := &Status{ + Status: status, + } + + json.NewEncoder(w).Encode(s) +} + +var ok Status = Status{ + Status: "OK", +} + +func (api *API) HandleOK(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(&ok) +} diff --git a/pkg/api/handle_database_manage.go b/pkg/api/handle_database_manage.go new file mode 100644 index 0000000..513081a --- /dev/null +++ b/pkg/api/handle_database_manage.go @@ -0,0 +1,81 @@ +package api + +import ( + "encoding/json" + "net/http" +) + +type WalkRequest struct { + Token string `json:"token"` + Root string `json:"root"` + Pattern []string `json:"pattern"` +} + +type ResetRequest struct { + Token string `json:"token"` +} + +func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) { + resetRequest := &ResetRequest{} + err := json.NewDecoder(r.Body).Decode(resetRequest) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check token + err = api.CheckToken(w, r, resetRequest.Token) + if err != nil { + return + } + + // reset + err = api.Db.ResetFiles() + if err != nil { + api.HandleError(w, r, err) + return + } + err = api.Db.ResetFolder() + if err != nil { + api.HandleError(w, r, err) + return + } + + api.HandleStatus(w, r, "Database reseted") +} + +func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) { + walkRequest := &WalkRequest{} + err := json.NewDecoder(r.Body).Decode(walkRequest) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check token match + err = api.CheckToken(w, r, walkRequest.Token) + if err != nil { + return + } + + // check root empty + if walkRequest.Root == "" { + api.HandleErrorString(w, r, `key "root" can't be empty`) + return + } + + // check pattern empty + if len(walkRequest.Pattern) == 0 { + api.HandleErrorString(w, r, `"[]pattern" can't be empty`) + return + } + + // walk + err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern) + if err != nil { + api.HandleError(w, r, err) + return + } + + api.HandleStatus(w, r, "Database udpated") +} diff --git a/pkg/api/handle_error.go b/pkg/api/handle_error.go new file mode 100644 index 0000000..3f80bab --- /dev/null +++ b/pkg/api/handle_error.go @@ -0,0 +1,28 @@ +package api + +import ( + "encoding/json" + "log" + "net/http" +) + +func (api *API) HandleError(w http.ResponseWriter, r *http.Request, err error) { + api.HandleErrorString(w, r, err.Error()) +} + +func (api *API) HandleErrorCode(w http.ResponseWriter, r *http.Request, err error, code int) { + api.HandleErrorStringCode(w, r, err.Error(), code) +} + +func (api *API) HandleErrorString(w http.ResponseWriter, r *http.Request, errorString string) { + api.HandleErrorStringCode(w, r, errorString, 500) +} + +func (api *API) HandleErrorStringCode(w http.ResponseWriter, r *http.Request, errorString string, code int) { + log.Println("[api] [Error]", code, errorString) + errStatus := &Status{ + Status: errorString, + } + w.WriteHeader(code) + json.NewEncoder(w).Encode(errStatus) +} diff --git a/pkg/api/handle_feedback.go b/pkg/api/handle_feedback.go new file mode 100644 index 0000000..f9375dd --- /dev/null +++ b/pkg/api/handle_feedback.go @@ -0,0 +1,45 @@ +package api + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "time" +) + +type FeedbackRequest struct { + Feedback string `json:"feedback"` +} + +func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) { + feedbackRequest := &FeedbackRequest{} + err := json.NewDecoder(r.Body).Decode(feedbackRequest) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check empty feedback + if feedbackRequest.Feedback == "" { + api.HandleErrorString(w, r, `"feedback" can't be empty`) + return + } + + log.Println("[api] Feedback", feedbackRequest.Feedback) + + headerBuff := &bytes.Buffer{} + err = r.Header.Write(headerBuff) + if err != nil { + api.HandleError(w, r, err) + return + } + header := headerBuff.String() + + err = api.Db.InsertFeedback(time.Now().Unix(), feedbackRequest.Feedback, header) + if err != nil { + api.HandleError(w, r, err) + return + } + api.HandleOK(w, r) +} diff --git a/pkg/api/handle_ffmpeg_config.go b/pkg/api/handle_ffmpeg_config.go new file mode 100644 index 0000000..82b7a2e --- /dev/null +++ b/pkg/api/handle_ffmpeg_config.go @@ -0,0 +1,74 @@ +package api + +import ( + "encoding/json" + "log" + "net/http" +) + +type FfmpegConfig struct { + Name string `json:"name"` + Args string `json:"args"` +} + +type FfmpegConfigList struct { + FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"` +} + +func (api *API) GetFfmpegConfig(configName string) (FfmpegConfig, bool) { + ffmpegConfig := FfmpegConfig{} + for _, f := range api.APIConfig.FfmpegConfigList { + if f.Name == configName { + ffmpegConfig = f + } + } + if ffmpegConfig.Name == "" { + return ffmpegConfig, false + } + return ffmpegConfig, true +} + +func (api *API) HandleGetFfmpegConfigs(w http.ResponseWriter, r *http.Request) { + log.Println("[api] Get ffmpeg config list") + ffmpegConfigList := &FfmpegConfigList{ + FfmpegConfigList: api.APIConfig.FfmpegConfigList, + } + json.NewEncoder(w).Encode(&ffmpegConfigList) +} + +type AddFfmpegConfigRequest struct { + Token string `json:"token"` + Name string `json:"name"` + FfmpegConfig FfmpegConfig `json:"ffmpeg_config"` +} + +func (api *API) HandleAddFfmpegConfig(w http.ResponseWriter, r *http.Request) { + addFfmpegConfigRequest := AddFfmpegConfigRequest{} + err := json.NewDecoder(r.Body).Decode(&addFfmpegConfigRequest) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check token + err = api.CheckToken(w, r, addFfmpegConfigRequest.Token) + if err != nil { + return + } + + // check name and args not null + if addFfmpegConfigRequest.Name == "" { + api.HandleErrorString(w, r, `"ffmpeg_config.name" can't be empty`) + return + } + if addFfmpegConfigRequest.FfmpegConfig.Args == "" { + api.HandleErrorString(w, r, `"ffmpeg_config.args" can't be empty`) + return + } + + log.Println("[api] Add ffmpeg config") + + api.APIConfig.FfmpegConfigList = append(api.APIConfig.FfmpegConfigList, addFfmpegConfigRequest.FfmpegConfig) + + api.HandleOK(w, r) +} diff --git a/pkg/api/handle_get_file_info.go b/pkg/api/handle_get_file_info.go new file mode 100644 index 0000000..681283e --- /dev/null +++ b/pkg/api/handle_get_file_info.go @@ -0,0 +1,117 @@ +package api + +import ( + "encoding/json" + "io" + "log" + "net/http" + "os" + "strconv" +) + +type GetFileRequest struct { + ID int64 `json:"id"` +} + +func (api *API) HandleGetFileInfo(w http.ResponseWriter, r *http.Request) { + getFileRequest := &GetFileRequest{ + ID: -1, + } + + err := json.NewDecoder(r.Body).Decode(getFileRequest) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check empty + if getFileRequest.ID < 0 { + api.HandleErrorString(w, r, `"id" can't be none or negative`) + return + } + + file, err := api.Db.GetFile(getFileRequest.ID) + if err != nil { + api.HandleError(w, r, err) + return + } + + err = json.NewEncoder(w).Encode(file) + if err != nil { + api.HandleError(w, r, err) + return + } +} + +// /get_file +// get raw file with io.Copy method +func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) { + getFileRequest := &GetFileRequest{ + ID: -1, + } + + err := json.NewDecoder(r.Body).Decode(getFileRequest) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check empty + if getFileRequest.ID < 0 { + api.HandleErrorString(w, r, `"id" can't be none or negative`) + return + } + + file, err := api.Db.GetFile(getFileRequest.ID) + if err != nil { + api.HandleError(w, r, err) + return + } + + path, err := file.Path() + if err != nil { + api.HandleError(w, r, err) + return + } + + log.Println("[api] Get pipe raw file", path) + + src, err := os.Open(path) + if err != nil { + api.HandleError(w, r, err) + return + } + defer src.Close() + io.Copy(w, src) +} + +// /get_file_direct?id=1 +// get raw file with http.ServeFile method +func (api *API) HandleGetFileDirect(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + ids := q["id"] + if len(ids) == 0 { + api.HandleErrorString(w, r, `parameter "id" can't be empty`) + return + } + id, err := strconv.Atoi(ids[0]) + if err != nil { + api.HandleErrorString(w, r, `parameter "id" should be an integer`) + return + } + file, err := api.Db.GetFile(int64(id)) + if err != nil { + api.HandleError(w, r, err) + return + } + + path, err := file.Path() + if err != nil { + api.HandleError(w, r, err) + return + } + + log.Println("[api] Get direct raw file", path) + + http.ServeFile(w, r, path) +} diff --git a/pkg/api/handle_get_files_in_folder.go b/pkg/api/handle_get_files_in_folder.go new file mode 100644 index 0000000..2ca6e05 --- /dev/null +++ b/pkg/api/handle_get_files_in_folder.go @@ -0,0 +1,50 @@ +package api + +import ( + "encoding/json" + "log" + "msw-open-music/pkg/database" + "net/http" +) + +type GetFilesInFolderRequest struct { + Folder_id int64 `json:"folder_id"` + Limit int64 `json:"limit"` + Offset int64 `json:"offset"` +} + +type GetFilesInFolderResponse struct { + Files *[]database.File `json:"files"` +} + +func (api *API) HandleGetFilesInFolder(w http.ResponseWriter, r *http.Request) { + getFilesInFolderRequest := &GetFilesInFolderRequest{ + Folder_id: -1, + } + + err := json.NewDecoder(r.Body).Decode(getFilesInFolderRequest) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check empyt + if getFilesInFolderRequest.Folder_id < 0 { + api.HandleErrorString(w, r, `"folder_id" can't be none or negative`) + return + } + + files, err := api.Db.GetFilesInFolder(getFilesInFolderRequest.Folder_id, getFilesInFolderRequest.Limit, getFilesInFolderRequest.Offset) + if err != nil { + api.HandleError(w, r, err) + return + } + + getFilesInFolderResponse := &GetFilesInFolderResponse{ + Files: &files, + } + + log.Println("[api] Get files in folder", getFilesInFolderRequest.Folder_id) + + json.NewEncoder(w).Encode(getFilesInFolderResponse) +} diff --git a/pkg/api/handle_get_random_files.go b/pkg/api/handle_get_random_files.go new file mode 100644 index 0000000..a164376 --- /dev/null +++ b/pkg/api/handle_get_random_files.go @@ -0,0 +1,25 @@ +package api + +import ( + "encoding/json" + "log" + "msw-open-music/pkg/database" + "net/http" +) + +type GetRandomFilesResponse struct { + Files *[]database.File `json:"files"` +} + +func (api *API) HandleGetRandomFiles(w http.ResponseWriter, r *http.Request) { + files, err := api.Db.GetRandomFiles(10) + if err != nil { + api.HandleError(w, r, err) + return + } + getRandomFilesResponse := &GetRandomFilesResponse{ + Files: &files, + } + log.Println("[api] Get random files") + json.NewEncoder(w).Encode(getRandomFilesResponse) +} diff --git a/pkg/api/handle_search_files.go b/pkg/api/handle_search_files.go new file mode 100644 index 0000000..02a2f9d --- /dev/null +++ b/pkg/api/handle_search_files.go @@ -0,0 +1,48 @@ +package api + +import ( + "encoding/json" + "log" + "msw-open-music/pkg/database" + "net/http" +) + +type SearchFilesRequest struct { + Filename string `json:"filename"` + Limit int64 `json:"limit"` + Offset int64 `json:"offset"` +} + +type SearchFilesResponse struct { + Files []database.File `json:"files"` +} + +func (api *API) HandleSearchFiles(w http.ResponseWriter, r *http.Request) { + searchFilesRequest := &SearchFilesRequest{} + err := json.NewDecoder(r.Body).Decode(searchFilesRequest) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check empty + if searchFilesRequest.Filename == "" { + api.HandleErrorString(w, r, `"filename" can't be empty`) + return + } + if api.CheckLimit(w, r, searchFilesRequest.Limit) != nil { + return + } + + searchFilesResponse := &SearchFilesResponse{} + + searchFilesResponse.Files, err = api.Db.SearchFiles(searchFilesRequest.Filename, searchFilesRequest.Limit, searchFilesRequest.Offset) + if err != nil { + api.HandleError(w, r, err) + return + } + + log.Println("[api] Search files", searchFilesRequest.Filename, searchFilesRequest.Limit, searchFilesRequest.Offset) + + json.NewEncoder(w).Encode(searchFilesResponse) +} diff --git a/pkg/api/handle_search_folders.go b/pkg/api/handle_search_folders.go new file mode 100644 index 0000000..1aa92bf --- /dev/null +++ b/pkg/api/handle_search_folders.go @@ -0,0 +1,48 @@ +package api + +import ( + "encoding/json" + "log" + "msw-open-music/pkg/database" + "net/http" +) + +type SearchFoldersRequest struct { + Foldername string `json:"foldername"` + Limit int64 `json:"limit"` + Offset int64 `json:"offset"` +} + +type SearchFoldersResponse struct { + Folders []database.Folder `json:"folders"` +} + +func (api *API) HandleSearchFolders(w http.ResponseWriter, r *http.Request) { + searchFoldersRequest := &SearchFoldersRequest{} + err := json.NewDecoder(r.Body).Decode(searchFoldersRequest) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check empty + if searchFoldersRequest.Foldername == "" { + api.HandleErrorString(w, r, `"foldername" can't be empty`) + return + } + if api.CheckLimit(w, r, searchFoldersRequest.Limit) != nil { + return + } + + searchFoldersResponse := &SearchFoldersResponse{} + + searchFoldersResponse.Folders, err = api.Db.SearchFolders(searchFoldersRequest.Foldername, searchFoldersRequest.Limit, searchFoldersRequest.Offset) + if err != nil { + api.HandleError(w, r, err) + return + } + + log.Println("[api] Search folders", searchFoldersRequest.Foldername, searchFoldersRequest.Limit, searchFoldersRequest.Offset) + + json.NewEncoder(w).Encode(searchFoldersResponse) +} diff --git a/pkg/api/handle_stream.go b/pkg/api/handle_stream.go new file mode 100644 index 0000000..5f34ee5 --- /dev/null +++ b/pkg/api/handle_stream.go @@ -0,0 +1,191 @@ +package api + +import ( + "encoding/json" + "errors" + "log" + "net/http" + "os" + "os/exec" + "strconv" + "strings" +) + +func (api *API) CheckGetFileStream(w http.ResponseWriter, r *http.Request) error { + var err error + q := r.URL.Query() + ids := q["id"] + if len(ids) == 0 { + err = errors.New(`parameter "id" can't be empty`) + api.HandleError(w, r, err) + return err + } + _, err = strconv.Atoi(ids[0]) + if err != nil { + err = errors.New(`parameter "id" should be an integer`) + api.HandleError(w, r, err) + return err + } + configs := q["config"] + if len(configs) == 0 { + err = errors.New(`parameter "config" can't be empty`) + api.HandleError(w, r, err) + return err + } + return nil +} + +// /get_file_stream?id=1&config=ffmpeg_config_name +func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) { + err := api.CheckGetFileStream(w, r) + if err != nil { + return + } + q := r.URL.Query() + ids := q["id"] + id, err := strconv.Atoi(ids[0]) + configs := q["config"] + configName := configs[0] + file, err := api.Db.GetFile(int64(id)) + if err != nil { + api.HandleError(w, r, err) + return + } + + path, err := file.Path() + if err != nil { + api.HandleError(w, r, err) + return + } + + log.Println("[api] Stream file", path, configName) + + ffmpegConfig, ok := api.GetFfmpegConfig(configName) + if !ok { + api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404) + return + } + args := strings.Split(ffmpegConfig.Args, " ") + startArgs := []string{"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", path} + endArgs := []string{"-vn", "-f", "ogg", "-"} + ffmpegArgs := append(startArgs, args...) + ffmpegArgs = append(ffmpegArgs, endArgs...) + cmd := exec.Command("ffmpeg", ffmpegArgs...) + cmd.Stdout = w + err = cmd.Run() + if err != nil { + api.HandleError(w, r, err) + return + } +} + +type PrepareFileStreamDirectRequest struct { + ID int64 `json:"id"` + ConfigName string `json:"config_name"` +} + +type PrepareFileStreamDirectResponse struct { + Filesize int64 `json:"filesize"` +} + +// /prepare_file_stream_direct?id=1&config=ffmpeg_config_name +func (api *API) HandlePrepareFileStreamDirect(w http.ResponseWriter, r *http.Request) { + prepareFileStreamDirectRequst := &PrepareFileStreamDirectRequest{ + ID: -1, + } + err := json.NewDecoder(r.Body).Decode(prepareFileStreamDirectRequst) + if err != nil { + api.HandleError(w, r, err) + return + } + + // check empty + if prepareFileStreamDirectRequst.ID < 0 { + api.HandleErrorString(w, r, `"id" can't be none or negative`) + return + } + if prepareFileStreamDirectRequst.ConfigName == "" { + api.HandleErrorString(w, r, `"config_name" can't be empty`) + return + } + + file, err := api.Db.GetFile(prepareFileStreamDirectRequst.ID) + if err != nil { + api.HandleError(w, r, err) + return + } + srcPath, err := file.Path() + if err != nil { + api.HandleError(w, r, err) + return + } + + log.Println("[api] Prepare stream direct file", srcPath, prepareFileStreamDirectRequst.ConfigName) + ffmpegConfig, ok := api.GetFfmpegConfig(prepareFileStreamDirectRequst.ConfigName) + if !ok { + api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404) + return + } + objPath := api.Tmpfs.GetObjFilePath(prepareFileStreamDirectRequst.ID, prepareFileStreamDirectRequst.ConfigName) + + // check obj file exists + exists := api.Tmpfs.Exits(objPath) + if exists { + fileInfo, err := os.Stat(objPath) + if err != nil { + api.HandleError(w, r, err) + return + } + prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{ + Filesize: fileInfo.Size(), + } + json.NewEncoder(w).Encode(prepareFileStreamDirectResponse) + return + } + + api.Tmpfs.Record(objPath) + args := strings.Split(ffmpegConfig.Args, " ") + startArgs := []string{"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", srcPath} + endArgs := []string{"-vn", "-y", objPath} + ffmpegArgs := append(startArgs, args...) + ffmpegArgs = append(ffmpegArgs, endArgs...) + cmd := exec.Command("ffmpeg", ffmpegArgs...) + err = cmd.Run() + if err != nil { + api.HandleError(w, r, err) + return + } + + fileInfo, err := os.Stat(objPath) + if err != nil { + api.HandleError(w, r, err) + return + } + prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{ + Filesize: fileInfo.Size(), + } + json.NewEncoder(w).Encode(prepareFileStreamDirectResponse) +} + +// /get_file_stream_direct?id=1&config=ffmpeg_config_name +// return converted file with http.ServeFile method +func (api *API) HandleGetFileStreamDirect(w http.ResponseWriter, r *http.Request) { + err := api.CheckGetFileStream(w, r) + if err != nil { + return + } + q := r.URL.Query() + ids := q["id"] + id, err := strconv.Atoi(ids[0]) + configs := q["config"] + configName := configs[0] + + path := api.Tmpfs.GetObjFilePath(int64(id), configName) + if api.Tmpfs.Exits(path) { + api.Tmpfs.Record(path) + } + + log.Println("[api] Get direct cached file", path) + + http.ServeFile(w, r, path) +} diff --git a/pkg/api/handle_token.go b/pkg/api/handle_token.go new file mode 100644 index 0000000..c0ff661 --- /dev/null +++ b/pkg/api/handle_token.go @@ -0,0 +1,18 @@ +package api + +import ( + "errors" + "log" + "net/http" +) + +func (api *API) CheckToken(w http.ResponseWriter, r *http.Request, token string) error { + if token != api.token { + err := errors.New("token not matched") + log.Println("[api] [Warning] Token not matched", token) + api.HandleErrorCode(w, r, err, 403) + return err + } + log.Println("[api] Token passed") + return nil +} diff --git a/pkg/database/database.go b/pkg/database/database.go new file mode 100644 index 0000000..ceb27df --- /dev/null +++ b/pkg/database/database.go @@ -0,0 +1,36 @@ +package database + +import ( + "database/sql" + + _ "github.com/mattn/go-sqlite3" +) + +type Database struct { + sqlConn *sql.DB + stmt *Stmt +} + +func NewDatabase(dbName string) (*Database, error) { + var err error + + // open database + sqlConn, err := sql.Open("sqlite3", dbName) + if err != nil { + return nil, err + } + + // prepare statement + stmt, err := NewPreparedStatement(sqlConn) + if err != nil { + return nil, err + } + + // new database + database := &Database{ + sqlConn: sqlConn, + stmt: stmt, + } + + return database, nil +} diff --git a/internal/pkg/database/database_test.go b/pkg/database/database_test.go similarity index 100% rename from internal/pkg/database/database_test.go rename to pkg/database/database_test.go diff --git a/pkg/database/method.go b/pkg/database/method.go new file mode 100644 index 0000000..208fc1c --- /dev/null +++ b/pkg/database/method.go @@ -0,0 +1,225 @@ +package database + +import ( + "errors" + "log" + "os" + "path/filepath" +) + +func (database *Database) InsertFeedback(time int64, feedback string, header string) error { + _, err := database.stmt.insertFeedback.Exec(time, feedback, header) + if err != nil { + return err + } + return nil +} + +func (database *Database) GetRandomFiles(limit int64) ([]File, error) { + rows, err := database.stmt.getRandomFiles.Query(limit) + if err != nil { + return nil, err + } + defer rows.Close() + files := make([]File, 0) + for rows.Next() { + file := File{ + Db: database, + } + err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize) + if err != nil { + return nil, err + } + files = append(files, file) + } + return files, nil +} + +func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, error) { + rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset) + if err != nil { + return nil, err + } + defer rows.Close() + files := make([]File, 0) + for rows.Next() { + file := File{ + Db: database, + Folder_id: folder_id, + } + err = rows.Scan(&file.ID, &file.Filename, &file.Filesize, &file.Foldername) + if err != nil { + return nil, err + } + files = append(files, file) + } + return files, nil +} + +func (database *Database) SearchFolders(foldername string, limit int64, offset int64) ([]Folder, error) { + rows, err := database.stmt.searchFolders.Query("%"+foldername+"%", limit, offset) + if err != nil { + return nil, errors.New("Error searching folders at query " + err.Error()) + } + defer rows.Close() + folders := make([]Folder, 0) + for rows.Next() { + folder := Folder{ + Db: database, + } + err = rows.Scan(&folder.ID, &folder.Folder, &folder.Foldername) + if err != nil { + return nil, errors.New("Error scanning SearchFolders" + err.Error()) + } + folders = append(folders, folder) + } + return folders, nil +} + +func (database *Database) GetFile(id int64) (*File, error) { + file := &File{ + Db: database, + } + err := database.stmt.getFile.QueryRow(id).Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize) + if err != nil { + return nil, err + } + return file, nil +} + +func (database *Database) ResetFiles() error { + log.Println("[db] Reset files") + var err error + _, err = database.stmt.dropFiles.Exec() + if err != nil { + return err + } + _, err = database.stmt.initFilesTable.Exec() + if err != nil { + return err + } + return err +} + +func (database *Database) ResetFolder() error { + log.Println("[db] Reset folders") + var err error + _, err = database.stmt.dropFolder.Exec() + if err != nil { + return err + } + _, err = database.stmt.initFoldersTable.Exec() + if err != nil { + return err + } + return err +} + +func (database *Database) Walk(root string, pattern []string) error { + patternDict := make(map[string]bool) + for _, v := range pattern { + patternDict[v] = true + } + log.Println("[db] Walk", root, patternDict) + return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + // check pattern + ext := filepath.Ext(info.Name()) + if _, ok := patternDict[ext]; !ok { + return nil + } + + // insert file, folder will aut created + err = database.Insert(path, info.Size()) + if err != nil { + return err + } + return nil + }) +} + +func (database *Database) GetFolder(folderId int64) (*Folder, error) { + folder := &Folder{ + Db: database, + } + err := database.stmt.getFolder.QueryRow(folderId).Scan(&folder.Folder) + if err != nil { + return nil, err + } + return folder, nil +} + +func (database *Database) SearchFiles(filename string, limit int64, offset int64) ([]File, error) { + rows, err := database.stmt.searchFiles.Query("%"+filename+"%", limit, offset) + if err != nil { + return nil, errors.New("Error searching files at query " + err.Error()) + } + defer rows.Close() + files := make([]File, 0) + for rows.Next() { + var file File = File{ + Db: database, + } + err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize) + if err != nil { + return nil, errors.New("Error scanning SearchFiles " + err.Error()) + } + files = append(files, file) + } + if err = rows.Err(); err != nil { + return nil, errors.New("Error scanning SearchFiles exit without full result" + err.Error()) + } + return files, nil +} + +func (database *Database) FindFolder(folder string) (int64, error) { + var id int64 + err := database.stmt.findFolder.QueryRow(folder).Scan(&id) + if err != nil { + return 0, err + } + return id, nil +} + +func (database *Database) InsertFolder(folder string) (int64, error) { + result, err := database.stmt.insertFolder.Exec(folder, filepath.Base(folder)) + if err != nil { + return 0, err + } + lastInsertId, err := result.LastInsertId() + if err != nil { + return 0, err + } + return lastInsertId, nil +} + +func (database *Database) InsertFile(folderId int64, filename string, filesize int64) error { + _, err := database.stmt.insertFile.Exec(folderId, filename, filesize) + if err != nil { + return err + } + return nil +} + +func (database *Database) Insert(path string, filesize int64) error { + folder, filename := filepath.Split(path) + folderId, err := database.FindFolder(folder) + if err != nil { + folderId, err = database.InsertFolder(folder) + if err != nil { + return err + } + } + err = database.InsertFile(folderId, filename, filesize) + if err != nil { + return err + } + return nil +} diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go new file mode 100644 index 0000000..449dd52 --- /dev/null +++ b/pkg/database/sql_stmt.go @@ -0,0 +1,206 @@ +package database + +import ( + "database/sql" +) + +var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files ( + id INTEGER PRIMARY KEY, + folder_id INTEGER NOT NULL, + filename TEXT NOT NULL, + filesize INTEGER NOT NULL +);` + +var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders ( + id INTEGER PRIMARY KEY, + folder TEXT NOT NULL, + foldername TEXT NOT NULL +);` + +var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks ( + id INTEGER PRIMARY KEY, + time INTEGER NOT NULL, + feedback TEXT NOT NULL, + header TEXT NOT NULL +);` + +var insertFolderQuery = `INSERT INTO folders (folder, foldername) +VALUES (?, ?);` + +var findFolderQuery = `SELECT id FROM folders WHERE folder = ? LIMIT 1;` + +var insertFileQuery = `INSERT INTO files (folder_id, filename, filesize) +VALUES (?, ?, ?);` + +var searchFilesQuery = `SELECT +files.id, files.folder_id, files.filename, folders.foldername, files.filesize +FROM files +JOIN folders ON files.folder_id = folders.id +WHERE filename LIKE ? +LIMIT ? OFFSET ?;` + +var getFolderQuery = `SELECT folder FROM folders WHERE id = ? LIMIT 1;` + +var dropFilesQuery = `DROP TABLE files;` + +var dropFolderQuery = `DROP TABLE folders;` + +var getFileQuery = `SELECT +files.id, files.folder_id, files.filename, folders.foldername, files.filesize +FROM files +JOIN folders ON files.folder_id = folders.id +WHERE files.id = ? +LIMIT 1;` + +var searchFoldersQuery = `SELECT +id, folder, foldername +FROM folders +WHERE foldername LIKE ? +LIMIT ? OFFSET ?;` + +var getFilesInFolderQuery = `SELECT +files.id, files.filename, files.filesize, folders.foldername +FROM files +JOIN folders ON files.folder_id = folders.id +WHERE folder_id = ? +LIMIT ? OFFSET ?;` + +var getRandomFilesQuery = `SELECT +files.id, files.folder_id, files.filename, folders.foldername, files.filesize +FROM files +JOIN folders ON files.folder_id = folders.id +ORDER BY RANDOM() +LIMIT ?;` + +var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header) +VALUES (?, ?, ?);` + +type Stmt struct { + initFilesTable *sql.Stmt + initFoldersTable *sql.Stmt + initFeedbacksTable *sql.Stmt + insertFolder *sql.Stmt + insertFile *sql.Stmt + findFolder *sql.Stmt + searchFiles *sql.Stmt + getFolder *sql.Stmt + dropFiles *sql.Stmt + dropFolder *sql.Stmt + getFile *sql.Stmt + searchFolders *sql.Stmt + getFilesInFolder *sql.Stmt + getRandomFiles *sql.Stmt + insertFeedback *sql.Stmt +} + +func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { + var err error + + stmt := &Stmt{} + + // init files table + stmt.initFilesTable, err = sqlConn.Prepare(initFilesTableQuery) + if err != nil { + return nil, err + } + + // init folders table + stmt.initFoldersTable, err = sqlConn.Prepare(initFoldersTableQuery) + if err != nil { + return nil, err + } + + // init feedbacks tables + stmt.initFeedbacksTable, err = sqlConn.Prepare(initFeedbacksTableQuery) + if err != nil { + return nil, err + } + + // run init statement + _, err = stmt.initFilesTable.Exec() + if err != nil { + return nil, err + } + _, err = stmt.initFoldersTable.Exec() + if err != nil { + return nil, err + } + _, err = stmt.initFeedbacksTable.Exec() + if err != nil { + return nil, err + } + + // init insert folder statement + stmt.insertFolder, err = sqlConn.Prepare(insertFolderQuery) + if err != nil { + return nil, err + } + + // init findFolder statement + stmt.findFolder, err = sqlConn.Prepare(findFolderQuery) + if err != nil { + return nil, err + } + + // init insertFile stmt + stmt.insertFile, err = sqlConn.Prepare(insertFileQuery) + if err != nil { + return nil, err + } + + // init searchFile stmt + stmt.searchFiles, err = sqlConn.Prepare(searchFilesQuery) + if err != nil { + return nil, err + } + + // init getFolder stmt + stmt.getFolder, err = sqlConn.Prepare(getFolderQuery) + if err != nil { + return nil, err + } + + // init dropFolder stmt + stmt.dropFolder, err = sqlConn.Prepare(dropFolderQuery) + if err != nil { + return nil, err + } + + // init dropFiles stmt + stmt.dropFiles, err = sqlConn.Prepare(dropFilesQuery) + if err != nil { + return nil, err + } + + // init getFile stmt + stmt.getFile, err = sqlConn.Prepare(getFileQuery) + if err != nil { + return nil, err + } + + // init searchFolder stmt + stmt.searchFolders, err = sqlConn.Prepare(searchFoldersQuery) + if err != nil { + return nil, err + } + + // init getFilesInFolder stmt + stmt.getFilesInFolder, err = sqlConn.Prepare(getFilesInFolderQuery) + if err != nil { + return nil, err + } + + // init getRandomFiles + stmt.getRandomFiles, err = sqlConn.Prepare(getRandomFilesQuery) + if err != nil { + return nil, err + } + + // init insertFeedback + stmt.insertFeedback, err = sqlConn.Prepare(insertFeedbackQuery) + if err != nil { + return nil, err + } + + return stmt, err +} diff --git a/pkg/database/struct.go b/pkg/database/struct.go new file mode 100644 index 0000000..8abf8c0 --- /dev/null +++ b/pkg/database/struct.go @@ -0,0 +1,30 @@ +package database + +import ( + "path/filepath" +) + +type File struct { + Db *Database `json:"-"` + ID int64 `json:"id"` + Folder_id int64 `json:"folder_id"` + Foldername string `json:"foldername"` + Filename string `json:"filename"` + Filesize int64 `json:"filesize"` +} + +type Folder struct { + Db *Database `json:"-"` + ID int64 `json:"id"` + Folder string `json:"-"` + Foldername string `json:"foldername"` +} + +func (f *File) Path() (string, error) { + folder, err := f.Db.GetFolder(f.Folder_id) + if err != nil { + return "", err + } + return filepath.Join(folder.Folder, f.Filename), nil +} + diff --git a/internal/pkg/tmpfs/tmpfs.go b/pkg/tmpfs/tmpfs.go similarity index 100% rename from internal/pkg/tmpfs/tmpfs.go rename to pkg/tmpfs/tmpfs.go diff --git a/internal/pkg/tmpfs/tmpfs_test.go b/pkg/tmpfs/tmpfs_test.go similarity index 100% rename from internal/pkg/tmpfs/tmpfs_test.go rename to pkg/tmpfs/tmpfs_test.go From ca8b6cb89392e77edd3ada566a326808c23046d9 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Tue, 7 Dec 2021 10:26:50 +0800 Subject: [PATCH 005/104] Add SQL create table statement --- pkg/database/sql_stmt.go | 154 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index 449dd52..735f8b9 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -8,7 +8,8 @@ var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY, folder_id INTEGER NOT NULL, filename TEXT NOT NULL, - filesize INTEGER NOT NULL + filesize INTEGER NOT NULL, + FOREIGN KEY(folder_id) REFERENCES folders(id) );` var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders ( @@ -24,6 +25,69 @@ var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks ( header TEXT NOT NULL );` +var initUsersTableQuery = `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + password TEXT NOT NULL +);` + +var initAvatarsTableQuery = `CREATE TABLE IF NOT EXISTS avatars ( + id INTEGER PRIMARY KEY, + avatarname TEXT NOT NULL, + avatar BLOB NOT NULL +);` + +var initTagsTableQuery = `CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY, + tag TEXT NOT NULL +);` + +var initFileHasTagTableQuery = `CREATE TABLE IF NOT EXISTS file_has_tag ( + file_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + PRIMARY KEY (file_id, tag_id), + FOREIGN KEY(user_id) REFERENCES users(id) + FOREIGN KEY (file_id) REFERENCES files(id), + FOREIGN KEY (tag_id) REFERENCES tags(id) +);` + +var initLikesTableQuery = `CREATE TABLE IF NOT EXISTS likes ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + file_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (file_id) REFERENCES files(id) +);` + +var initReviewsTableQuery = `CREATE TABLE IF NOT EXISTS reviews ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + time INTEGER NOT NULL, + modified_time INTEGER DEFAULT 0, + review TEXT NOT NULL, + header TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) +);` + +var initPlaybacksTableQuery = `CREATE TABLE IF NOT EXISTS playbacks ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + file_id INTEGER NOT NULL, + time INTEGER NOT NULL, + mothod INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (file_id) REFERENCES files(id) +);` + +var initLogsTableQuery = `CREATE TABLE IF NOT EXISTS logs ( + id INTEGER PRIMARY KEY, + time INTEGER NOT NULL, + message TEXT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) +);` + var insertFolderQuery = `INSERT INTO folders (folder, foldername) VALUES (?, ?);` @@ -79,6 +143,14 @@ type Stmt struct { initFilesTable *sql.Stmt initFoldersTable *sql.Stmt initFeedbacksTable *sql.Stmt + initUsersTable *sql.Stmt + initAvatarsTable *sql.Stmt + initTagsTable *sql.Stmt + initFileHasTag *sql.Stmt + initLikesTable *sql.Stmt + initReviewsTable *sql.Stmt + initPlaybacksTable *sql.Stmt + initLogsTable *sql.Stmt insertFolder *sql.Stmt insertFile *sql.Stmt findFolder *sql.Stmt @@ -116,6 +188,54 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { return nil, err } + // init users table + stmt.initUsersTable, err = sqlConn.Prepare(initUsersTableQuery) + if err != nil { + return nil, err + } + + // init avatars table + stmt.initAvatarsTable, err = sqlConn.Prepare(initAvatarsTableQuery) + if err != nil { + return nil, err + } + + // init tags table + stmt.initTagsTable, err = sqlConn.Prepare(initTagsTableQuery) + if err != nil { + return nil, err + } + + // init file_has_tag table + stmt.initFileHasTag, err = sqlConn.Prepare(initFileHasTagTableQuery) + if err != nil { + return nil, err + } + + // init likes table + stmt.initLikesTable, err = sqlConn.Prepare(initLikesTableQuery) + if err != nil { + return nil, err + } + + // init reviews table + stmt.initReviewsTable, err = sqlConn.Prepare(initReviewsTableQuery) + if err != nil { + return nil, err + } + + // init playbacks table + stmt.initPlaybacksTable, err = sqlConn.Prepare(initPlaybacksTableQuery) + if err != nil { + return nil, err + } + + // init logs table + stmt.initLogsTable, err = sqlConn.Prepare(initLogsTableQuery) + if err != nil { + return nil, err + } + // run init statement _, err = stmt.initFilesTable.Exec() if err != nil { @@ -129,6 +249,38 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { if err != nil { return nil, err } + _, err = stmt.initUsersTable.Exec() + if err != nil { + return nil, err + } + _, err = stmt.initAvatarsTable.Exec() + if err != nil { + return nil, err + } + _, err = stmt.initTagsTable.Exec() + if err != nil { + return nil, err + } + _, err = stmt.initFileHasTag.Exec() + if err != nil { + return nil, err + } + _, err = stmt.initLikesTable.Exec() + if err != nil { + return nil, err + } + _, err = stmt.initReviewsTable.Exec() + if err != nil { + return nil, err + } + _, err = stmt.initPlaybacksTable.Exec() + if err != nil { + return nil, err + } + _, err = stmt.initLogsTable.Exec() + if err != nil { + return nil, err + } // init insert folder statement stmt.insertFolder, err = sqlConn.Prepare(insertFolderQuery) From 47a60ae671531ec5b548613eb47c015a7afa50f8 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Tue, 7 Dec 2021 10:42:18 +0800 Subject: [PATCH 006/104] Fix: SQL init user avatar --- pkg/database/sql_stmt.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index 735f8b9..0f7c85f 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -28,7 +28,9 @@ var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks ( var initUsersTableQuery = `CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, username TEXT NOT NULL, - password TEXT NOT NULL + password TEXT NOT NULL, + avatar_id INTEGER NOT NULL, + FOREIGN KEY(avatar_id) REFERENCES avatars(id) );` var initAvatarsTableQuery = `CREATE TABLE IF NOT EXISTS avatars ( From 2d7ac69db591941d805cafeebcd1bce6ebf239fa Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Tue, 7 Dec 2021 14:15:46 +0800 Subject: [PATCH 007/104] Update: init SQL feedbacks tags tmpfs --- pkg/database/sql_stmt.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index 0f7c85f..a29936a 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -21,8 +21,7 @@ var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders ( var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks ( id INTEGER PRIMARY KEY, time INTEGER NOT NULL, - feedback TEXT NOT NULL, - header TEXT NOT NULL + feedback TEXT NOT NULL );` var initUsersTableQuery = `CREATE TABLE IF NOT EXISTS users ( @@ -55,9 +54,9 @@ var initFileHasTagTableQuery = `CREATE TABLE IF NOT EXISTS file_has_tag ( );` var initLikesTableQuery = `CREATE TABLE IF NOT EXISTS likes ( - id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, file_id INTEGER NOT NULL, + PRIMARY KEY (user_id, file_id), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (file_id) REFERENCES files(id) );` @@ -90,6 +89,17 @@ var initLogsTableQuery = `CREATE TABLE IF NOT EXISTS logs ( FOREIGN KEY (user_id) REFERENCES users(id) );` +var initTmpfsTableQuery = `CREATE TABLE IF NOT EXISTS tmpfs ( + id INTEGER PRIMARY KEY, + path TEXT NOT NULL, + size INTEGER NOT NULL, + file_id INTEGER NOT NULL, + ffmpeg_config TEXT NOT NULL, + created_time INTEGER NOT NULL, + accessed_time INTEGER NOT NULL, + FOREIGN KEY (file_id) REFERENCES files(id) +);` + var insertFolderQuery = `INSERT INTO folders (folder, foldername) VALUES (?, ?);` @@ -153,6 +163,7 @@ type Stmt struct { initReviewsTable *sql.Stmt initPlaybacksTable *sql.Stmt initLogsTable *sql.Stmt + initTmpfsTable *sql.Stmt insertFolder *sql.Stmt insertFile *sql.Stmt findFolder *sql.Stmt @@ -238,6 +249,12 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { return nil, err } + // init tmpfs table + stmt.initTmpfsTable, err = sqlConn.Prepare(initTmpfsTableQuery) + if err != nil { + return nil, err + } + // run init statement _, err = stmt.initFilesTable.Exec() if err != nil { @@ -283,6 +300,10 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { if err != nil { return nil, err } + _, err = stmt.initTmpfsTable.Exec() + if err != nil { + return nil, err + } // init insert folder statement stmt.insertFolder, err = sqlConn.Prepare(insertFolderQuery) From e961c10d4e8c6d9d53409b098443ba6a3fcbea0d Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Tue, 7 Dec 2021 23:23:43 +0800 Subject: [PATCH 008/104] Add: ER Diagram (draw.io) --- docs/ER Diagram.drawio | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/ER Diagram.drawio diff --git a/docs/ER Diagram.drawio b/docs/ER Diagram.drawio new file mode 100644 index 0000000..72219d3 --- /dev/null +++ b/docs/ER Diagram.drawio @@ -0,0 +1 @@ +7V3blpu2Gn6auYwXOiFxmaSHdLdJm5Xu3fYqi9iMTWIbb8wkM336igyyQZKxBoQksnqTAIMx/vXpPx9u0Mvd/Y9leti8LlbZ9gZGq/sb9N0NhJgw/m994eHxAo2jxwvrMl89XgLnC+/yv7PmorjtLl9lx86NVVFsq/zQvbgs9vtsWXWupWVZfOnedltsu996SNeZcuHdMt2qV//IV9WmuQri5PyHV1m+3jRfzSB9/MMuFTc3v+S4SVfFl9Yl9P0NelkWRfV4tLt/mW1r2gm6PH7uhwt/Pb1Yme0rkw/s/ni22pQf36Dqd3Q8/Pnbbfp29ax5yud0e9f84Nt8y+n9+MbVgyDDl01eZe8O6bI+/8JX+ga92FS7LT8D/DDd5us9P17yd8lKfkF9OfFNWVll961Lzcv+mBW7rCof+C3NX5lASoMcIiDxpbUOUXNt01oC3FxLm6Vfnx59pg4/aAj0FGKdlvJMmGzF4dKc7os9/+9FWdztV1n9oIifFWW1KdbFPt3+UhSHhl4fs6p6aMCe3lVFl5rZfvW8hu75kfzKD3n9sl8fmd3n1Z/iXn78V319AUlz+t19677vHpqTiytyLO7KZdbzs1Gz7dJynVV95AHNnqhp0rvCZbZNq/xzd4tZXy6kYpvv/qwMEt0U+kY3wb3gPuP4+/PV6bF+rMriU/ay2Bbl+ZbhWCaGWEZBIRng/rXRMp7jp6xaboavzEleRUbLlJaVuGO5TY/HfCkuN7cBJ0yI2F645qO/FTl/5fP2jbrbF0RU2pePb9p8TFr/03uM2K0Kc7uB8ZZT5sUq/8wP1/UhV4pWd8vqKP7Ev6n1VwVSfMUP9WG5KXYf7vjveHHIypy/ac30xNXfzpde+OCbpEv4WMc3oYZvxlPxTQaDUArE4m3z/adRO40a7jQaFo+kyoa4OwYi6wHuYhZ6l/UIKHRxprWK40etlU6ltTJDGAMYFIxRGNxEv1je1wqEtVYo4LWazBw0XqwoqMViinz46d1zZf2qMk/366+mzRVBURYVf+uilhXPkl6KPkFQyC4PpAqKWCMn2GQej0ih2nOOwYddcReGZDVQBt26iFSH2pui3KX1B2t9JAii4dCoBvuFnjfXg5FNe2K/C8JohwUn3DTo48F8CcuHP9snLTFbn54/9vXsofWQlhk2kp8LBegqQ4fWpe/Xj3IKpw+tGw61yXxsPVkyvRFMOvClXb83P3h8olVzWxCpta9fp/t0HcaOhjS0HQ1U5+u2WAchM2LShU/km1aQBMn9bPkTADbkLyAsh4J47ysutjJLq+wbcrCBROIliW8HG1SV0CoNg5dgWWEn3pmJqoAKiM4QjIh0mbV/MGLVcxYesx6qWA5n8sjUayyAHwiTJ4nn5TzbFecV7VgVT3DAnZYddJcdXlv3jqXTAKxt5jjCBrUNDb0BAaUQEo4kZnEhdPdUQ0XWygGVEnSu3A+RA8MGqXGTdU3aWUoLOSiLvUsL346Ni+zlgoS4yir0uUcWfM3P41+ef/zj181Pxzcf376K8cc3P796Bo1zjyZhHU/d8ZQ9bcfL9xMnroxIlXnTxyhOeDsrIX+1sXgFbh0xOFFgQ3CLNti0qHQkpwiiCxYBShFDAAECOlhBkC5AjJIIYshYgrCRDLMGIuF59x37Gourc5wsCGgxN9CKI7iIMI4ppYRgEscKtDDgxhfCXCdhMCKOsdVvY80HWzQobFnPzr2ALUIWSRJFgEUwigiQsQUWMUkSGsc0SZDIlncGrUBC9iOhBULClf1A/wVtCXCJhxBjJOY8iyFJHgK84HBiMUYIxHHC3ApErDopy+xznn0J0k8JvPspMe3diOFZRmEZRtivYaTmQ0s5BUQGzuMvGu1ToZLvRgD0ooUl3U9c+FTwHAJ6M8G5JniovxG6ATpOhgFdte7k0JH8oKkFVhw6AwbDgGk9ZdUYmD6d2oi6YcCQ9TLgaRgq6TcIw2CoRljSb0XTlFocVoEl8e3dHpW2Z1X+TVbGYBxXdeQ5UoJncCI+IwfDyNOCZ6gpZ51W0VOTyjepavDNMXaGYCPgvMXOSL+HOQAFReIcNCzVGZqyDlcmoly5KVct2DIRYS8nuHo/caHRANDv/wjdETlC2UlUWOrdio4055ixBQE4IphgCBPR2EWgFLMFiFAEQMQSECWo+/ipjbREETCHbfrwIV1+CtKviJBGarjtYqEWxt9m2SoYiklqCga+CRarYnZyrjOcexBvuWTjUKmvTt+nOzXTij8uPxwzP2qgJAtPBYn+4NlvY7qSk8MRG4NZIla8dn/1w4FryF+KcmVY/+AV2lKtqP8mDPGApj5hQds0CBUYtNXaNH6/PazeFvtKLCK2z5NPKoQ/4LK5A9e0DRix3k5qHOFV5XaTpStNEWpInBbF8YL4hmwY5vYIyMYzhWysQFbYY2GBVo6fRb4NM9rv/5wBZDWent4Cz1AgqzpdgtYPSHDV/3TuRhs1NdoCgy5VrbYqXYfFaGWfoX+4sjDyzSfvoUqNc1asd2IwSyZhibTEF0JCttzrVLUE5xq/BUjy3HmvfaT9pdXOu+MtXBRjmG+xsHwibO7KpjnhrSfKjNslapeZ9HPKf0UQ0Sq5VZx31xPtz1qdAU5NXU8CGKHgVHU9BW0UScDVNNJ0DFw1Avi4z8OLAcqb3vvcFaqm2T3SLiy6QdxtVuQdc2zuASbmrcppHN1nFmBKJNsBe/d9srmLeWYq5gNDriZ7Kt9m4ckomgQH2bkHRZm3fOFRdE/CsFyHVq2NWC/T8EpYbVXFaytj2cJiMInkmvQ/nw2AuXOYxDSqEhZixWu3k7Dr4Z5h49W7qyaZd6uVETCHpoI0MJxDBedBmy2ncEYwkAcoDBbdwjx0BfqZdnlPVP97lYdm7YDggG6hxYnV+aQmHYwHl3YO6so1WV0n8uZa0AfxY7nFDjNrF3i1QJTJoWlLBaJx0vXTkm5p+UQFn5p+KzPYD1f3tN7uHdTW1caOeXV3/O/9rw+/b9++en34/uX/1veHt89MzQ5PrVoYMMO5mj4j+23l9BlbjcyljC0M+yuq5fvF+cQV1TNL25Qttti/VNeMhZ2Tyea4qS+AGkNPX8ruSBSDiMaLqK5hRwwSLkBZV9BFeEExTur2mAAR076+mq+RKzzkCQoT5+2BUKbk6p2+w2AK/MPUesrJBfwgABaAwgQlNGEACxtMMMIYLzCOEUlgghmFzAil9rAViN9qyLhs91GIJ6ArdoMuWa5CQ63IHoDCSLuwqZlPNkz6CfCxXtd2gTmBiCySKEkYRIgmkHaZE0XRIkJfR3VQbkgKQeQKW4JcbXfV7nAbRrJoJFcq+4+ZiVmxvveiNgm9X+K3tGDaUYOv6RdGu1G/x4zzrQQOJ9+NfMMtYppgGjEaRVR8r7BEGVvEmESARiRCCZYU2snV0EByD8ahyxW4NJzeM7gAoYuYq5sMQgYpExHLlrmU1E3BKDemACTAMbjCKFmX24YPKKeZNASrx5ppR113WONm+QLDCLKEEcIZmeSDjhKuVjR/4rYPNeNkmq+RkrdPJRuWfYIgkhuuOpldhgPhuJdk83QBVoBM870Ci7CeXryls+6y4zFdBxZlld3pIYRZ1XyCcIJGlrPDADZNQA2sxcnpxecSeZC8t1g0IvOH8yu9nrVsfTro9zWNnyYL+KxVIRK3dfhFdMX1Up+0Ss/HVieaihj7kwguqBlyu/nRYy2s6+lXhlM4xu5Atm1tlgXQ9XeeBxdX1ZTj3YddXinLO3k/iC5YLHD8WKpMBNqObEDD87mFTMZTvG+MrhupaWdIAunS0aWC2DcuVhk6aJGMFsiGpdkSLt3CfZNQg875JLIz3TfRBHdom3HFKr/Ns9X7AMknuSe8b1WRA9IWL7V4D4pqibRTgcsa9l6vcdjFN1I16olE/sjmtATdUvjQ55Da3oSYdq3i7e6Qrd8vi/1tHlhjPbmMHzKHdn1vzL7duGO5zI7HIEUGk1uoMe/4U5vnLsssrcIknzKa1zv8VAsjPLIpep5/TUW1KtbZPis57hTSzaI1opyuSnTieareiPrZbP3+zqlm2w3IE7hIf3vV3uJG944g/dr0+/OmXpvWylipiNE3YTBcGm+9EvUroyq1nIxzZEmEdh1JTru16mmrqroXKds4iVs06sJ/SD2kvvJyysxlQfKrmafQNJ6uk9hja8Xktg6GQzPVufXXHmQvc1VPRFWV/XbwpZ8QDw3xhT3iK5ZsbtOx8cZTVJ3hS6PrfxuywekkZj1tVYNg+N61OnZ9Kv3IeEgtNK0Z/lc2XCbilTTqb8IaMg6L28+sGLX3RUmwTbk9LHfYlV4ITe1W49kvE+x9KreEGbr32bUHWcoXxkBKeKZ204X14IVTCK6nFUy4ZTOm2DXNvZkAukqchxpOfdE9STKjI9eSSxMgyrfZ+016fK8bvCTAlt3zxXmx4y+X1+oj4ier4u7D19uAH7VTHsvktKNub6OjFnGL/UWSnlX6DiUt6/f2Z7dJyn7iW9lHMzXUdVmQRny0d2NfNQCQR05KpS3LDNtwXNUmmGPjHdk0MENzDpm2pOrFoQ94YSmrZXCfKjn/T8HpxPASaJ5RMgyWp9V7jwpjA0NQzTI3sAWf0Ktp0kLScY4gPMAYJJr1I3b2LpFFAxy4d+NLvUdd7V3VhNvmn7LLQxpnoFr7z+XFAyOpc1attXOsnKrWWLUW1/UW/7bojuRsf/90t2nSjO5NaLk1of4Xa3oEB6dfEnRFLTSWUdceNLWMUmOPfarRubz0iZ7y0Dq66FBmzVP+r3LUAV6fFalJD/7w4FmqTCBI3PrGKvrL+5/L8j/38B39+Yf809//Xz9TddHhcmTMDnfW3dG0Htw0T3AKL4Wc3GvYNV7lCtceNLUUUd3dZsPo56QcYrmeX+fXcDqmnqnCuw7hhFebxySphWJtEfdkdqSOIRr4bQcqPkO8t1OwxZHtxYLWe06bcazegydUuHWw05QiW8Kdm/Qy40bbbnCBRG2yUGOH6sNIwkVsTYTy07IoqvbtnOluXherrL7jHw== \ No newline at end of file From 05a569e3953ab2141a22fe96d005b3861daff9b7 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Thu, 9 Dec 2021 11:21:08 +0800 Subject: [PATCH 009/104] Fix: feedbacks's column header --- pkg/database/sql_stmt.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index a29936a..e1ebf95 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -67,7 +67,6 @@ var initReviewsTableQuery = `CREATE TABLE IF NOT EXISTS reviews ( time INTEGER NOT NULL, modified_time INTEGER DEFAULT 0, review TEXT NOT NULL, - header TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) );` @@ -148,8 +147,8 @@ JOIN folders ON files.folder_id = folders.id ORDER BY RANDOM() LIMIT ?;` -var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header) -VALUES (?, ?, ?);` +var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback) +VALUES (?, ?);` type Stmt struct { initFilesTable *sql.Stmt From f3a95973e98f1e6204e65d3c888ff19686971613 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Sat, 11 Dec 2021 18:47:25 +0800 Subject: [PATCH 010/104] Add: Simple user login/register function --- pkg/api/api.go | 3 ++ pkg/api/handle_error.go | 8 +++- pkg/api/handle_user.go | 83 +++++++++++++++++++++++++++++++++ pkg/database/method_user.go | 31 ++++++++++++ pkg/database/sql_stmt.go | 66 +++++++++++++++++++++++++- pkg/database/struct.go | 9 +++- web/src/App.js | 16 ++++--- web/src/component/Login.js | 65 ++++++++++++++++++++++++++ web/src/component/Manage.js | 19 +++++++- web/src/component/Register.js | 76 ++++++++++++++++++++++++++++++ web/src/component/UserStatus.js | 16 +++++++ 11 files changed, 379 insertions(+), 13 deletions(-) create mode 100644 pkg/api/handle_user.go create mode 100644 pkg/database/method_user.go create mode 100644 web/src/component/Login.js create mode 100644 web/src/component/Register.js create mode 100644 web/src/component/UserStatus.js diff --git a/pkg/api/api.go b/pkg/api/api.go index 0774fd8..ba7c2f9 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -70,6 +70,9 @@ func NewAPI(config Config) (*API, error) { apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo) apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect) apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect) + // user + apiMux.HandleFunc("/login", api.HandleLogin) + apiMux.HandleFunc("/register", api.HandleRegister) // below needs token apiMux.HandleFunc("/walk", api.HandleWalk) apiMux.HandleFunc("/reset", api.HandleReset) diff --git a/pkg/api/handle_error.go b/pkg/api/handle_error.go index 3f80bab..818eca9 100644 --- a/pkg/api/handle_error.go +++ b/pkg/api/handle_error.go @@ -6,6 +6,10 @@ import ( "net/http" ) +type Error struct { + Error string `json:"error,omitempty"` +} + func (api *API) HandleError(w http.ResponseWriter, r *http.Request, err error) { api.HandleErrorString(w, r, err.Error()) } @@ -20,8 +24,8 @@ func (api *API) HandleErrorString(w http.ResponseWriter, r *http.Request, errorS func (api *API) HandleErrorStringCode(w http.ResponseWriter, r *http.Request, errorString string, code int) { log.Println("[api] [Error]", code, errorString) - errStatus := &Status{ - Status: errorString, + errStatus := &Error{ + Error: errorString, } w.WriteHeader(code) json.NewEncoder(w).Encode(errStatus) diff --git a/pkg/api/handle_user.go b/pkg/api/handle_user.go new file mode 100644 index 0000000..92d19be --- /dev/null +++ b/pkg/api/handle_user.go @@ -0,0 +1,83 @@ +package api + +import ( + "encoding/json" + "log" + "msw-open-music/pkg/database" + "net/http" +) + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type LoginResponse struct { + User *database.User `json:"user"` +} + +func (api *API) HandleLogin(w http.ResponseWriter, r *http.Request) { + // Get method will login as anonymous user + if r.Method == "GET" { + log.Println("Login as anonymous user") + user, err := api.Db.LoginAsAnonymous() + if err != nil { + api.HandleError(w, r, err) + return + } + resp := &LoginResponse{ + User: user, + } + err = json.NewEncoder(w).Encode(resp) + return + } + + var request LoginRequest + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + api.HandleError(w, r, err) + return + } + + log.Println("Login as user", request.Username) + + user, err := api.Db.Login(request.Username, request.Password) + if err != nil { + api.HandleError(w, r, err) + return + } + + resp := &LoginResponse{ + User: user, + } + err = json.NewEncoder(w).Encode(resp) + if err != nil { + api.HandleError(w, r, err) + return + } +} + +type RegisterRequest struct { + Username string `json:"username"` + Password string `json:"password"` + Role int64 `json:"role"` +} + +func (api *API) HandleRegister(w http.ResponseWriter, r *http.Request) { + var request RegisterRequest + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + api.HandleError(w, r, err) + return + } + + log.Println("Register user", request.Username) + + err = api.Db.Register(request.Username, request.Password, request.Role) + if err != nil { + api.HandleError(w, r, err) + return + } + + api.HandleOK(w, r) +} diff --git a/pkg/database/method_user.go b/pkg/database/method_user.go new file mode 100644 index 0000000..ecd3f5c --- /dev/null +++ b/pkg/database/method_user.go @@ -0,0 +1,31 @@ +package database + +func (database *Database) Login(username string, password string) (*User, error) { + user := &User{} + + // get user from database + err := database.stmt.getUser.QueryRow(username, password).Scan(&user.ID, &user.Username, &user.Role, &user.AvatarId) + if err != nil { + return user, err + } + return user, nil +} + +func (database *Database) LoginAsAnonymous() (*User, error) { + user := &User{} + + // get user from database + err := database.stmt.getAnonymousUser.QueryRow().Scan(&user.ID, &user.Username, &user.Role, &user.AvatarId) + if err != nil { + return user, err + } + return user, nil +} + +func (database *Database) Register(username string, password string, usertype int64) (error) { + _, err := database.stmt.insertUser.Exec(username, password, usertype, 0) + if err != nil { + return err + } + return nil +} diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index 2a5e354..e4bad08 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -25,10 +25,13 @@ var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks ( header TEXT NOT NULL );` +// User table schema definition +// role: 0 - Anonymous User, 1 - Admin, 2 - User var initUsersTableQuery = `CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - username TEXT NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, + role INTEGER NOT NULL, avatar_id INTEGER NOT NULL, FOREIGN KEY(avatar_id) REFERENCES avatars(id) );` @@ -153,6 +156,17 @@ LIMIT ?;` var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header) VALUES (?, ?, ?);` +var insertUserQuery = `INSERT INTO users (username, password, role, avatar_id) +VALUES (?, ?, ?, ?);` + +var countUserQuery = `SELECT count(*) FROM users;` + +var countAdminQuery = `SELECT count(*) FROM users WHERE role= 1;` + +var getUserQuery = `SELECT id, username, role, avatar_id FROM users WHERE username = ? AND password = ? LIMIT 1;` + +var getAnonymousUserQuery = `SELECT id, username, role, avatar_id FROM users WHERE role = 0 LIMIT 1;` + type Stmt struct { initFilesTable *sql.Stmt initFoldersTable *sql.Stmt @@ -179,6 +193,11 @@ type Stmt struct { getFilesInFolder *sql.Stmt getRandomFiles *sql.Stmt insertFeedback *sql.Stmt + insertUser *sql.Stmt + countUser *sql.Stmt + countAdmin *sql.Stmt + getUser *sql.Stmt + getAnonymousUser *sql.Stmt } func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { @@ -386,5 +405,48 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { return nil, err } + // init insertUser + stmt.insertUser, err = sqlConn.Prepare(insertUserQuery) + if err != nil { + return nil, err + } + + // init countUser + stmt.countUser, err = sqlConn.Prepare(countUserQuery) + if err != nil { + return nil, err + } + + // init countAdmin + stmt.countAdmin, err = sqlConn.Prepare(countAdminQuery) + if err != nil { + return nil, err + } + + // init getUser + stmt.getUser, err = sqlConn.Prepare(getUserQuery) + if err != nil { + return nil, err + } + + // init getAnonymousUser + stmt.getAnonymousUser, err = sqlConn.Prepare(getAnonymousUserQuery) + if err != nil { + return nil, err + } + + // insert Anonymous user if users is empty + userCount := 0 + err = stmt.countUser.QueryRow().Scan(&userCount) + if err != nil { + return nil, err + } + if userCount == 0 { + _, err = stmt.insertUser.Exec("Anonymous user", "", 0, 0) + if err != nil { + return nil, err + } + } + return stmt, err } diff --git a/pkg/database/struct.go b/pkg/database/struct.go index 8abf8c0..f4c56dc 100644 --- a/pkg/database/struct.go +++ b/pkg/database/struct.go @@ -20,6 +20,14 @@ type Folder struct { Foldername string `json:"foldername"` } +type User struct { + ID int64 `json:"id"` + Username string `json:"username"` + Password string `json:"-"` + Role int64 `json:"role"` + AvatarId int64 `json:"avatar_id"` +} + func (f *File) Path() (string, error) { folder, err := f.Db.GetFolder(f.Folder_id) if err != nil { @@ -27,4 +35,3 @@ func (f *File) Path() (string, error) { } return filepath.Join(folder.Folder, f.Filename), nil } - diff --git a/web/src/App.js b/web/src/App.js index 2896642..afc4b5a 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -1,9 +1,4 @@ -import { - HashRouter as Router, - Routes, - Route, - NavLink, -} from "react-router-dom"; +import { HashRouter as Router, Routes, Route, NavLink } from "react-router-dom"; import "./App.css"; import GetRandomFiles from "./component/GetRandomFiles"; @@ -12,11 +7,15 @@ import SearchFolders from "./component/SearchFolders"; import FilesInFolder from "./component/FilesInFolder"; import Manage from "./component/Manage"; import Share from "./component/Share"; +import Login from "./component/Login"; +import Register from "./component/Register"; import AudioPlayer from "./component/AudioPlayer"; +import UserStatus from "./component/UserStatus"; import { useState } from "react"; function App() { const [playingFile, setPlayingFile] = useState({}); + const [user, setUser] = useState({}); return (
@@ -24,6 +23,7 @@ function App() {

logo MSW Open Music Project +

+ ); +} + +export default SingleReview; diff --git a/web/src/component/ReviewPage.js b/web/src/component/ReviewPage.js index 2e3d74c..817bf05 100644 --- a/web/src/component/ReviewPage.js +++ b/web/src/component/ReviewPage.js @@ -1,10 +1,11 @@ import { useState, useEffect } from "react"; -import { useParams } from "react-router"; +import { useParams, useNavigate } from "react-router"; import { Link } from "react-router-dom"; import { convertIntToDateTime } from "./Common"; -function ReviewPage() { +function ReviewPage(props) { let params = useParams(); + let navigate = useNavigate(); const [newReview, setNewReview] = useState(""); const [reviews, setReviews] = useState([]); @@ -61,10 +62,22 @@ function ReviewPage() { {reviews.map((review) => (

- @{review.user.username} wrote on{" "} - {convertIntToDateTime(review.created_at)}{" "} + + @{review.user.username} + {" "} + wrote on {convertIntToDateTime(review.created_at)}{" "}

{review.content}

+ {(props.user.role === 1 || review.user.id === props.user.id) && + props.user.role != 0 && ( + + )}
))} From f32c922fafae6c11893cba0ce061365657581e79 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 05:52:10 +0800 Subject: [PATCH 029/104] Add: delete review --- pkg/api/api.go | 1 + pkg/api/handle_review.go | 61 +++++++++++++++++++++++++++++++++ pkg/api/handle_user.go | 1 - pkg/database/method_review.go | 5 +++ pkg/database/sql_stmt.go | 9 +++++ web/src/component/EditReview.js | 26 +++++++++++++- 6 files changed, 101 insertions(+), 2 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 28eb51c..ceafb0c 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -97,6 +97,7 @@ func NewAPI(config Config) (*API, error) { apiMux.HandleFunc("/get_reviews_on_file", api.HandleGetReviewsOnFile) apiMux.HandleFunc("/get_review", api.HandleGetReview) apiMux.HandleFunc("/update_review", api.HandleUpdateReview) + apiMux.HandleFunc("/delete_review", api.HandleDeleteReview) // below needs token apiMux.HandleFunc("/walk", api.HandleWalk) apiMux.HandleFunc("/reset", api.HandleReset) diff --git a/pkg/api/handle_review.go b/pkg/api/handle_review.go index 0d25682..e9b0d73 100644 --- a/pkg/api/handle_review.go +++ b/pkg/api/handle_review.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "msw-open-music/pkg/database" "net/http" "time" @@ -102,6 +103,32 @@ func (api *API) HandleGetReview(w http.ResponseWriter, r *http.Request) { } } +func (api *API) CheckUserCanModifyReview(w http.ResponseWriter, r *http.Request, reviewID int64) error { + review, err := api.Db.GetReview(reviewID) + if err != nil { + return err + } + + err = api.CheckNotAnonymous(w, r) + if err != nil { + return err + } + + err = api.CheckAdmin(w, r) + if err != nil { + userID, err := api.GetUserID(w, r) + if err != nil { + return err + } + + if review.UserId != userID { + return errors.New("you are not allowed to modify this review") + } + } + + return nil +} + func (api *API) HandleUpdateReview(w http.ResponseWriter, r *http.Request) { req := &database.Review{} @@ -111,6 +138,12 @@ func (api *API) HandleUpdateReview(w http.ResponseWriter, r *http.Request) { return } + err = api.CheckUserCanModifyReview(w, r, req.ID) + if err != nil { + api.HandleError(w, r, err) + return + } + req.UpdatedAt = time.Now().Unix() err = api.Db.UpdateReview(req) @@ -121,3 +154,31 @@ func (api *API) HandleUpdateReview(w http.ResponseWriter, r *http.Request) { api.HandleOK(w, r) } + +type DeleteReviewRequest struct { + ID int64 `json:"id"` +} + +func (api *API) HandleDeleteReview(w http.ResponseWriter, r *http.Request) { + req := &DeleteReviewRequest{} + + err := json.NewDecoder(r.Body).Decode(req) + if err != nil { + api.HandleError(w, r, err) + return + } + + err = api.CheckUserCanModifyReview(w, r, req.ID) + if err != nil { + api.HandleError(w, r, err) + return + } + + err = api.Db.DeleteReview(req.ID) + if err != nil { + api.HandleError(w, r, err) + return + } + + api.HandleOK(w, r) +} diff --git a/pkg/api/handle_user.go b/pkg/api/handle_user.go index 1bb54ff..105974a 100644 --- a/pkg/api/handle_user.go +++ b/pkg/api/handle_user.go @@ -161,7 +161,6 @@ func (api *API) CheckAdmin(w http.ResponseWriter, r *http.Request) error { return ErrNotAdmin } - w.WriteHeader(http.StatusOK) return nil } diff --git a/pkg/database/method_review.go b/pkg/database/method_review.go index 915456f..f6d29c9 100644 --- a/pkg/database/method_review.go +++ b/pkg/database/method_review.go @@ -66,3 +66,8 @@ func (database *Database) UpdateReview(review *Review) error { review.ID) return err } + +func (database *Database) DeleteReview(reviewId int64) error { + _, err := database.stmt.deleteReview.Exec(reviewId) + return err +} diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index 4dce78e..da3f0cd 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -222,6 +222,8 @@ var getReviewQuery = `SELECT id, file_id, user_id, created_at, updated_at, conte var updateReviewQuery = `UPDATE reviews SET content = ?, updated_at = ? WHERE id = ?;` +var deleteReviewQuery = `DELETE FROM reviews WHERE id = ?;` + type Stmt struct { initFilesTable *sql.Stmt initFoldersTable *sql.Stmt @@ -266,6 +268,7 @@ type Stmt struct { getReviewsOnFile *sql.Stmt getReview *sql.Stmt updateReview *sql.Stmt + deleteReview *sql.Stmt } func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { @@ -594,5 +597,11 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { return nil, err } + // init deleteReview + stmt.deleteReview, err = sqlConn.Prepare(deleteReviewQuery) + if err != nil { + return nil, err + } + return stmt, err } diff --git a/web/src/component/EditReview.js b/web/src/component/EditReview.js index e335a7d..bf1fb70 100644 --- a/web/src/component/EditReview.js +++ b/web/src/component/EditReview.js @@ -56,6 +56,27 @@ function SingleReview() { }); } + function deleteReview() { + fetch("/api/v1/delete_review", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + id: parseInt(params.id), + }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + alert(data.error); + } else { + alert("Review deleted!"); + navigate(-1); + } + }); + } + useEffect(() => { refresh(); }, []); @@ -67,7 +88,10 @@ function SingleReview() { value={review.content} onChange={(e) => setReview({ ...review, content: e.target.value })} > - +
+ + +
); } From 164dd0f2827a90c0937e7e8acb1f8ba4c6368751 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 06:18:14 +0800 Subject: [PATCH 030/104] Add: show reviews created by user --- pkg/api/api.go | 1 + pkg/api/handle_user.go | 25 ++++++++++++++++ pkg/database/method_review.go | 32 ++++++++++++++++++++ pkg/database/sql_stmt.go | 18 +++++++++++ web/src/App.js | 5 ++++ web/src/component/ReviewEntry.js | 30 +++++++++++++++++++ web/src/component/ReviewPage.js | 26 ++-------------- web/src/component/UserProfile.js | 51 ++++++++++++++++++++++++++++++++ 8 files changed, 165 insertions(+), 23 deletions(-) create mode 100644 web/src/component/ReviewEntry.js create mode 100644 web/src/component/UserProfile.js diff --git a/pkg/api/api.go b/pkg/api/api.go index ceafb0c..cc81fdc 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -98,6 +98,7 @@ func NewAPI(config Config) (*API, error) { apiMux.HandleFunc("/get_review", api.HandleGetReview) apiMux.HandleFunc("/update_review", api.HandleUpdateReview) apiMux.HandleFunc("/delete_review", api.HandleDeleteReview) + apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser) // below needs token apiMux.HandleFunc("/walk", api.HandleWalk) apiMux.HandleFunc("/reset", api.HandleReset) diff --git a/pkg/api/handle_user.go b/pkg/api/handle_user.go index 105974a..0fd8751 100644 --- a/pkg/api/handle_user.go +++ b/pkg/api/handle_user.go @@ -192,3 +192,28 @@ func (api *API) GetUserID(w http.ResponseWriter, r *http.Request) (int64, error) return userId.(int64), nil } + +type GetReviewsByUserRequest struct { + ID int64 `json:"id"` +} + +func (api *API) HandleGetReviewsByUser(w http.ResponseWriter, r *http.Request) { + req := &GetReviewsByUserRequest{} + err := json.NewDecoder(r.Body).Decode(req) + if err != nil { + api.HandleError(w, r, err) + return + } + + reviews, err := api.Db.GetReviewsByUser(req.ID) + if err != nil { + api.HandleError(w, r, err) + return + } + + err = json.NewEncoder(w).Encode(reviews) + if err != nil { + api.HandleError(w, r, err) + return + } +} diff --git a/pkg/database/method_review.go b/pkg/database/method_review.go index f6d29c9..748e3f7 100644 --- a/pkg/database/method_review.go +++ b/pkg/database/method_review.go @@ -71,3 +71,35 @@ func (database *Database) DeleteReview(reviewId int64) error { _, err := database.stmt.deleteReview.Exec(reviewId) return err } + +func (database *Database) GetReviewsByUser(userId int64) ([]*Review, error) { + rows, err := database.stmt.getReviewsByUser.Query(userId) + if err != nil { + return nil, err + } + defer rows.Close() + + reviews := make([]*Review, 0) + for rows.Next() { + review := &Review{ + User: &User{}, + File: &File{}, + } + err := rows.Scan( + &review.ID, + &review.CreatedAt, + &review.UpdatedAt, + &review.Content, + &review.User.ID, + &review.User.Username, + &review.User.Role, + &review.User.AvatarId, + &review.File.ID, + &review.File.Filename) + if err != nil { + return nil, err + } + reviews = append(reviews, review) + } + return reviews, nil +} diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index da3f0cd..0afbbb0 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -224,6 +224,17 @@ var updateReviewQuery = `UPDATE reviews SET content = ?, updated_at = ? WHERE id var deleteReviewQuery = `DELETE FROM reviews WHERE id = ?;` +var getReviewsByUserQuery = `SELECT +reviews.id, reviews.created_at, reviews.updated_at, reviews.content, +users.id, users.username, users.role, users.avatar_id, +files.id, files.filename +FROM reviews +JOIN users ON reviews.user_id = users.id +JOIN files ON reviews.file_id = files.id +WHERE reviews.user_id = ? +ORDER BY reviews.created_at +;` + type Stmt struct { initFilesTable *sql.Stmt initFoldersTable *sql.Stmt @@ -269,6 +280,7 @@ type Stmt struct { getReview *sql.Stmt updateReview *sql.Stmt deleteReview *sql.Stmt + getReviewsByUser *sql.Stmt } func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { @@ -603,5 +615,11 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { return nil, err } + // init getReviewsByUser + stmt.getReviewsByUser, err = sqlConn.Prepare(getReviewsByUserQuery) + if err != nil { + return nil, err + } + return stmt, err } diff --git a/web/src/App.js b/web/src/App.js index 0129e5f..2b79f9c 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -16,6 +16,7 @@ import EditReview from "./component/EditReview"; import AudioPlayer from "./component/AudioPlayer"; import UserStatus from "./component/UserStatus"; import ReviewPage from "./component/ReviewPage"; +import UserProfile from "./component/UserProfile"; import { useState } from "react"; function App() { @@ -88,6 +89,10 @@ function App() { path="/manage/reviews/:id" element={} /> + } + /> } diff --git a/web/src/component/ReviewEntry.js b/web/src/component/ReviewEntry.js new file mode 100644 index 0000000..b5cf648 --- /dev/null +++ b/web/src/component/ReviewEntry.js @@ -0,0 +1,30 @@ +import { Link } from "react-router-dom"; +import { useNavigate } from "react-router"; +import { convertIntToDateTime } from "./Common"; + +function ReviewEntry(props) { + let navigate = useNavigate(); + return ( +
+

+ + @{props.review.user.username} + {" "} + wrote on {convertIntToDateTime(props.review.created_at)}{" "} +

+

{props.review.content}

+ {(props.user.role === 1 || props.review.user.id === props.user.id) && + props.user.role != 0 && ( + + )} +
+ ); +} + +export default ReviewEntry; diff --git a/web/src/component/ReviewPage.js b/web/src/component/ReviewPage.js index 817bf05..fba6bf2 100644 --- a/web/src/component/ReviewPage.js +++ b/web/src/component/ReviewPage.js @@ -1,11 +1,9 @@ import { useState, useEffect } from "react"; -import { useParams, useNavigate } from "react-router"; -import { Link } from "react-router-dom"; -import { convertIntToDateTime } from "./Common"; +import { useParams } from "react-router"; +import ReviewEntry from "./ReviewEntry"; function ReviewPage(props) { let params = useParams(); - let navigate = useNavigate(); const [newReview, setNewReview] = useState(""); const [reviews, setReviews] = useState([]); @@ -60,25 +58,7 @@ function ReviewPage(props) {

Review Page

{reviews.map((review) => ( -
-

- - @{review.user.username} - {" "} - wrote on {convertIntToDateTime(review.created_at)}{" "} -

-

{review.content}

- {(props.user.role === 1 || review.user.id === props.user.id) && - props.user.role != 0 && ( - - )} -
+ ))}
diff --git a/web/src/component/UserProfile.js b/web/src/component/UserProfile.js new file mode 100644 index 0000000..96ad64f --- /dev/null +++ b/web/src/component/UserProfile.js @@ -0,0 +1,51 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router"; +import ReviewEntry from "./ReviewEntry"; + +function UserProfile(props) { + let params = useParams(); + const [reviews, setReviews] = useState([]); + + function getReviews() { + fetch("/api/v1/get_reviews_by_user", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + id: parseInt(params.id), + }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + alert(data.error); + } else { + setReviews(data); + } + }); + } + + useEffect(() => { + getReviews(); + }, []); + + return ( +
+

User Profile

+
+

Reviews

+ {reviews.map((review) => ( + + ))} +
+
+ ); +} + +export default UserProfile; From 7a10922ec441cc9a303939b668ff773e37eb9d99 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 06:23:32 +0800 Subject: [PATCH 031/104] Add: link to user on tags page --- web/src/component/Tags.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/component/Tags.js b/web/src/component/Tags.js index f909bc2..6caa573 100644 --- a/web/src/component/Tags.js +++ b/web/src/component/Tags.js @@ -40,7 +40,11 @@ function Tags() { {tag.name} {tag.description} - {tag.created_by_user.username} + + + @{tag.created_by_user.username} + + Edit From d4718ac120d2daab96ba1338f95c551fb1f448e0 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 07:07:43 +0800 Subject: [PATCH 032/104] Add: support select ramdom files by tag --- pkg/api/api.go | 1 + pkg/api/handle_get_random_files.go | 26 +++++++ pkg/database/method.go | 22 ++++++ pkg/database/sql_stmt.go | 106 ++++++++++++++++------------ web/src/component/GetRandomFiles.js | 70 +++++++++++++++++- 5 files changed, 178 insertions(+), 47 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index cc81fdc..3bdda2d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -72,6 +72,7 @@ func NewAPI(config Config) (*API, error) { apiMux.HandleFunc("/search_folders", api.HandleSearchFolders) apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder) apiMux.HandleFunc("/get_random_files", api.HandleGetRandomFiles) + apiMux.HandleFunc("/get_random_files_with_tag", api.HandleGetRandomFilesWithTag) apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream) apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs) apiMux.HandleFunc("/feedback", api.HandleFeedback) diff --git a/pkg/api/handle_get_random_files.go b/pkg/api/handle_get_random_files.go index a164376..f7eefa8 100644 --- a/pkg/api/handle_get_random_files.go +++ b/pkg/api/handle_get_random_files.go @@ -23,3 +23,29 @@ func (api *API) HandleGetRandomFiles(w http.ResponseWriter, r *http.Request) { log.Println("[api] Get random files") json.NewEncoder(w).Encode(getRandomFilesResponse) } + +type GetRandomFilesWithTagRequest struct { + ID int64 `json:"id"` +} + +func (api *API) HandleGetRandomFilesWithTag(w http.ResponseWriter, r *http.Request) { + req := &GetRandomFilesWithTagRequest{} + err := json.NewDecoder(r.Body).Decode(req) + if err != nil { + api.HandleError(w, r, err) + return + } + + files, err := api.Db.GetRandomFilesWithTag(req.ID, 10) + if err != nil { + api.HandleError(w, r, err) + return + } + + getRandomFilesResponse := &GetRandomFilesResponse{ + Files: &files, + } + + log.Println("[api] Get random files with tag", req.ID) + json.NewEncoder(w).Encode(getRandomFilesResponse) +} diff --git a/pkg/database/method.go b/pkg/database/method.go index 9083d85..e6542a5 100644 --- a/pkg/database/method.go +++ b/pkg/database/method.go @@ -35,6 +35,28 @@ func (database *Database) GetRandomFiles(limit int64) ([]File, error) { return files, nil } +func (database *Database) GetRandomFilesWithTag(tagID, limit int64) ([]File, error) { + rows, err := database.stmt.getRandomFilesWithTag.Query(tagID, limit) + if err != nil { + return nil, err + } + defer rows.Close() + files := make([]File, 0) + for rows.Next() { + file := File{ + Db: database, + } + err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize) + if err != nil { + return nil, err + } + files = append(files, file) + log.Println("[db] GetRandomFilesWithTag", file.ID, file.Filename, file.Foldername, file.Filesize) + } + log.Println("[db] GetRandomFilesWithTag", files) + return files, nil +} + func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, error) { rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset) if err != nil { diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index 0afbbb0..9e9d50a 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -158,6 +158,15 @@ JOIN folders ON files.folder_id = folders.id ORDER BY RANDOM() LIMIT ?;` +var getRandomFilesWithTagQuery = `SELECT +files.id, files.folder_id, files.filename, folders.foldername, files.filesize +FROM file_has_tag +JOIN files ON file_has_tag.file_id = files.id +JOIN folders ON files.folder_id = folders.id +WHERE file_has_tag.tag_id = ? +ORDER BY RANDOM() +LIMIT ?;` + var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header) VALUES (?, ?, ?);` @@ -236,51 +245,52 @@ ORDER BY reviews.created_at ;` type Stmt struct { - initFilesTable *sql.Stmt - initFoldersTable *sql.Stmt - initFeedbacksTable *sql.Stmt - initUsersTable *sql.Stmt - initAvatarsTable *sql.Stmt - initTagsTable *sql.Stmt - initFileHasTag *sql.Stmt - initLikesTable *sql.Stmt - initReviewsTable *sql.Stmt - initPlaybacksTable *sql.Stmt - initLogsTable *sql.Stmt - initTmpfsTable *sql.Stmt - insertFolder *sql.Stmt - insertFile *sql.Stmt - findFolder *sql.Stmt - findFile *sql.Stmt - searchFiles *sql.Stmt - getFolder *sql.Stmt - dropFiles *sql.Stmt - dropFolder *sql.Stmt - getFile *sql.Stmt - searchFolders *sql.Stmt - getFilesInFolder *sql.Stmt - getRandomFiles *sql.Stmt - insertFeedback *sql.Stmt - insertUser *sql.Stmt - countUser *sql.Stmt - countAdmin *sql.Stmt - getUser *sql.Stmt - getUserById *sql.Stmt - getAnonymousUser *sql.Stmt - insertTag *sql.Stmt - getTag *sql.Stmt - getTags *sql.Stmt - updateTag *sql.Stmt - putTagOnFile *sql.Stmt - getTagsOnFile *sql.Stmt - deleteTagOnFile *sql.Stmt - updateFoldername *sql.Stmt - insertReview *sql.Stmt - getReviewsOnFile *sql.Stmt - getReview *sql.Stmt - updateReview *sql.Stmt - deleteReview *sql.Stmt - getReviewsByUser *sql.Stmt + initFilesTable *sql.Stmt + initFoldersTable *sql.Stmt + initFeedbacksTable *sql.Stmt + initUsersTable *sql.Stmt + initAvatarsTable *sql.Stmt + initTagsTable *sql.Stmt + initFileHasTag *sql.Stmt + initLikesTable *sql.Stmt + initReviewsTable *sql.Stmt + initPlaybacksTable *sql.Stmt + initLogsTable *sql.Stmt + initTmpfsTable *sql.Stmt + insertFolder *sql.Stmt + insertFile *sql.Stmt + findFolder *sql.Stmt + findFile *sql.Stmt + searchFiles *sql.Stmt + getFolder *sql.Stmt + dropFiles *sql.Stmt + dropFolder *sql.Stmt + getFile *sql.Stmt + searchFolders *sql.Stmt + getFilesInFolder *sql.Stmt + getRandomFiles *sql.Stmt + getRandomFilesWithTag *sql.Stmt + insertFeedback *sql.Stmt + insertUser *sql.Stmt + countUser *sql.Stmt + countAdmin *sql.Stmt + getUser *sql.Stmt + getUserById *sql.Stmt + getAnonymousUser *sql.Stmt + insertTag *sql.Stmt + getTag *sql.Stmt + getTags *sql.Stmt + updateTag *sql.Stmt + putTagOnFile *sql.Stmt + getTagsOnFile *sql.Stmt + deleteTagOnFile *sql.Stmt + updateFoldername *sql.Stmt + insertReview *sql.Stmt + getReviewsOnFile *sql.Stmt + getReview *sql.Stmt + updateReview *sql.Stmt + deleteReview *sql.Stmt + getReviewsByUser *sql.Stmt } func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { @@ -482,6 +492,12 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { return nil, err } + // init getRandomFilesWithTag + stmt.getRandomFilesWithTag, err = sqlConn.Prepare(getRandomFilesWithTagQuery) + if err != nil { + return nil, err + } + // init insertFeedback stmt.insertFeedback, err = sqlConn.Prepare(insertFeedbackQuery) if err != nil { diff --git a/web/src/component/GetRandomFiles.js b/web/src/component/GetRandomFiles.js index 2d38807..9f5eeae 100644 --- a/web/src/component/GetRandomFiles.js +++ b/web/src/component/GetRandomFiles.js @@ -4,8 +4,10 @@ import FilesTable from "./FilesTable"; function GetRandomFiles(props) { const [files, setFiles] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [tags, setTags] = useState([]); + const [selectedTag, setSelectedTag] = useState(""); - function refresh(setFiles) { + function getRandomFiles() { setIsLoading(true); fetch("/api/v1/get_random_files") .then((response) => response.json()) @@ -20,15 +22,79 @@ function GetRandomFiles(props) { }); } + function getRandomFilesWithTag() { + setIsLoading(true); + fetch("/api/v1/get_random_files_with_tag", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + id: parseInt(selectedTag), + }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + alert(data.error); + } else { + setFiles(data.files); + } + }) + .catch((error) => { + alert("get_random_files_with_tag error: " + error); + }) + .finally(() => { + setIsLoading(false); + }); + } + + function refresh() { + if (selectedTag === "") { + getRandomFiles(); + } else { + getRandomFilesWithTag(); + } + } + + function getTags() { + fetch("/api/v1/get_tags") + .then((response) => response.json()) + .then((data) => { + setTags(data.tags); + }) + .catch((error) => { + alert("get_tags error: " + error); + }); + } + useEffect(() => { - refresh(setFiles); + getTags(); }, []); + + useEffect(() => { + refresh(); + }, [selectedTag]); + return (
+
From a826e4bf291654453b002d8a7dcd7232fc8e15fd Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 13:24:25 +0800 Subject: [PATCH 033/104] Add: support walk database with tags --- pkg/api/handle_database_manage.go | 10 +++- pkg/database/method.go | 49 ++++++++++----- web/src/component/Database.js | 99 +++++++++++++++++++++++++++++++ web/src/component/Manage.js | 52 +--------------- 4 files changed, 144 insertions(+), 66 deletions(-) create mode 100644 web/src/component/Database.js diff --git a/pkg/api/handle_database_manage.go b/pkg/api/handle_database_manage.go index de256d1..ee929e4 100644 --- a/pkg/api/handle_database_manage.go +++ b/pkg/api/handle_database_manage.go @@ -8,6 +8,7 @@ import ( type WalkRequest struct { Root string `json:"root"` Pattern []string `json:"pattern"` + TagIDs []int64 `json:"tag_ids"` } type ResetRequest struct { @@ -71,8 +72,15 @@ func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) { return } + // get userID + userID, err := api.GetUserID(w, r) + if err != nil { + api.HandleError(w, r, err) + return + } + // walk - err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern) + err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern, walkRequest.TagIDs, userID) if err != nil { api.HandleError(w, r, err) return diff --git a/pkg/database/method.go b/pkg/database/method.go index e6542a5..8db719a 100644 --- a/pkg/database/method.go +++ b/pkg/database/method.go @@ -51,9 +51,7 @@ func (database *Database) GetRandomFilesWithTag(tagID, limit int64) ([]File, err return nil, err } files = append(files, file) - log.Println("[db] GetRandomFilesWithTag", file.ID, file.Filename, file.Foldername, file.Filesize) } - log.Println("[db] GetRandomFilesWithTag", files) return files, nil } @@ -137,12 +135,22 @@ func (database *Database) ResetFolder() error { return err } -func (database *Database) Walk(root string, pattern []string) error { +func (database *Database) Walk(root string, pattern []string, tagIDs []int64, userID int64) error { patternDict := make(map[string]bool) for _, v := range pattern { patternDict[v] = true } log.Println("[db] Walk", root, patternDict) + + tags := make([]*Tag, 0) + for _, tagID := range tagIDs { + tag, err := database.GetTag(tagID) + if err != nil { + return err + } + tags = append(tags, tag) + } + return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -159,10 +167,17 @@ func (database *Database) Walk(root string, pattern []string) error { } // insert file, folder will aut created - err = database.Insert(path, info.Size()) + fileID, err := database.Insert(path, info.Size()) if err != nil { return err } + + for _, tag := range tags { + err = database.PutTagOnFile(tag.ID, fileID, userID) + if err != nil { + return err + } + } return nil }) } @@ -231,35 +246,39 @@ func (database *Database) InsertFolder(folder string) (int64, error) { return lastInsertId, nil } -func (database *Database) InsertFile(folderId int64, filename string, filesize int64) error { - _, err := database.stmt.insertFile.Exec(folderId, filename, filesize) +func (database *Database) InsertFile(folderId int64, filename string, filesize int64) (int64, error) { + result, err := database.stmt.insertFile.Exec(folderId, filename, filesize) if err != nil { - return err + return 0, err } - return nil + lastInsertId, err := result.LastInsertId() + if err != nil { + return 0, err + } + return lastInsertId, nil } -func (database *Database) Insert(path string, filesize int64) error { +func (database *Database) Insert(path string, filesize int64) (int64, error) { folder, filename := filepath.Split(path) folderId, err := database.FindFolder(folder) if err != nil { folderId, err = database.InsertFolder(folder) if err != nil { - return err + return 0, err } } // if file exists, skip it - _, err = database.FindFile(folderId, filename) + lastInsertId, err := database.FindFile(folderId, filename) if err == nil { - return nil + return lastInsertId, nil } - err = database.InsertFile(folderId, filename, filesize) + lastInsertId, err = database.InsertFile(folderId, filename, filesize) if err != nil { - return err + return 0, err } - return nil + return lastInsertId, nil } func (database *Database) UpdateFoldername(folderId int64, foldername string) error { diff --git a/web/src/component/Database.js b/web/src/component/Database.js new file mode 100644 index 0000000..9389e84 --- /dev/null +++ b/web/src/component/Database.js @@ -0,0 +1,99 @@ +import { useState, useEffect } from "react"; + +function Database() { + const [walkPath, setWalkPath] = useState(""); + const [patternString, setPatternString] = useState(""); + const [tags, setTags] = useState([]); + const [selectedTags, setSelectedTags] = useState([]); + + function getTags() { + fetch("/api/v1/get_tags") + .then((response) => response.json()) + .then((data) => { + if (data.error) { + alert(data.error); + } else { + setTags(data.tags); + } + }); + } + + useEffect(() => { + getTags(); + }, []); + + function updateDatabase() { + // split pattern string into array + let patternArray = patternString.split(" "); + // remove whitespace from array + patternArray = patternArray.map((item) => item.trim()); + // remove empty strings from array + patternArray = patternArray.filter((item) => item !== ""); + // add dot before item array + patternArray = patternArray.map((item) => "." + item); + + fetch("/api/v1/walk", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + root: walkPath, + pattern: patternArray, + tag_ids: selectedTags, + }), + }) + .then((res) => res.json()) + .then((data) => { + console.log(data); + }); + } + return ( +
+

Update Database

+ setWalkPath(e.target.value)} + /> + setPatternString(e.target.value)} + /> +
+

Tags

+ {tags.map((tag) => ( +
+ { + if (e.target.checked) { + setSelectedTags([...selectedTags, tag.id]); + } else { + setSelectedTags( + selectedTags.filter((item) => item !== tag.id) + ); + } + }} + /> + +
+ ))} +
+ +
+ ); +} + +export default Database; diff --git a/web/src/component/Manage.js b/web/src/component/Manage.js index 893bb87..01c44cf 100644 --- a/web/src/component/Manage.js +++ b/web/src/component/Manage.js @@ -1,38 +1,9 @@ -import { useState } from "react"; import { useNavigate } from "react-router"; +import Database from "./Database"; function Manage(props) { let navigate = useNavigate(); - const [walkPath, setWalkPath] = useState(""); - const [patternString, setPatternString] = useState(""); - - function updateDatabase() { - // split pattern string into array - let patternArray = patternString.split(" "); - // remove whitespace from array - patternArray = patternArray.map((item) => item.trim()); - // remove empty strings from array - patternArray = patternArray.filter((item) => item !== ""); - // add dot before item array - patternArray = patternArray.map((item) => "." + item); - - fetch("/api/v1/walk", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - root: walkPath, - pattern: patternArray, - }), - }) - .then((res) => res.json()) - .then((data) => { - console.log(data); - }); - } - return (

Manage

@@ -65,26 +36,7 @@ function Manage(props) { )}
-

Update Database

- setWalkPath(e.target.value)} - /> - setPatternString(e.target.value)} - /> - +
); } From d7ca68aad17bd63b95fff115b8bb3b8ba222078b Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 13:43:09 +0800 Subject: [PATCH 034/104] Add: support insert active user --- pkg/database/method_user.go | 26 +++++++++++++++++++++++++- pkg/database/sql_stmt.go | 5 +++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pkg/database/method_user.go b/pkg/database/method_user.go index 01a4571..50762fd 100644 --- a/pkg/database/method_user.go +++ b/pkg/database/method_user.go @@ -23,7 +23,22 @@ func (database *Database) LoginAsAnonymous() (*User, error) { } func (database *Database) Register(username string, password string, usertype int64) (*User, error) { - _, err := database.stmt.insertUser.Exec(username, password, usertype, 0) + countAdmin, err := database.CountAdmin() + if err != nil { + return nil, err + } + + active := false + if countAdmin == 0 { + active = true + } + + // active normal user by default + if usertype == 2 { + active = true + } + + _, err = database.stmt.insertUser.Exec(username, password, usertype, active, 0) if err != nil { return nil, err } @@ -40,3 +55,12 @@ func (database *Database) GetUserById(id int64) (*User, error) { } return user, nil } + +func (database *Database) CountAdmin() (int64, error) { + var count int64 + err := database.stmt.countAdmin.QueryRow().Scan(&count) + if err != nil { + return 0, err + } + return count, nil +} diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index 9e9d50a..561e1c1 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -32,6 +32,7 @@ var initUsersTableQuery = `CREATE TABLE IF NOT EXISTS users ( username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, role INTEGER NOT NULL, + active BOOLEAN NOT NULL, avatar_id INTEGER NOT NULL, FOREIGN KEY(avatar_id) REFERENCES avatars(id) );` @@ -170,8 +171,8 @@ LIMIT ?;` var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header) VALUES (?, ?, ?);` -var insertUserQuery = `INSERT INTO users (username, password, role, avatar_id) -VALUES (?, ?, ?, ?);` +var insertUserQuery = `INSERT INTO users (username, password, role, active, avatar_id) +VALUES (?, ?, ?, ?, ?);` var countUserQuery = `SELECT count(*) FROM users;` From adee9bcb658df557861cd47f7d0eb6750b8b2888 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 13:47:02 +0800 Subject: [PATCH 035/104] Simplify: register not return user object --- pkg/api/handle_user.go | 14 +++----------- pkg/database/method_user.go | 8 ++++---- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/pkg/api/handle_user.go b/pkg/api/handle_user.go index 0fd8751..7868e20 100644 --- a/pkg/api/handle_user.go +++ b/pkg/api/handle_user.go @@ -128,21 +128,13 @@ func (api *API) HandleRegister(w http.ResponseWriter, r *http.Request) { log.Println("Register user", request.Username) - user, err := api.Db.Register(request.Username, request.Password, request.Role) - if err != nil { - api.HandleError(w, r, err) - return - } - - resp := &LoginResponse{ - User: user, - } - - err = json.NewEncoder(w).Encode(resp) + err = api.Db.Register(request.Username, request.Password, request.Role) if err != nil { api.HandleError(w, r, err) return } + + api.HandleOK(w, r) } func (api *API) CheckAdmin(w http.ResponseWriter, r *http.Request) error { diff --git a/pkg/database/method_user.go b/pkg/database/method_user.go index 50762fd..826da88 100644 --- a/pkg/database/method_user.go +++ b/pkg/database/method_user.go @@ -22,10 +22,10 @@ func (database *Database) LoginAsAnonymous() (*User, error) { return user, nil } -func (database *Database) Register(username string, password string, usertype int64) (*User, error) { +func (database *Database) Register(username string, password string, usertype int64) (error) { countAdmin, err := database.CountAdmin() if err != nil { - return nil, err + return err } active := false @@ -40,9 +40,9 @@ func (database *Database) Register(username string, password string, usertype in _, err = database.stmt.insertUser.Exec(username, password, usertype, active, 0) if err != nil { - return nil, err + return err } - return database.Login(username, password) + return nil } func (database *Database) GetUserById(id int64) (*User, error) { From ab67575976ad3ec1b1be0e140dfb7ca9f2011177 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 14:20:36 +0800 Subject: [PATCH 036/104] Add: set user acitve --- pkg/api/api.go | 2 + pkg/api/handle_review.go | 25 ++++++++++ pkg/api/handle_user.go | 46 +++++++++++++++---- pkg/database/method_user.go | 30 +++++++++++- pkg/database/sql_stmt.go | 20 +++++++- pkg/database/struct.go | 1 + web/src/App.js | 5 ++ web/src/component/Manage.js | 1 + web/src/component/ManageUser.js | 78 ++++++++++++++++++++++++++++++++ web/src/component/ReviewEntry.js | 2 +- 10 files changed, 199 insertions(+), 11 deletions(-) create mode 100644 web/src/component/ManageUser.js diff --git a/pkg/api/api.go b/pkg/api/api.go index 3bdda2d..9049b09 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -83,6 +83,8 @@ func NewAPI(config Config) (*API, error) { apiMux.HandleFunc("/login", api.HandleLogin) apiMux.HandleFunc("/register", api.HandleRegister) apiMux.HandleFunc("/logout", api.LoginAsAnonymous) + apiMux.HandleFunc("/get_users", api.HandleGetUsers) + apiMux.HandleFunc("/update_user_active", api.HandleUpdateUserActive) // tag apiMux.HandleFunc("/get_tags", api.HandleGetTags) apiMux.HandleFunc("/get_tag_info", api.HandleGetTagInfo) diff --git a/pkg/api/handle_review.go b/pkg/api/handle_review.go index e9b0d73..406e043 100644 --- a/pkg/api/handle_review.go +++ b/pkg/api/handle_review.go @@ -182,3 +182,28 @@ func (api *API) HandleDeleteReview(w http.ResponseWriter, r *http.Request) { api.HandleOK(w, r) } + +type GetReviewsByUserRequest struct { + ID int64 `json:"id"` +} + +func (api *API) HandleGetReviewsByUser(w http.ResponseWriter, r *http.Request) { + req := &GetReviewsByUserRequest{} + err := json.NewDecoder(r.Body).Decode(req) + if err != nil { + api.HandleError(w, r, err) + return + } + + reviews, err := api.Db.GetReviewsByUser(req.ID) + if err != nil { + api.HandleError(w, r, err) + return + } + + err = json.NewEncoder(w).Encode(reviews) + if err != nil { + api.HandleError(w, r, err) + return + } +} diff --git a/pkg/api/handle_user.go b/pkg/api/handle_user.go index 7868e20..cf06872 100644 --- a/pkg/api/handle_user.go +++ b/pkg/api/handle_user.go @@ -133,7 +133,7 @@ func (api *API) HandleRegister(w http.ResponseWriter, r *http.Request) { api.HandleError(w, r, err) return } - + api.HandleOK(w, r) } @@ -185,27 +185,57 @@ func (api *API) GetUserID(w http.ResponseWriter, r *http.Request) (int64, error) return userId.(int64), nil } -type GetReviewsByUserRequest struct { - ID int64 `json:"id"` +type GetUsersResponse struct { + Users []*database.User `json:"users"` } -func (api *API) HandleGetReviewsByUser(w http.ResponseWriter, r *http.Request) { - req := &GetReviewsByUserRequest{} - err := json.NewDecoder(r.Body).Decode(req) +func (api *API) HandleGetUsers(w http.ResponseWriter, r *http.Request) { + err := api.CheckAdmin(w, r) if err != nil { api.HandleError(w, r, err) return } - reviews, err := api.Db.GetReviewsByUser(req.ID) + users, err := api.Db.GetUsers() if err != nil { api.HandleError(w, r, err) return } - err = json.NewEncoder(w).Encode(reviews) + ret := &GetUsersResponse{ + Users: users, + } + + err = json.NewEncoder(w).Encode(ret) if err != nil { api.HandleError(w, r, err) return } } + +type UpdateUserActiveRequest struct { + ID int64 `json:"id"` + Active bool `json:"active"` +} + +func (api *API) HandleUpdateUserActive(w http.ResponseWriter, r *http.Request) { + err := api.CheckAdmin(w, r) + if err != nil { + api.HandleError(w, r, err) + return + } + + req := &UpdateUserActiveRequest{} + err = json.NewDecoder(r.Body).Decode(req) + if err != nil { + api.HandleError(w, r, err) + return + } + + err = api.Db.UpdateUserActive(req.ID, req.Active) + if err != nil { + api.HandleError(w, r, err) + return + } + api.HandleOK(w, r) +} diff --git a/pkg/database/method_user.go b/pkg/database/method_user.go index 826da88..e5f493c 100644 --- a/pkg/database/method_user.go +++ b/pkg/database/method_user.go @@ -22,7 +22,7 @@ func (database *Database) LoginAsAnonymous() (*User, error) { return user, nil } -func (database *Database) Register(username string, password string, usertype int64) (error) { +func (database *Database) Register(username string, password string, usertype int64) error { countAdmin, err := database.CountAdmin() if err != nil { return err @@ -64,3 +64,31 @@ func (database *Database) CountAdmin() (int64, error) { } return count, nil } + +func (database *Database) GetUsers() ([]*User, error) { + users := make([]*User, 0) + + rows, err := database.stmt.getUsers.Query() + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + user := &User{} + err = rows.Scan(&user.ID, &user.Username, &user.Role, &user.Active, &user.AvatarId) + if err != nil { + return nil, err + } + users = append(users, user) + } + return users, nil +} + +func (database *Database) UpdateUserActive(id int64, active bool) error { + _, err := database.stmt.updateUserActive.Exec(active, id) + if err != nil { + return err + } + return nil +} diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index 561e1c1..6dd0156 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -180,8 +180,12 @@ var countAdminQuery = `SELECT count(*) FROM users WHERE role= 1;` var getUserQuery = `SELECT id, username, role, avatar_id FROM users WHERE username = ? AND password = ? LIMIT 1;` +var getUsersQuery = `SELECT id, username, role, active, avatar_id FROM users;` + var getUserByIdQuery = `SELECT id, username, role, avatar_id FROM users WHERE id = ? LIMIT 1;` +var updateUserActiveQuery = `UPDATE users SET active = ? WHERE id = ?;` + var getAnonymousUserQuery = `SELECT id, username, role, avatar_id FROM users WHERE role = 0 LIMIT 1;` var insertTagQuery = `INSERT INTO tags (name, description, created_by_user_id) VALUES (?, ?, ?);` @@ -276,7 +280,9 @@ type Stmt struct { countUser *sql.Stmt countAdmin *sql.Stmt getUser *sql.Stmt + getUsers *sql.Stmt getUserById *sql.Stmt + updateUserActive *sql.Stmt getAnonymousUser *sql.Stmt insertTag *sql.Stmt getTag *sql.Stmt @@ -529,12 +535,24 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { return nil, err } + // init getUsers + stmt.getUsers, err = sqlConn.Prepare(getUsersQuery) + if err != nil { + return nil, err + } + // init getUserById stmt.getUserById, err = sqlConn.Prepare(getUserByIdQuery) if err != nil { return nil, err } + // init updateUserActive + stmt.updateUserActive, err = sqlConn.Prepare(updateUserActiveQuery) + if err != nil { + return nil, err + } + // init getAnonymousUser stmt.getAnonymousUser, err = sqlConn.Prepare(getAnonymousUserQuery) if err != nil { @@ -548,7 +566,7 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { return nil, err } if userCount == 0 { - _, err = stmt.insertUser.Exec("Anonymous user", "", 0, 0) + _, err = stmt.insertUser.Exec("Anonymous user", "", 0, 1, 0) if err != nil { return nil, err } diff --git a/pkg/database/struct.go b/pkg/database/struct.go index 343b597..f02455c 100644 --- a/pkg/database/struct.go +++ b/pkg/database/struct.go @@ -25,6 +25,7 @@ type User struct { Username string `json:"username"` Password string `json:"-"` Role int64 `json:"role"` + Active bool `json:"active"` AvatarId int64 `json:"avatar_id"` } diff --git a/web/src/App.js b/web/src/App.js index 2b79f9c..6a961ad 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -6,6 +6,7 @@ import SearchFiles from "./component/SearchFiles"; import SearchFolders from "./component/SearchFolders"; import FilesInFolder from "./component/FilesInFolder"; import Manage from "./component/Manage"; +import ManageUser from "./component/ManageUser"; import FileInfo from "./component/FileInfo"; import Share from "./component/Share"; import Login from "./component/Login"; @@ -89,6 +90,10 @@ function App() { path="/manage/reviews/:id" element={} /> + } + /> } diff --git a/web/src/component/Manage.js b/web/src/component/Manage.js index 01c44cf..99f11c9 100644 --- a/web/src/component/Manage.js +++ b/web/src/component/Manage.js @@ -36,6 +36,7 @@ function Manage(props) { )}
+
); diff --git a/web/src/component/ManageUser.js b/web/src/component/ManageUser.js new file mode 100644 index 0000000..5cc7ff3 --- /dev/null +++ b/web/src/component/ManageUser.js @@ -0,0 +1,78 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; + +function ManageUser() { + const [users, setUsers] = useState([]); + const roleDict = { + 0: "Anonymous", + 1: "Admin", + 2: "Normal User", + }; + + function getUsers() { + fetch("/api/v1/get_users") + .then((res) => res.json()) + .then((data) => { + if (data.error) { + alert(data.error); + } else { + setUsers(data.users); + } + }); + } + + useEffect(() => { + getUsers(); + }, []); + + return ( +
+

Manage User

+ + + + + + + + + + {users.map((user) => ( + + + + + + ))} + +
NameRoleActive
+ @{user.username} + {roleDict[user.role]} + { + fetch("/api/v1/update_user_active", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + id: user.id, + active: e.target.checked, + }), + }).then((res) => res.json()).then((data) => { + if (data.error) { + alert(data.error); + } else { + getUsers(); + } + }); + }} + /> +
+
+ ); +} + +export default ManageUser; diff --git a/web/src/component/ReviewEntry.js b/web/src/component/ReviewEntry.js index b5cf648..0a5c432 100644 --- a/web/src/component/ReviewEntry.js +++ b/web/src/component/ReviewEntry.js @@ -14,7 +14,7 @@ function ReviewEntry(props) {

{props.review.content}

{(props.user.role === 1 || props.review.user.id === props.user.id) && - props.user.role != 0 && ( + props.user.role !== 0 && ( )} + {props.user.role !== 0 && ( + + )} {props.user.role !== 0 && ( +
+ setOldPassword(e.target.value)} + /> + setNewPassword(e.target.value)} + /> + setNewPasswordConfirm(e.target.value)} + /> + +
+

Reviews

+ {reviews.map((review) => ( + + ))} ); } From e1d9eac51434feb9b026d890a9bc748e20e48a2e Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 22:21:24 +0800 Subject: [PATCH 039/104] Add: updating database.. --- pkg/api/handle_user.go | 8 +------- web/src/component/Database.js | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pkg/api/handle_user.go b/pkg/api/handle_user.go index f8f5adf..a59c321 100644 --- a/pkg/api/handle_user.go +++ b/pkg/api/handle_user.go @@ -196,12 +196,6 @@ type GetUsersResponse struct { } func (api *API) HandleGetUsers(w http.ResponseWriter, r *http.Request) { - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - users, err := api.Db.GetUsers() if err != nil { api.HandleError(w, r, err) @@ -320,7 +314,7 @@ func (api *API) HandleGetUserInfo(w http.ResponseWriter, r *http.Request) { } type UpdateUserPasswordRequest struct { - ID int64 `json:"id"` + ID int64 `json:"id"` OldPassword string `json:"old_password"` NewPassword string `json:"new_password"` } diff --git a/web/src/component/Database.js b/web/src/component/Database.js index 9389e84..3ba5bde 100644 --- a/web/src/component/Database.js +++ b/web/src/component/Database.js @@ -5,6 +5,7 @@ function Database() { const [patternString, setPatternString] = useState(""); const [tags, setTags] = useState([]); const [selectedTags, setSelectedTags] = useState([]); + const [updating, setUpdating] = useState(false); function getTags() { fetch("/api/v1/get_tags") @@ -32,6 +33,8 @@ function Database() { // add dot before item array patternArray = patternArray.map((item) => "." + item); + setUpdating(true); + fetch("/api/v1/walk", { method: "POST", headers: { @@ -45,7 +48,14 @@ function Database() { }) .then((res) => res.json()) .then((data) => { - console.log(data); + if (data.error) { + alert(data.error); + } else { + alert("Database updated"); + } + }) + .finally(() => { + setUpdating(false); }); } return ( @@ -89,8 +99,9 @@ function Database() { onClick={() => { updateDatabase(); }} + disabled={updating} > - Update Database + {updating ? "Updating..." : "Update Database"} ); From 22f7ea8476ff5c1d1dea8ba764a3bd74e658df3e Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 22:27:01 +0800 Subject: [PATCH 040/104] Update: title space evenly --- web/src/App.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/App.css b/web/src/App.css index 68ca5ed..5169e17 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -17,8 +17,10 @@ body { } .title { margin-left: 1em; + margin-right: 1em; display: flex; align-items: center; + justify-content: space-between; } .title-text { margin-left: 1em; From 0c9048072fd25c8cb879c8bb6125103ed853b775 Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 13 Dec 2021 23:18:46 +0800 Subject: [PATCH 041/104] Add: support feedback --- pkg/api/api.go | 1 + pkg/api/handle_feedback.go | 44 ++++++++++++++-- pkg/database/method.go | 8 --- pkg/database/method_feedback.go | 32 ++++++++++++ pkg/database/sql_stmt.go | 22 ++++++-- pkg/database/struct.go | 9 ++++ web/src/App.js | 5 ++ web/src/component/FeedbackPage.js | 83 +++++++++++++++++++++++++++++++ web/src/component/Manage.js | 1 + 9 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 pkg/database/method_feedback.go create mode 100644 web/src/component/FeedbackPage.js diff --git a/pkg/api/api.go b/pkg/api/api.go index ebfedc5..2522f0b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -76,6 +76,7 @@ func NewAPI(config Config) (*API, error) { apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream) apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs) apiMux.HandleFunc("/feedback", api.HandleFeedback) + apiMux.HandleFunc("/get_feedbacks", api.HandleGetFeedbacks) apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo) apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect) apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect) diff --git a/pkg/api/handle_feedback.go b/pkg/api/handle_feedback.go index f9375dd..018b666 100644 --- a/pkg/api/handle_feedback.go +++ b/pkg/api/handle_feedback.go @@ -4,12 +4,13 @@ import ( "bytes" "encoding/json" "log" + "msw-open-music/pkg/database" "net/http" "time" ) type FeedbackRequest struct { - Feedback string `json:"feedback"` + Content string `json:"content"` } func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) { @@ -21,12 +22,12 @@ func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) { } // check empty feedback - if feedbackRequest.Feedback == "" { + if feedbackRequest.Content == "" { api.HandleErrorString(w, r, `"feedback" can't be empty`) return } - log.Println("[api] Feedback", feedbackRequest.Feedback) + log.Println("[api] Feedback", feedbackRequest.Content) headerBuff := &bytes.Buffer{} err = r.Header.Write(headerBuff) @@ -36,10 +37,45 @@ func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) { } header := headerBuff.String() - err = api.Db.InsertFeedback(time.Now().Unix(), feedbackRequest.Feedback, header) + userID, err := api.GetUserID(w, r) + if err != nil { + api.HandleError(w, r, err) + return + } + + err = api.Db.InsertFeedback(time.Now().Unix(), feedbackRequest.Content, userID, header) if err != nil { api.HandleError(w, r, err) return } api.HandleOK(w, r) } + +type GetFeedbacksResponse struct { + Feedbacks []*database.Feedback `json:"feedbacks"` +} + +func (api *API) HandleGetFeedbacks(w http.ResponseWriter, r *http.Request) { + // check if admin + err := api.CheckAdmin(w, r) + if err != nil { + api.HandleError(w, r, err) + return + } + + feedbacks, err := api.Db.GetFeedbacks() + if err != nil { + api.HandleError(w, r, err) + return + } + + resp := &GetFeedbacksResponse{ + Feedbacks: feedbacks, + } + + err = json.NewEncoder(w).Encode(resp) + if err != nil { + api.HandleError(w, r, err) + return + } +} diff --git a/pkg/database/method.go b/pkg/database/method.go index 8db719a..febe89f 100644 --- a/pkg/database/method.go +++ b/pkg/database/method.go @@ -7,14 +7,6 @@ import ( "path/filepath" ) -func (database *Database) InsertFeedback(time int64, feedback string, header string) error { - _, err := database.stmt.insertFeedback.Exec(time, feedback, header) - if err != nil { - return err - } - return nil -} - func (database *Database) GetRandomFiles(limit int64) ([]File, error) { rows, err := database.stmt.getRandomFiles.Query(limit) if err != nil { diff --git a/pkg/database/method_feedback.go b/pkg/database/method_feedback.go new file mode 100644 index 0000000..4c4f5d9 --- /dev/null +++ b/pkg/database/method_feedback.go @@ -0,0 +1,32 @@ +package database + +func (database *Database) InsertFeedback(time int64, content string, userID int64, header string) error { + _, err := database.stmt.insertFeedback.Exec(time, content, userID, header) + if err != nil { + return err + } + return nil +} + +func (database *Database) GetFeedbacks() ([]*Feedback, error) { + rows, err := database.stmt.getFeedbacks.Query() + if err != nil { + return nil, err + } + defer rows.Close() + + feedbacks := make([]*Feedback, 0) + for rows.Next() { + feedback := &Feedback{ + User: &User{}, + } + err := rows.Scan( + &feedback.ID, &feedback.Time, &feedback.Content, &feedback.Header, + &feedback.User.ID, &feedback.User.Username, &feedback.User.Role, &feedback.User.Active, &feedback.User.AvatarId) + if err != nil { + return nil, err + } + feedbacks = append(feedbacks, feedback) + } + return feedbacks, nil +} diff --git a/pkg/database/sql_stmt.go b/pkg/database/sql_stmt.go index dfb36f6..98ba6c1 100644 --- a/pkg/database/sql_stmt.go +++ b/pkg/database/sql_stmt.go @@ -21,7 +21,8 @@ var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders ( var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks ( id INTEGER PRIMARY KEY, time INTEGER NOT NULL, - feedback TEXT NOT NULL, + content TEXT NOT NULL, + user_id INTEGER NOT NULL, header TEXT NOT NULL );` @@ -168,8 +169,16 @@ WHERE file_has_tag.tag_id = ? ORDER BY RANDOM() LIMIT ?;` -var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header) -VALUES (?, ?, ?);` +var insertFeedbackQuery = `INSERT INTO feedbacks (time, content, user_id, header) +VALUES (?, ?, ?, ?);` + +var getFeedbacksQuery = `SELECT +feedbacks.id, feedbacks.time, feedbacks.content, feedbacks.header, +users.id, users.username, users.role, users.active, users.avatar_id +FROM feedbacks +JOIN users ON feedbacks.user_id = users.id +ORDER BY feedbacks.time +;` var insertUserQuery = `INSERT INTO users (username, password, role, active, avatar_id) VALUES (?, ?, ?, ?, ?);` @@ -280,6 +289,7 @@ type Stmt struct { getRandomFiles *sql.Stmt getRandomFilesWithTag *sql.Stmt insertFeedback *sql.Stmt + getFeedbacks *sql.Stmt insertUser *sql.Stmt countUser *sql.Stmt countAdmin *sql.Stmt @@ -517,6 +527,12 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) { return nil, err } + // init getFeedbacks + stmt.getFeedbacks, err = sqlConn.Prepare(getFeedbacksQuery) + if err != nil { + return nil, err + } + // init insertUser stmt.insertUser, err = sqlConn.Prepare(insertUserQuery) if err != nil { diff --git a/pkg/database/struct.go b/pkg/database/struct.go index f02455c..fa8afce 100644 --- a/pkg/database/struct.go +++ b/pkg/database/struct.go @@ -48,6 +48,15 @@ type Review struct { Content string `json:"content"` } +type Feedback struct { + ID int64 `json:"id"` + UserId int64 `json:"user_id"` + User *User `json:"user"` + Content string `json:"content"` + Header string `json:"header"` + Time int64 `json:"time"` +} + var ( RoleAnonymous = int64(0) RoleAdmin = int64(1) diff --git a/web/src/App.js b/web/src/App.js index 6a961ad..6b0b011 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -18,6 +18,7 @@ import AudioPlayer from "./component/AudioPlayer"; import UserStatus from "./component/UserStatus"; import ReviewPage from "./component/ReviewPage"; import UserProfile from "./component/UserProfile"; +import FeedbackPage from "./component/FeedbackPage"; import { useState } from "react"; function App() { @@ -70,6 +71,10 @@ function App() { path="/manage" element={} /> + } + /> } diff --git a/web/src/component/FeedbackPage.js b/web/src/component/FeedbackPage.js new file mode 100644 index 0000000..f92fdb1 --- /dev/null +++ b/web/src/component/FeedbackPage.js @@ -0,0 +1,83 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { convertIntToDateTime } from "./Common"; + +function FeedbackPage() { + const [content, setContext] = useState(""); + const [feedbacks, setFeedbacks] = useState([]); + + function getFeedbacks() { + fetch("/api/v1/get_feedbacks") + .then((res) => res.json()) + .then((data) => { + if (data.error) { + console.log(data.error); + } else { + setFeedbacks(data.feedbacks); + } + }); + } + + function submitFeedback() { + fetch("/api/v1/feedback", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: content, + }), + }) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + alert(data.error); + } else { + setContext(""); + getFeedbacks(); + } + }); + } + + useEffect(() => { + getFeedbacks(); + }, []); + + return ( +
+

Feedback

+
- - + +
); diff --git a/web/src/component/EditTag.js b/web/src/component/EditTag.js index 02ece18..2cc3637 100644 --- a/web/src/component/EditTag.js +++ b/web/src/component/EditTag.js @@ -1,9 +1,11 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useContext } from "react"; import { useParams, useNavigate } from "react-router"; +import { tr, Tr, langCodeContext } from "../translate"; function EditTag() { let params = useParams(); let navigate = useNavigate(); + const { langCode } = useContext(langCodeContext); const [tag, setTag] = useState({ id: "", @@ -54,7 +56,7 @@ function EditTag() { if (data.error) { alert(data.error); } else { - alert("Tag updated successfully"); + alert(tr("Tag updated successfully", langCode)); refreshTagInfo(); } }); @@ -79,7 +81,7 @@ function EditTag() { if (data.error) { alert(data.error); } else { - alert("Tag deleted successfully"); + alert(tr("Tag deleted successfully", langCode)); navigate(-1); } }); @@ -87,9 +89,9 @@ function EditTag() { return (
-

Edit Tag

+

{Tr("Edit Tag")}

- + setTag({ ...tag, id: e.target.value })} /> - + - + setTag({ ...tag, name: e.target.value })} /> - +