187 lines
5.2 KiB
Go
187 lines
5.2 KiB
Go
package handlers
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"image"
|
||
"image/jpeg"
|
||
"image/png"
|
||
"io"
|
||
"mime/multipart"
|
||
"net/http"
|
||
oapi "nyanimedb/api"
|
||
sqlc "nyanimedb/sql"
|
||
|
||
"strings"
|
||
|
||
"github.com/disintegration/imaging"
|
||
"github.com/jackc/pgx/v5/pgconn"
|
||
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) {
|
||
// 1. Получаем multipart body
|
||
mp := request.MultipartBody
|
||
if mp == nil {
|
||
log.Errorf("PostMedia without body")
|
||
return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil
|
||
}
|
||
|
||
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 {
|
||
return oapi.PostMediaUpload400JSONResponse("Empty file"), nil
|
||
}
|
||
|
||
// 2. Проверка и декодирование (оставляем как было)
|
||
mimeType := http.DetectContentType(data)
|
||
var img image.Image
|
||
switch mimeType {
|
||
case "image/jpeg":
|
||
img, err = jpeg.Decode(bytes.NewReader(data))
|
||
case "image/png":
|
||
img, err = png.Decode(bytes.NewReader(data))
|
||
case "image/webp":
|
||
img, err = webp.Decode(bytes.NewReader(data))
|
||
default:
|
||
log.Errorf("PostMedia unsupported type: %s", mimeType)
|
||
return oapi.PostMediaUpload400JSONResponse("Unsupported image type"), nil
|
||
}
|
||
|
||
if err != nil {
|
||
log.Errorf("PostMedia decode error: %v", err)
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
|
||
// 3. Перекодируем в PNG перед отправкой
|
||
var buf bytes.Buffer
|
||
if err := imaging.Encode(&buf, img, imaging.PNG); err != nil {
|
||
log.Errorf("PostMedia encode error: %v", err)
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
|
||
// 4. Подготавливаем запрос к внешнему хранилищу (Image Server)
|
||
body := &bytes.Buffer{}
|
||
writer := multipart.NewWriter(body)
|
||
|
||
// Создаем часть с файлом
|
||
partWriter, err := writer.CreateFormFile("file", "image.png")
|
||
if err != nil {
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
if _, err := io.Copy(partWriter, &buf); err != nil {
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
|
||
// Добавляем subdir
|
||
_ = writer.WriteField("subdir", "posters")
|
||
writer.Close()
|
||
|
||
// Формируем полный URL (s.AppConfig.ImageSocket теперь base_url, например "http://storage.local")
|
||
// Убедимся, что путь не дублирует слэши
|
||
uploadURL := strings.TrimSuffix(s.ImageServerURL, "/") + "/upload"
|
||
|
||
// 5. Отправляем обычный HTTP POST
|
||
reqUpstream, err := http.NewRequestWithContext(ctx, "POST", uploadURL, body)
|
||
if err != nil {
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
reqUpstream.Header.Set("Content-Type", writer.FormDataContentType())
|
||
|
||
resp, err := http.DefaultClient.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 {
|
||
log.Errorf("PostMedia storage returned status: %d", resp.StatusCode)
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
|
||
// 6. Получаем путь из ответа хранилища
|
||
var storageResp struct {
|
||
Path string `json:"path"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&storageResp); err != nil {
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
|
||
// В storageResp.Path теперь лежит что-то вроде "posters/a1/b2/hash.png"
|
||
log.Infof("Successfully uploaded: %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 {
|
||
var pgErr *pgconn.PgError
|
||
if errors.As(err, &pgErr) {
|
||
if pgErr.Code == pgErrDuplicateKey { //duplicate key value
|
||
log.Errorf("%v", err)
|
||
|
||
_image, err = s.db.GetImageByPath(ctx, storageResp.Path)
|
||
if err != nil {
|
||
log.Errorf("%v", err)
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
image, err := mapImage(_image)
|
||
if err != nil {
|
||
log.Errorf("%v", err)
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
return oapi.PostMediaUpload200JSONResponse(*image), nil
|
||
} else {
|
||
log.Errorf("%v", err)
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
} else {
|
||
log.Errorf("%v", err)
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
}
|
||
|
||
image, err := mapImage(_image)
|
||
if err != nil {
|
||
log.Errorf("%v", err)
|
||
return oapi.PostMediaUpload500Response{}, nil
|
||
}
|
||
|
||
return oapi.PostMediaUpload200JSONResponse(*image), nil
|
||
}
|
||
|
||
func mapImage(_image sqlc.Image) (*oapi.Image, error) {
|
||
|
||
sType, err := sql2StorageType(&_image.StorageType)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("mapImage: %v", err)
|
||
}
|
||
|
||
image := oapi.Image{
|
||
Id: &_image.ID,
|
||
ImagePath: &_image.ImagePath,
|
||
StorageType: sType,
|
||
}
|
||
|
||
return &image, nil
|
||
}
|