nyanimedb/modules/backend/handlers/images.go

226 lines
6.7 KiB
Go
Raw 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"
"context"
"encoding/json"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"mime/multipart"
"net"
"net/http"
oapi "nyanimedb/api"
sqlc "nyanimedb/sql"
"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
}
}
}
// Перекодируем в PNG (как было в оригинале)
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
}
// ---------------------------------------------------------
// Взаимодействие с Image Storage Service через Unix Socket
// ---------------------------------------------------------
// 1. Формируем Multipart Body для запроса к хранилищу
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Поле "file" (обязательное по спецификации хранилища)
// Имя файла ставим фиксированное, так как хранилище генерирует sha1
partWriter, err := writer.CreateFormFile("file", "upload.png")
if err != nil {
log.Errorf("PostMedia failed to create form file: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
// Копируем перекодированное PNG изображение в форму
if _, err := io.Copy(partWriter, &buf); err != nil {
log.Errorf("PostMedia failed to write body: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
// Поле "subdir" (опционально, ставим "posters" или можно вынести в конфиг)
if err := writer.WriteField("subdir", "posters"); err != nil {
log.Errorf("PostMedia failed to write field subdir: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
// Закрываем writer, чтобы записать boundary
if err := writer.Close(); err != nil {
log.Errorf("PostMedia failed to close multipart writer: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
// 2. Настраиваем клиент для Unix сокета
socketPath := s.ImageServerSocket
transport := &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
}
client := &http.Client{Transport: transport}
// 3. Создаем запрос
// Хост в URL ("http://unix") игнорируется транспортом, важен путь "/upload"
reqUpstream, err := http.NewRequest("POST", "http://unix/upload", body)
if err != nil {
log.Errorf("PostMedia failed to create upstream request: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
reqUpstream.Header.Set("Content-Type", writer.FormDataContentType())
// 4. Отправляем запрос
resp, err := client.Do(reqUpstream)
if err != nil {
log.Errorf("PostMedia upstream request failed: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyErr, _ := io.ReadAll(resp.Body)
log.Errorf("PostMedia upstream error %d: %s", resp.StatusCode, string(bodyErr))
return oapi.PostMediaUpload500Response{}, nil
}
// 5. Разбираем ответ {"path": "..."}
var storageResp struct {
Path string `json:"path"`
}
if err := json.NewDecoder(resp.Body).Decode(&storageResp); err != nil {
log.Errorf("PostMedia failed to decode upstream response: %v", err)
return oapi.PostMediaUpload500Response{}, nil
}
log.Infof("File uploaded to image storage: %s", storageResp.Path)
params := sqlc.CreateImageParams{
StorageType: sqlc.StorageTypeTLocal,
ImagePath: storageResp.Path,
}
// TODO: param for local/s3 case
_image, err := s.db.CreateImage(ctx, params)
if err != nil {
log.Errorf("%v", err)
return oapi.PostMediaUpload500Response{}, nil
}
sType, err := sql2StorageType(&_image.StorageType)
if err != nil {
log.Errorf("%v", err)
return oapi.PostMediaUpload500Response{}, nil
}
image := oapi.Image{
Id: &_image.ID,
ImagePath: &_image.ImagePath,
StorageType: sType,
}
return oapi.PostMediaUpload200JSONResponse(image), 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
}