- classes.StoreMedia holds the media upload route's storage: extension allowlist, 10240 KiB cap, /journal/<folder>/<name> keys with the unique suffix, bucket write and public URL - UploadMedia keeps authentication, permissions, the folder and form checks and answers with the same 201 body and messages - uniqueMediaKey fails instead of overwriting when every numbered name is taken; the media path tests move to classes
64 lines
1.9 KiB
Go
64 lines
1.9 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-journal-plugin/classes"
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/wire"
|
|
"gocloud.dev/blob"
|
|
)
|
|
|
|
// UploadMedia is POST /_journal/api/v1/media/upload (JOURNAL-006). The
|
|
// route authenticates, checks permissions, the folder and the form, then
|
|
// stores the file with classes.StoreMedia, which the Blog Editor's image
|
|
// upload shares.
|
|
func UploadMedia(app *backpack.App) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
r, pr, ok := requireBackendPrincipal(app, w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if !requireAccessPosts(w, pr) {
|
|
return
|
|
}
|
|
folder := strings.TrimSpace(r.FormValue("folder"))
|
|
if len(folder) > 100 || !classes.MediaFolderPattern.MatchString(folder) || strings.Contains(folder, "..") {
|
|
writeValidation(w, map[string][]string{"folder": {"The folder format is invalid."}})
|
|
return
|
|
}
|
|
file, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
writeValidation(w, map[string][]string{"file": {"The file field is required."}})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
bucket, _ := app.Lookup[*blob.Bucket]()
|
|
stored, err := classes.StoreMedia(r.Context(), bucket, folder, header.Filename, file)
|
|
if err != nil {
|
|
writeMediaStoreError(w, err)
|
|
return
|
|
}
|
|
wire.WriteJSON(w, http.StatusCreated, map[string]any{
|
|
"data": map[string]any{
|
|
"url": stored.URL,
|
|
"path": stored.Path,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// writeMediaStoreError answers a StoreMedia failure as the route always
|
|
// has: a classes.MediaError is a 422 on its field with its message, any
|
|
// other error the opaque 500.
|
|
func writeMediaStoreError(w http.ResponseWriter, err error) {
|
|
var refused *classes.MediaError
|
|
if errors.As(err, &refused) && refused != nil {
|
|
writeValidation(w, map[string][]string{refused.Field: {refused.Message}})
|
|
return
|
|
}
|
|
writeAPIError(w, http.StatusInternalServerError, "Internal server error")
|
|
}
|