package handlers import ( "bytes" "context" "encoding/json" "fmt" "image" "image/jpeg" "image/png" "io" "mime/multipart" "net/http" oapi "nyanimedb/api" sqlc "nyanimedb/sql" "strings" "github.com/disintegration/imaging" 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 { 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 } // Вспомогательные функции — как раньше 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 }