diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index 7f2807f..fc045ed 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -18,16 +18,16 @@ import ( // } type Server struct { - db *sqlc.Queries - // publisher *rmq.Publisher - RPCclient *rmq.RPCClient + db *sqlc.Queries + ImageServerSocket string + 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{ - db: db, - // publisher: publisher, - RPCclient: rpcclient, + db: db, + ImageServerSocket: ImageServerSocket, + RPCclient: rpcclient, } } diff --git a/modules/backend/handlers/images.go b/modules/backend/handlers/images.go index c1e3d4b..dde4f99 100644 --- a/modules/backend/handlers/images.go +++ b/modules/backend/handlers/images.go @@ -3,15 +3,18 @@ package handlers import ( "bytes" "context" + "encoding/json" "fmt" "image" "image/jpeg" "image/png" "io" + "mime/multipart" + "net" "net/http" oapi "nyanimedb/api" - "os" - "path/filepath" + sqlc "nyanimedb/sql" + "strings" "github.com/disintegration/imaging" @@ -36,8 +39,7 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo } defer part.Close() - // Читаем ВЕСЬ файл в память (для небольших изображений — нормально) - // Если файлы могут быть большими — используйте лимитированный буфер (см. ниже) + // Читаем ВЕСЬ файл в память data, err := io.ReadAll(part) if err != nil { 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 err = imaging.Encode(&buf, img, imaging.PNG) if err != nil { @@ -92,26 +95,108 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo return oapi.PostMediaUpload500Response{}, nil } - // TODO: to delete - filename := part.FileName() - if filename == "" { - filename = "upload_" + generateRandomHex(8) + ".jpg" - } else { - filename = sanitizeFilename(filename) - if !strings.HasSuffix(strings.ToLower(filename), ".png") { - filename += ".png" - } - } + // --------------------------------------------------------- + // Взаимодействие с Image Storage Service через Unix Socket + // --------------------------------------------------------- - // TODO: пойти на хуй ( вызвать файловую помойку) - os.Mkdir("uploads", 0644) - err = os.WriteFile(filepath.Join("./uploads", filename), buf.Bytes(), 0644) + // 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 write: %v", err) + log.Errorf("PostMedia failed to create form file: %v", err) 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 } // Вспомогательные функции — как раньше diff --git a/modules/backend/main.go b/modules/backend/main.go index e7e6ec8..f95dee6 100644 --- a/modules/backend/main.go +++ b/modules/backend/main.go @@ -10,6 +10,7 @@ import ( "time" oapi "nyanimedb/api" + handlers "nyanimedb/modules/backend/handlers" middleware "nyanimedb/modules/backend/middlewares" "nyanimedb/modules/backend/rmq" @@ -61,7 +62,7 @@ func main() { 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{ AllowOrigins: []string{AppConfig.ServiceAddress}, diff --git a/modules/backend/types.go b/modules/backend/types.go index ceaec4e..0429187 100644 --- a/modules/backend/types.go +++ b/modules/backend/types.go @@ -1,11 +1,12 @@ package main type Config struct { - Mode string - ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"` - DdUrl string `toml:"DbUrl" env:"DATABASE_URL"` - JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"` - LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` - RmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"` - AuthEnabled string `toml:"AuthEnabled" env:"AUTH_ENABLED"` + Mode string + ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"` + DdUrl string `toml:"DbUrl" env:"DATABASE_URL"` + JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"` + LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` + RmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"` + AuthEnabled string `toml:"AuthEnabled" env:"AUTH_ENABLED"` + ImageServerSocket string `toml:"ImageServerSocket" env:"IMAGES_BASE_URL"` }