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 }