fix
All checks were successful
Build (backend build only) / build (push) Successful in 3m28s

This commit is contained in:
Iron_Felix 2025-12-20 00:17:54 +03:00
parent 53e270015c
commit c58b578023
4 changed files with 47 additions and 94 deletions

View file

@ -18,16 +18,16 @@ import (
// } // }
type Server struct { type Server struct {
db *sqlc.Queries db *sqlc.Queries
ImageServerSocket string ImageServerURL string
RPCclient *rmq.RPCClient RPCclient *rmq.RPCClient
} }
func NewServer(db *sqlc.Queries, ImageServerSocket string, rpcclient *rmq.RPCClient) *Server { func NewServer(db *sqlc.Queries, ImageServerURL string, rpcclient *rmq.RPCClient) *Server {
return &Server{ return &Server{
db: db, db: db,
ImageServerSocket: ImageServerSocket, ImageServerURL: ImageServerURL,
RPCclient: rpcclient, RPCclient: rpcclient,
} }
} }

View file

@ -10,7 +10,6 @@ import (
"image/png" "image/png"
"io" "io"
"mime/multipart" "mime/multipart"
"net"
"net/http" "net/http"
oapi "nyanimedb/api" oapi "nyanimedb/api"
sqlc "nyanimedb/sql" sqlc "nyanimedb/sql"
@ -24,14 +23,13 @@ import (
// PostMediaUpload implements oapi.StrictServerInterface. // PostMediaUpload implements oapi.StrictServerInterface.
func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUploadRequestObject) (oapi.PostMediaUploadResponseObject, error) { func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUploadRequestObject) (oapi.PostMediaUploadResponseObject, error) {
// Получаем multipart body // 1. Получаем multipart body
mp := request.MultipartBody mp := request.MultipartBody
if mp == nil { if mp == nil {
log.Errorf("PostMedia without body") log.Errorf("PostMedia without body")
return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil
} }
// Парсим первую часть (предполагаем, что файл в поле "file")
part, err := mp.NextPart() part, err := mp.NextPart()
if err != nil { if err != nil {
log.Errorf("PostMedia without file") log.Errorf("PostMedia without file")
@ -39,7 +37,6 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo
} }
defer part.Close() defer part.Close()
// Читаем ВЕСЬ файл в память
data, err := io.ReadAll(part) data, err := io.ReadAll(part)
if err != nil { if err != nil {
log.Errorf("PostMedia cannot read file") log.Errorf("PostMedia cannot read file")
@ -47,108 +44,65 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo
} }
if len(data) == 0 { if len(data) == 0 {
log.Errorf("PostMedia empty file")
return oapi.PostMediaUpload400JSONResponse("Empty file"), nil return oapi.PostMediaUpload400JSONResponse("Empty file"), nil
} }
// Проверка MIME по первым 512 байтам // 2. Проверка и декодирование (оставляем как было)
mimeType := http.DetectContentType(data) 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 var img image.Image
switch mimeType { switch mimeType {
case "image/jpeg": case "image/jpeg":
{ img, err = jpeg.Decode(bytes.NewReader(data))
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": case "image/png":
{ img, err = png.Decode(bytes.NewReader(data))
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": case "image/webp":
{ img, err = webp.Decode(bytes.NewReader(data))
img, err = webp.Decode(bytes.NewReader(data)) default:
if err != nil { log.Errorf("PostMedia unsupported type: %s", mimeType)
log.Errorf("PostMedia cannot decode file: %v", err) return oapi.PostMediaUpload400JSONResponse("Unsupported image type"), nil
return oapi.PostMediaUpload500Response{}, nil
}
}
} }
// Перекодируем в PNG (как было в оригинале)
var buf bytes.Buffer
err = imaging.Encode(&buf, img, imaging.PNG)
if err != nil { if err != nil {
log.Errorf("PostMedia failed to re-encode JPEG: %v", err) log.Errorf("PostMedia decode error: %v", err)
return oapi.PostMediaUpload500Response{}, nil return oapi.PostMediaUpload500Response{}, nil
} }
// --------------------------------------------------------- // 3. Перекодируем в PNG перед отправкой
// Взаимодействие с Image Storage Service через Unix Socket 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
}
// 1. Формируем Multipart Body для запроса к хранилищу // 4. Подготавливаем запрос к внешнему хранилищу (Image Server)
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
// Поле "file" (обязательное по спецификации хранилища) // Создаем часть с файлом
// Имя файла ставим фиксированное, так как хранилище генерирует sha1 partWriter, err := writer.CreateFormFile("file", "image.png")
partWriter, err := writer.CreateFormFile("file", "upload.png")
if err != nil { if err != nil {
log.Errorf("PostMedia failed to create form file: %v", err)
return oapi.PostMediaUpload500Response{}, nil return oapi.PostMediaUpload500Response{}, nil
} }
// Копируем перекодированное PNG изображение в форму
if _, err := io.Copy(partWriter, &buf); err != nil { if _, err := io.Copy(partWriter, &buf); err != nil {
log.Errorf("PostMedia failed to write body: %v", err)
return oapi.PostMediaUpload500Response{}, nil return oapi.PostMediaUpload500Response{}, nil
} }
// Поле "subdir" (опционально, ставим "posters" или можно вынести в конфиг) // Добавляем subdir
if err := writer.WriteField("subdir", "posters"); err != nil { _ = writer.WriteField("subdir", "posters")
log.Errorf("PostMedia failed to write field subdir: %v", err) writer.Close()
return oapi.PostMediaUpload500Response{}, nil
}
// Закрываем writer, чтобы записать boundary // Формируем полный URL (s.AppConfig.ImageSocket теперь base_url, например "http://storage.local")
if err := writer.Close(); err != nil { // Убедимся, что путь не дублирует слэши
log.Errorf("PostMedia failed to close multipart writer: %v", err) uploadURL := strings.TrimSuffix(s.ImageServerURL, "/") + "/upload"
return oapi.PostMediaUpload500Response{}, nil
}
// 2. Настраиваем клиент для Unix сокета // 5. Отправляем обычный HTTP POST
socketPath := s.ImageServerSocket reqUpstream, err := http.NewRequestWithContext(ctx, "POST", uploadURL, body)
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 { if err != nil {
log.Errorf("PostMedia failed to create upstream request: %v", err)
return oapi.PostMediaUpload500Response{}, nil return oapi.PostMediaUpload500Response{}, nil
} }
reqUpstream.Header.Set("Content-Type", writer.FormDataContentType()) reqUpstream.Header.Set("Content-Type", writer.FormDataContentType())
// 4. Отправляем запрос resp, err := http.DefaultClient.Do(reqUpstream)
resp, err := client.Do(reqUpstream)
if err != nil { if err != nil {
log.Errorf("PostMedia upstream request failed: %v", err) log.Errorf("PostMedia upstream request failed: %v", err)
return oapi.PostMediaUpload500Response{}, nil return oapi.PostMediaUpload500Response{}, nil
@ -156,21 +110,20 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
bodyErr, _ := io.ReadAll(resp.Body) log.Errorf("PostMedia storage returned status: %d", resp.StatusCode)
log.Errorf("PostMedia upstream error %d: %s", resp.StatusCode, string(bodyErr))
return oapi.PostMediaUpload500Response{}, nil return oapi.PostMediaUpload500Response{}, nil
} }
// 5. Разбираем ответ {"path": "..."} // 6. Получаем путь из ответа хранилища
var storageResp struct { var storageResp struct {
Path string `json:"path"` Path string `json:"path"`
} }
if err := json.NewDecoder(resp.Body).Decode(&storageResp); err != nil { if err := json.NewDecoder(resp.Body).Decode(&storageResp); err != nil {
log.Errorf("PostMedia failed to decode upstream response: %v", err)
return oapi.PostMediaUpload500Response{}, nil return oapi.PostMediaUpload500Response{}, nil
} }
log.Infof("File uploaded to image storage: %s", storageResp.Path) // В storageResp.Path теперь лежит что-то вроде "posters/a1/b2/hash.png"
log.Infof("Successfully uploaded: %s", storageResp.Path)
params := sqlc.CreateImageParams{ params := sqlc.CreateImageParams{
StorageType: sqlc.StorageTypeTLocal, StorageType: sqlc.StorageTypeTLocal,

View file

@ -62,7 +62,7 @@ func main() {
rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second) rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second)
server := handlers.NewServer(queries, AppConfig.ImageServerSocket, rpcClient) server := handlers.NewServer(queries, AppConfig.ImageServerURL, rpcClient)
r.Use(cors.New(cors.Config{ r.Use(cors.New(cors.Config{
AllowOrigins: []string{AppConfig.ServiceAddress}, AllowOrigins: []string{AppConfig.ServiceAddress},

View file

@ -1,12 +1,12 @@
package main package main
type Config struct { type Config struct {
Mode string Mode string
ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"` ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"`
DdUrl string `toml:"DbUrl" env:"DATABASE_URL"` DdUrl string `toml:"DbUrl" env:"DATABASE_URL"`
JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"` JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"`
LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"`
RmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"` RmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"`
AuthEnabled string `toml:"AuthEnabled" env:"AUTH_ENABLED"` AuthEnabled string `toml:"AuthEnabled" env:"AUTH_ENABLED"`
ImageServerSocket string `toml:"ImageServerSocket" env:"IMAGES_BASE_URL"` ImageServerURL string `toml:"ImageServerURL" env:"IMAGES_BASE_URL"`
} }