diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index b75fb66..7f2807f 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -18,16 +18,16 @@ import ( // } type Server struct { - db *sqlc.Queries - ImageServerURL string - RPCclient *rmq.RPCClient + db *sqlc.Queries + // publisher *rmq.Publisher + RPCclient *rmq.RPCClient } -func NewServer(db *sqlc.Queries, ImageServerURL string, rpcclient *rmq.RPCClient) *Server { +func NewServer(db *sqlc.Queries, rpcclient *rmq.RPCClient) *Server { return &Server{ - db: db, - ImageServerURL: ImageServerURL, - RPCclient: rpcclient, + db: db, + // publisher: publisher, + RPCclient: rpcclient, } } diff --git a/modules/backend/handlers/images.go b/modules/backend/handlers/images.go index c3d3a7f..c1e3d4b 100644 --- a/modules/backend/handlers/images.go +++ b/modules/backend/handlers/images.go @@ -3,35 +3,32 @@ 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" - + "os" + "path/filepath" "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 + // Получаем multipart body mp := request.MultipartBody if mp == nil { log.Errorf("PostMedia without body") return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil } + // Парсим первую часть (предполагаем, что файл в поле "file") part, err := mp.NextPart() if err != nil { log.Errorf("PostMedia without file") @@ -39,6 +36,8 @@ 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") @@ -46,142 +45,97 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo } if len(data) == 0 { + log.Errorf("PostMedia empty file") return oapi.PostMediaUpload400JSONResponse("Empty file"), nil } - // 2. Проверка и декодирование (оставляем как было) + // Проверка MIME по первым 512 байтам 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 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) + { + 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": + { + 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": + { + img, err = webp.Decode(bytes.NewReader(data)) + if err != nil { + log.Errorf("PostMedia cannot decode file: %v", err) return oapi.PostMediaUpload500Response{}, nil } - } else { - log.Errorf("%v", err) - return oapi.PostMediaUpload500Response{}, nil } } - image, err := mapImage(_image) + var buf bytes.Buffer + err = imaging.Encode(&buf, img, imaging.PNG) if err != nil { - log.Errorf("%v", err) + log.Errorf("PostMedia failed to re-encode JPEG: %v", err) return oapi.PostMediaUpload500Response{}, nil } - return oapi.PostMediaUpload200JSONResponse(*image), 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" + } + } -func mapImage(_image sqlc.Image) (*oapi.Image, error) { - - sType, err := sql2StorageType(&_image.StorageType) + // TODO: пойти на хуй ( вызвать файловую помойку) + os.Mkdir("uploads", 0644) + err = os.WriteFile(filepath.Join("./uploads", filename), buf.Bytes(), 0644) if err != nil { - return nil, fmt.Errorf("mapImage: %v", err) + log.Errorf("PostMedia failed to write: %v", err) + return oapi.PostMediaUpload500Response{}, nil } - image := oapi.Image{ - Id: &_image.ID, - ImagePath: &_image.ImagePath, - StorageType: sType, - } - - return &image, nil + return oapi.PostMediaUpload200JSONResponse{}, nil +} + +// Вспомогательные функции — как раньше +func generateRandomHex(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = byte('a' + (i % 16)) + } + return fmt.Sprintf("%x", b) +} + +func sanitizeFilename(name string) string { + var clean strings.Builder + for _, r := range name { + if (r >= 'a' && r <= 'z') || + (r >= 'A' && r <= 'Z') || + (r >= '0' && r <= '9') || + r == '.' || r == '_' || r == '-' { + clean.WriteRune(r) + } + } + s := clean.String() + if s == "" { + return "file" + } + return s } diff --git a/modules/backend/main.go b/modules/backend/main.go index a67e533..e7e6ec8 100644 --- a/modules/backend/main.go +++ b/modules/backend/main.go @@ -10,7 +10,6 @@ import ( "time" oapi "nyanimedb/api" - handlers "nyanimedb/modules/backend/handlers" middleware "nyanimedb/modules/backend/middlewares" "nyanimedb/modules/backend/rmq" @@ -62,7 +61,7 @@ func main() { rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second) - server := handlers.NewServer(queries, AppConfig.ImageServerURL, rpcClient) + server := handlers.NewServer(queries, rpcClient) r.Use(cors.New(cors.Config{ AllowOrigins: []string{AppConfig.ServiceAddress}, diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 99b59be..7117456 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -3,11 +3,6 @@ SELECT id, storage_type, image_path FROM images WHERE id = sqlc.arg('illust_id')::bigint; --- name: GetImageByPath :one -SELECT id, storage_type, image_path -FROM images -WHERE image_path = sqlc.arg('illust_path')::text; - -- name: CreateImage :one INSERT INTO images (storage_type, image_path) VALUES ($1, $2) diff --git a/modules/backend/types.go b/modules/backend/types.go index 36c5380..ceaec4e 100644 --- a/modules/backend/types.go +++ b/modules/backend/types.go @@ -8,5 +8,4 @@ type Config struct { LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` RmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"` AuthEnabled string `toml:"AuthEnabled" env:"AUTH_ENABLED"` - ImageServerURL string `toml:"ImageServerURL" env:"IMAGES_BASE_URL"` } diff --git a/sql/queries.sql.go b/sql/queries.sql.go index de8e12d..0c863e8 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -102,19 +102,6 @@ func (q *Queries) GetImageByID(ctx context.Context, illustID int64) (Image, erro return i, err } -const getImageByPath = `-- name: GetImageByPath :one -SELECT id, storage_type, image_path -FROM images -WHERE image_path = $1::text -` - -func (q *Queries) GetImageByPath(ctx context.Context, illustPath string) (Image, error) { - row := q.db.QueryRow(ctx, getImageByPath, illustPath) - var i Image - err := row.Scan(&i.ID, &i.StorageType, &i.ImagePath) - return i, err -} - const getReviewByID = `-- name: GetReviewByID :one SELECT id, data, rating, user_id, title_id, created_at