nyanimedb/modules/backend/handlers/images.go
Iron_Felix 4fe077d229
All checks were successful
Build (backend build only) / build (push) Successful in 3m18s
Build and Deploy Go App / build (push) Successful in 8m32s
Build and Deploy Go App / deploy (push) Successful in 27s
feat: now PostImage return Image struct even on duplicate data
2025-12-20 01:16:26 +03:00

187 lines
5.2 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"
"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
}