feat: UploadImage is written

This commit is contained in:
Iron_Felix 2025-12-19 23:48:47 +03:00
parent 9affe90988
commit 858818c17e
4 changed files with 121 additions and 34 deletions

View file

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

View file

@ -3,15 +3,18 @@ package handlers
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"image" "image"
"image/jpeg" "image/jpeg"
"image/png" "image/png"
"io" "io"
"mime/multipart"
"net"
"net/http" "net/http"
oapi "nyanimedb/api" oapi "nyanimedb/api"
"os" sqlc "nyanimedb/sql"
"path/filepath"
"strings" "strings"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
@ -36,8 +39,7 @@ 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")
@ -85,6 +87,7 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo
} }
} }
// Перекодируем в PNG (как было в оригинале)
var buf bytes.Buffer var buf bytes.Buffer
err = imaging.Encode(&buf, img, imaging.PNG) err = imaging.Encode(&buf, img, imaging.PNG)
if err != nil { if err != nil {
@ -92,26 +95,108 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo
return oapi.PostMediaUpload500Response{}, nil return oapi.PostMediaUpload500Response{}, nil
} }
// TODO: to delete // ---------------------------------------------------------
filename := part.FileName() // Взаимодействие с Image Storage Service через Unix Socket
if filename == "" { // ---------------------------------------------------------
filename = "upload_" + generateRandomHex(8) + ".jpg"
} else {
filename = sanitizeFilename(filename)
if !strings.HasSuffix(strings.ToLower(filename), ".png") {
filename += ".png"
}
}
// TODO: пойти на хуй ( вызвать файловую помойку) // 1. Формируем Multipart Body для запроса к хранилищу
os.Mkdir("uploads", 0644) body := &bytes.Buffer{}
err = os.WriteFile(filepath.Join("./uploads", filename), buf.Bytes(), 0644) writer := multipart.NewWriter(body)
// Поле "file" (обязательное по спецификации хранилища)
// Имя файла ставим фиксированное, так как хранилище генерирует sha1
partWriter, err := writer.CreateFormFile("file", "upload.png")
if err != nil { if err != nil {
log.Errorf("PostMedia failed to write: %v", err) log.Errorf("PostMedia failed to create form file: %v", err)
return oapi.PostMediaUpload500Response{}, nil return oapi.PostMediaUpload500Response{}, nil
} }
return oapi.PostMediaUpload200JSONResponse{}, 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
} }
// Вспомогательные функции — как раньше // Вспомогательные функции — как раньше

View file

@ -10,6 +10,7 @@ import (
"time" "time"
oapi "nyanimedb/api" oapi "nyanimedb/api"
handlers "nyanimedb/modules/backend/handlers" handlers "nyanimedb/modules/backend/handlers"
middleware "nyanimedb/modules/backend/middlewares" middleware "nyanimedb/modules/backend/middlewares"
"nyanimedb/modules/backend/rmq" "nyanimedb/modules/backend/rmq"
@ -61,7 +62,7 @@ func main() {
rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second) rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second)
server := handlers.NewServer(queries, rpcClient) server := handlers.NewServer(queries, AppConfig.ImageServerSocket, rpcClient)
r.Use(cors.New(cors.Config{ r.Use(cors.New(cors.Config{
AllowOrigins: []string{AppConfig.ServiceAddress}, AllowOrigins: []string{AppConfig.ServiceAddress},

View file

@ -1,11 +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"`
} }