Add TypeScript migration, image resizing, media upload UX, and multimedia support
All checks were successful
Deploy to NAS / deploy (push) Successful in 2m20s
All checks were successful
Deploy to NAS / deploy (push) Successful in 2m20s
- Migrate static JS to TypeScript (static-ts/ → compiled to internal/api/static/) - Add image resizing on upload: JPEG/PNG/WebP scaled to max 1920px at quality 80 - Extract shared upload logic into upload.go (saveUpload, saveResizedImage, saveResizedWebP) - Add POST /media endpoint for drag-drop/paste media uploads with markdown ref return - Add background music player with video/audio coordination (autoplay.ts) - Add global nav, public feed, hashtags, visibility, Markdown rendering for entries - Add Dockerfile stage for TypeScript compilation (static-ts-builder) - Add goldmark, disintegration/imaging, golang.org/x/image dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
76
backend/internal/api/media.go
Normal file
76
backend/internal/api/media.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MediaHandler struct {
|
||||
uploadDir string
|
||||
}
|
||||
|
||||
func NewMediaHandler(uploadDir string) *MediaHandler {
|
||||
return &MediaHandler{uploadDir: uploadDir}
|
||||
}
|
||||
|
||||
// HandleUpload handles POST /media — uploads a single file and returns its markdown reference.
|
||||
func (h *MediaHandler) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
|
||||
http.Error(w, "too large", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
fh, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "missing file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
buf := make([]byte, 512)
|
||||
n, _ := fh.Read(buf)
|
||||
mime := http.DetectContentType(buf[:n])
|
||||
if _, ok := allowedMIME[mime]; !ok {
|
||||
http.Error(w, "unsupported type", http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
filename, err := saveUpload(h.uploadDir, randomID(), mime, buf[:n], fh)
|
||||
if err != nil {
|
||||
http.Error(w, "storage error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ref := markdownRef(mime, filename)
|
||||
slog.Info("media uploaded", "filename", filename, "mime", mime)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"filename": filename,
|
||||
"mime": mime,
|
||||
"ref": ref,
|
||||
})
|
||||
}
|
||||
|
||||
func markdownRef(mime, filename string) string {
|
||||
url := "/uploads/" + filename
|
||||
switch {
|
||||
case strings.HasPrefix(mime, "image/"):
|
||||
return ""
|
||||
case strings.HasPrefix(mime, "video/"):
|
||||
return ""
|
||||
case strings.HasPrefix(mime, "audio/"):
|
||||
return "[" + filename + "](" + url + ")"
|
||||
default:
|
||||
return "[" + filename + "](" + url + ")"
|
||||
}
|
||||
}
|
||||
|
||||
func randomID() string {
|
||||
b := make([]byte, 12)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
Reference in New Issue
Block a user