package handlers import ( "bytes" "context" "encoding/json" "fmt" "image" "image/jpeg" "image/png" "io" "mime/multipart" "net" "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) { // Получаем 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") 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 { log.Errorf("PostMedia empty file") return oapi.PostMediaUpload400JSONResponse("Empty file"), nil } // Проверка 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)) 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 } } } // Перекодируем в PNG (как было в оригинале) var buf bytes.Buffer err = imaging.Encode(&buf, img, imaging.PNG) if err != nil { log.Errorf("PostMedia failed to re-encode JPEG: %v", err) return oapi.PostMediaUpload500Response{}, nil } // --------------------------------------------------------- // Взаимодействие с Image Storage Service через Unix Socket // --------------------------------------------------------- // 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 create form file: %v", err) return oapi.PostMediaUpload500Response{}, 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 } // Вспомогательные функции — как раньше 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 }