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