diff --git a/docs/changelog.md b/docs/changelog.md index 22f3cf81..40c9b1db 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,6 @@ +# 0.1.5 +- Add image upload endpoints for artist/albums @ v1/albums/{uuid}/upload + # 0.1.4 - Fix spotify image import on scrobble for new artists/albums - Create image resizer diff --git a/internal/goscrobble/image.go b/internal/goscrobble/image.go index be10c9a3..3a8a10e7 100644 --- a/internal/goscrobble/image.go +++ b/internal/goscrobble/image.go @@ -3,13 +3,38 @@ package goscrobble import ( "fmt" "io" + "io/ioutil" "log" + "mime/multipart" "net/http" "os" "github.com/disintegration/imaging" ) +func importUploadedImage(file multipart.File, uuid string, recordType string) error { + // Create image + out, err := os.Create(DataDirectory + string(os.PathSeparator) + "img" + string(os.PathSeparator) + uuid + "_full.jpg") + if err != nil { + return err + } + defer out.Close() + + fileBytes, err := ioutil.ReadAll(file) + if err != nil { + return err + } + + // Write image + _, err = out.Write(fileBytes) + if err == nil { + // Make sure we queue it to process! + _, err = db.Exec("UPDATE `"+recordType+"` SET `img` = 'pending' WHERE `uuid` = UUID_TO_BIN(?,true)", uuid) + } + + return err +} + func importImage(uuid string, url string) error { // Create image out, err := os.Create(DataDirectory + string(os.PathSeparator) + "img" + string(os.PathSeparator) + uuid + "_full.jpg") diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index df5054b6..99bf4abb 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -70,6 +70,10 @@ func HandleRequests(port string) { v1.HandleFunc("/config", limitMiddleware(adminMiddleware(getConfig), standardLimiter)).Methods("GET") v1.HandleFunc("/config", limitMiddleware(adminMiddleware(postConfig), standardLimiter)).Methods("POST") + // Image uploads - Mod+ + v1.HandleFunc("/artists/{uuid}/upload", limitMiddleware(modMiddleware(handleArtistUpload), heavyLimiter)).Methods("POST") + v1.HandleFunc("/albums/{uuid}/upload", limitMiddleware(modMiddleware(handleAlbumUpload), heavyLimiter)).Methods("POST") + // No Auth v1.HandleFunc("/stats", limitMiddleware(handleStats, lightLimiter)).Methods("GET") v1.HandleFunc("/recent", limitMiddleware(handleRecentScrobbles, lightLimiter)).Methods("GET") @@ -841,3 +845,101 @@ func handleRecentScrobbles(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(json) } + +// handleArtistUpload - Image uploads! +func handleArtistUpload(w http.ResponseWriter, r *http.Request, jwtUser string) { + var uuid string + for k, v := range mux.Vars(r) { + if k == "uuid" { + uuid = v + } + } + + if uuid == "" { + throwOkError(w, "Invalid UUID") + return + } + + // Little bit of validation + _, err := getArtistByUUID(uuid) + if err != nil { + throwOkError(w, err.Error()) + return + } + + // 10MB Maximum... + err = r.ParseMultipartForm(10 << 20) + if err != nil { + log.Print(err) + throwOkError(w, "Error processing upload") + return + } + + // Get files from request + file, _, err := r.FormFile("file") + if err != nil { + log.Print(err) + throwOkError(w, "Error processing upload") + return + } + defer file.Close() + + // Upload ze file + err = importUploadedImage(file, uuid, "artists") + if err != nil { + log.Println(err) + throwBadReq(w, "Error processing upload") + return + } + + throwOkMessage(w, "Successfully uploaded!") +} + +// handleAlbumUpload - Image uploads! +func handleAlbumUpload(w http.ResponseWriter, r *http.Request, jwtUser string) { + var uuid string + for k, v := range mux.Vars(r) { + if k == "uuid" { + uuid = v + } + } + + if uuid == "" { + throwOkError(w, "Invalid UUID") + return + } + + // Little bit of validation + _, err := getAlbumByUUID(uuid) + if err != nil { + throwOkError(w, err.Error()) + return + } + + // 10MB Maximum... + err = r.ParseMultipartForm(10 << 20) + if err != nil { + log.Print(err) + throwOkError(w, "Error processing upload") + return + } + + // Get files from request + file, _, err := r.FormFile("file") + if err != nil { + log.Print(err) + throwOkError(w, "Error processing upload") + return + } + defer file.Close() + + // Upload ze file + err = importUploadedImage(file, uuid, "albums") + if err != nil { + log.Println(err) + throwBadReq(w, "Error processing upload") + return + } + + throwOkMessage(w, "Successfully uploaded!") +} diff --git a/internal/goscrobble/server_middleware.go b/internal/goscrobble/server_middleware.go index c3dcc5f4..39e68a93 100644 --- a/internal/goscrobble/server_middleware.go +++ b/internal/goscrobble/server_middleware.go @@ -66,6 +66,32 @@ func jwtMiddleware(next func(http.ResponseWriter, *http.Request, CustomClaims, s } } +// modMiddleware - Validates user is admin OR mod +func modMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + fullToken := r.Header.Get("Authorization") + authToken := strings.Replace(fullToken, "Bearer ", "", 1) + claims, err := verifyJWTToken(authToken) + if err != nil { + throwUnauthorized(w, "Invalid JWT Token") + return + } + + user, err := getUserByUUID(claims.Subject) + if err != nil { + throwUnauthorized(w, err.Error()) + return + } + + if !user.Admin && !user.Mod { + throwUnauthorized(w, "User is not moderator or administrator") + return + } + + next(w, r, claims.Subject) + } +} + // adminMiddleware - Validates user is admin func adminMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/goscrobble/server_responses.go b/internal/goscrobble/server_responses.go index 207c120a..85198419 100644 --- a/internal/goscrobble/server_responses.go +++ b/internal/goscrobble/server_responses.go @@ -27,7 +27,7 @@ func throwBadReq(w http.ResponseWriter, m string) { http.Error(w, err.Error(), http.StatusBadRequest) } -// throwOkError - Throws a 403 +// throwOkError - Throws a 200 error func throwOkError(w http.ResponseWriter, m string) { jr := jsonResponse{ Err: m,