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