Files
Arkie-Library-Backend/internal/handlers/upload.go
2026-05-16 00:18:22 +08:00

116 lines
3.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/google/uuid"
)
const uploadMaxBytes = 512 << 20 // 512 MiB upper bound per upload
// UploadDeps configures admin multipart upload (local disk and/or S3).
type UploadDeps struct {
LocalDir string
MaxMultipartMem int64
S3 *s3.Client
S3Bucket string
AWSRegion string
S3Prefix string // e.g. "uploads" (no leading/trailing slashes)
S3PublicBase string // optional, e.g. https://cdn.example.com — else virtual-hosted S3 URL
}
func UploadFile(d UploadDeps) http.HandlerFunc {
maxMem := d.MaxMultipartMem
if maxMem <= 0 {
maxMem = 32 << 20
}
return func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(maxMem); err != nil {
http.Error(w, "multipart required", http.StatusBadRequest)
return
}
file, hdr, err := r.FormFile("file")
if err != nil {
http.Error(w, "file field required", http.StatusBadRequest)
return
}
defer file.Close()
ext := filepath.Ext(hdr.Filename)
if ext == "" {
ext = ".bin"
}
name := uuid.NewString() + ext
data, err := io.ReadAll(io.LimitReader(file, uploadMaxBytes+1))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(data) > uploadMaxBytes {
http.Error(w, "file too large", http.StatusRequestEntityTooLarge)
return
}
ct := hdr.Header.Get("Content-Type")
if ct == "" {
ct = http.DetectContentType(data)
}
if d.S3 != nil && strings.TrimSpace(d.S3Bucket) != "" {
pfx := strings.Trim(strings.TrimSpace(d.S3Prefix), "/")
if pfx == "" {
pfx = "uploads"
}
key := pfx + "/" + name
ctx := r.Context()
_, err := d.S3.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(d.S3Bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
ContentType: aws.String(ct),
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
pub := publicObjectURL(d.S3PublicBase, d.S3Bucket, d.AWSRegion, key)
writeJSON(w, map[string]any{"url": pub, "filename": name, "storage": "s3"})
return
}
dst := filepath.Join(d.LocalDir, name)
if err := os.MkdirAll(d.LocalDir, 0o755); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
out, err := os.Create(dst)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer out.Close()
if _, err := out.Write(data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]any{"url": "/uploads/" + name, "filename": name, "storage": "local"})
}
}
func publicObjectURL(base, bucket, region, key string) string {
base = strings.TrimSpace(base)
if base != "" {
return strings.TrimSuffix(base, "/") + "/" + key
}
// Virtual-hostedstyle URL (works for most buckets; use S3_PUBLIC_BASE_URL if not).
return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", bucket, region, key)
}