226 lines
6.7 KiB
Go
226 lines
6.7 KiB
Go
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
|
||
}
|