Files
sm-journal-plugin/controllers/api/media.go
Jakub Zych 15fcad7a93 refactor(api): share media storage with the editor upload
- 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
2026-10-10 12:08:42 +02:00

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")
}