feat: file upload imlemented
All checks were successful
Build and Deploy Go App / build (push) Successful in 9m4s
Build and Deploy Go App / deploy (push) Successful in 41s

This commit is contained in:
Iron_Felix 2025-12-06 04:13:27 +03:00
parent 7623adf2a7
commit 184868b142
7 changed files with 355 additions and 8 deletions

View file

@ -0,0 +1,141 @@
package handlers
import (
"bytes"
"context"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"net/http"
oapi "nyanimedb/api"
"os"
"path/filepath"
"strings"
"github.com/disintegration/imaging"
log "github.com/sirupsen/logrus"
"golang.org/x/image/webp"
)
// PostMediaUpload implements oapi.StrictServerInterface.
func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUploadRequestObject) (oapi.PostMediaUploadResponseObject, error) {
// Получаем multipart body
mp := request.MultipartBody
if mp == nil {
log.Errorf("PostMedia without body")
return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil
}
// Парсим первую часть (предполагаем, что файл в поле "file")
part, err := mp.NextPart()
if err != nil {
log.Errorf("PostMedia without file")
return oapi.PostMediaUpload400JSONResponse("File required"), nil
}
defer part.Close()
// Читаем ВЕСЬ файл в память (для небольших изображений — нормально)
// Если файлы могут быть большими — используйте лимитированный буфер (см. ниже)
data, err := io.ReadAll(part)
if err != nil {
log.Errorf("PostMedia cannot read file")
return oapi.PostMediaUpload400JSONResponse("File required"), nil
}
if len(data) == 0 {
log.Errorf("PostMedia empty file")
return oapi.PostMediaUpload400JSONResponse("Empty file"), nil
}
// Проверка MIME по первым 512 байтам
mimeType := http.DetectContentType(data)
if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" {
log.Errorf("PostMedia bad type")
return oapi.PostMediaUpload400JSONResponse("Bad data type"), nil
}
// Декодируем изображение из буфера
var img image.Image
switch mimeType {
case "image/jpeg":
{
img, err = jpeg.Decode(bytes.NewReader(data))
if err != nil {
log.Errorf("PostMedia cannot decode file: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
}
case "image/png":
{
img, err = png.Decode(bytes.NewReader(data))
if err != nil {
log.Errorf("PostMedia cannot decode file: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
}
case "image/webp":
{
img, err = webp.Decode(bytes.NewReader(data))
if err != nil {
log.Errorf("PostMedia cannot decode file: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
}
}
// Перекодируем в чистый JPEG (без EXIF, сжатие, RGB)
var buf bytes.Buffer
err = imaging.Encode(&buf, img, imaging.PNG)
if err != nil {
log.Errorf("PostMedia failed to re-encode JPEG: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
// TODO: to delete
filename := part.FileName()
if filename == "" {
filename = "upload_" + generateRandomHex(8) + ".jpg"
} else {
filename = sanitizeFilename(filename)
if !strings.HasSuffix(strings.ToLower(filename), ".jpg") {
filename += ".jpg"
}
}
// TODO: пойти на хуй ( вызвать файловую помойку)
err = os.WriteFile(filepath.Join("/uploads", filename), buf.Bytes(), 0644)
if err != nil {
log.Errorf("PostMedia failed to write: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
return oapi.PostMediaUpload200JSONResponse{}, nil
}
// Вспомогательные функции — как раньше
func generateRandomHex(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = byte('a' + (i % 16))
}
return fmt.Sprintf("%x", b)
}
func sanitizeFilename(name string) string {
var clean strings.Builder
for _, r := range name {
if (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') ||
r == '.' || r == '_' || r == '-' {
clean.WriteRune(r)
}
}
s := clean.String()
if s == "" {
return "file"
}
return s
}