Files
dumpsterChat/internal/upload/handlers.go
T
hobokenchicken e69553af02 Profiles, Giphy, uploads, settings UI
Backend:
- Auth: split routes into RegisterPublicRoutes + RegisterProtectedRoutes
- Auth: PATCH /me for profile updates (display_name, bio, accent_color, status_text)
- Auth: Gravatar fallback for avatars on register
- DB: users table now has bio, accent_color, status_text columns
- giphy/client.go: Search() and GetTrending() against Giphy API
- upload/handlers.go: MinIO file upload + serve
- config: added GiphyConfig and MinIO config
- cmd/server: wired giphy (/gifs/search, /gifs/trending), upload (/upload), files (/files/*) routes

Frontend:
- auth store: updated User interface with new profile fields, added updateProfile()
- UserSettings.tsx: terminal-styled profile editor (display_name, bio, accent_color, status_text, avatar upload)
- GiphyPicker.tsx: terminal-styled GIF picker with search + trending
- ChatArea.tsx: integrated [GIF] button into message input
- App.tsx: imports UserSettings, added /settings route
2026-06-28 16:06:08 -04:00

121 lines
3.0 KiB
Go

package upload
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
"github.com/google/uuid"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
type Handler struct {
client *minio.Client
bucket string
}
func NewHandler(cfg *config.Config) (*Handler, error) {
endpoint := cfg.MinIO.Endpoint
if endpoint == "" {
return nil, fmt.Errorf("minio endpoint not configured")
}
client, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.MinIO.AccessKey, cfg.MinIO.SecretKey, ""),
Secure: cfg.MinIO.UseSSL,
})
if err != nil {
return nil, fmt.Errorf("minio client: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
exists, err := client.BucketExists(ctx, cfg.MinIO.Bucket)
if err != nil {
return nil, fmt.Errorf("bucket check: %w", err)
}
if !exists {
if err := client.MakeBucket(ctx, cfg.MinIO.Bucket, minio.MakeBucketOptions{}); err != nil {
return nil, fmt.Errorf("make bucket: %w", err)
}
}
return &Handler{
client: client,
bucket: cfg.MinIO.Bucket,
}, nil
}
func (h *Handler) Upload(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(50 << 20); err != nil {
http.Error(w, `{"error":"invalid form"}`, http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, `{"error":"missing file"}`, http.StatusBadRequest)
return
}
defer file.Close()
contentType := header.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
ext := ""
if idx := strings.LastIndex(header.Filename, "."); idx >= 0 {
ext = header.Filename[idx:]
}
objectName := uuid.New().String() + ext
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
info, err := h.client.PutObject(ctx, h.bucket, objectName, file, header.Size, minio.PutObjectOptions{
ContentType: contentType,
})
if err != nil {
http.Error(w, `{"error":"upload failed"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"url":"/%s/%s","size":%d}`, h.bucket, objectName, info.Size)
}
func (h *Handler) Serve(w http.ResponseWriter, r *http.Request) {
objectName := strings.TrimPrefix(r.URL.Path, "/files/")
if objectName == "" {
http.Error(w, `{"error":"missing file"}`, http.StatusBadRequest)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
obj, err := h.client.GetObject(ctx, h.bucket, objectName, minio.GetObjectOptions{})
if err != nil {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
defer obj.Close()
stat, err := obj.Stat()
if err != nil {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", stat.ContentType)
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
io.Copy(w, obj)
}