package handlers import ( "crypto/rand" "encoding/hex" "fmt" "io" "net/http" "os" "path/filepath" "strconv" "strings" "time" imageModels "ginimageApi/app/images/models" "ginimageApi/configs" imageProcessor "ginimageApi/pkg/images" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type ProcessImageResponse struct { Message string `json:"message"` FileName string `json:"file_name"` PublicPath string `json:"public_path"` URL string `json:"url"` MimeType string `json:"mime_type"` Size int64 `json:"size"` Width int `json:"width"` Height int `json:"height"` Quality int `json:"quality"` Format string `json:"format"` } type ImageRecordResponse struct { ID uint `json:"id"` FileName string `json:"file_name"` PublicPath string `json:"public_path"` URL string `json:"url"` MimeType string `json:"mime_type"` Size int64 `json:"size"` Width int `json:"width"` Height int `json:"height"` Quality int `json:"quality"` Format string `json:"format"` Mode string `json:"mode"` CreatedAt time.Time `json:"created_at"` } type ListImagesResponse struct { Count int `json:"count"` Items []ImageRecordResponse `json:"items"` } type ImageErrorResponse struct { Error string `json:"error"` } func parseIntForm(c *gin.Context, key string, defaultValue int) (int, error) { raw := strings.TrimSpace(c.PostForm(key)) if raw == "" { return defaultValue, nil } v, err := strconv.Atoi(raw) if err != nil { return 0, fmt.Errorf("%s sayi olmali", key) } return v, nil } func parseBoolForm(c *gin.Context, key string, defaultValue bool) bool { raw := strings.TrimSpace(strings.ToLower(c.PostForm(key))) if raw == "" { return defaultValue } return raw == "1" || raw == "true" || raw == "yes" || raw == "on" } func mimeFromFormat(format string) string { switch format { case "avif": return "image/avif" case "webp": return "image/webp" case "png": return "image/png" default: return "image/jpeg" } } func outputDir() string { d := strings.TrimSpace(os.Getenv("IMAGE_OUTPUT_DIR")) if d == "" { return "uploads/processed" } return d } func randomSuffix() string { b := make([]byte, 4) if _, err := rand.Read(b); err != nil { return "rand" } return hex.EncodeToString(b) } func getUserID(c *gin.Context) (uint, bool) { v, ok := c.Get("user_id") if !ok { return 0, false } switch t := v.(type) { case uint: return t, true case int: if t < 0 { return 0, false } return uint(t), true default: return 0, false } } func requestBaseURL(c *gin.Context) string { if base := strings.TrimSpace(os.Getenv("PUBLIC_BASE_URL")); base != "" { return strings.TrimRight(base, "/") } scheme := "http" if c.Request.TLS != nil { scheme = "https" } if proto := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); proto != "" { scheme = proto } return fmt.Sprintf("%s://%s", scheme, c.Request.Host) } func ensureDBAndUser(c *gin.Context) (uint, bool) { if configs.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"}) return 0, false } userID, ok := getUserID(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "kullanici bulunamadi"}) return 0, false } return userID, true } func toImageRecordResponse(c *gin.Context, img imageModels.Image) ImageRecordResponse { return ImageRecordResponse{ ID: img.ID, FileName: img.Filename, PublicPath: img.PublicPath, URL: requestBaseURL(c) + img.PublicPath, MimeType: img.MimeType, Size: img.Size, Width: img.Width, Height: img.Height, Quality: img.Quality, Format: img.Format, Mode: img.Mode, CreatedAt: img.CreatedAt, } } // ListImages godoc // @Summary Giris yapan kullanicinin kayitli resimlerini listeler // @Tags images // @Produce json // @Security BearerAuth // @Success 200 {object} ListImagesResponse // @Failure 401 {object} ImageErrorResponse // @Failure 500 {object} ImageErrorResponse // @Router /api/v1/images [get] func ListImages(c *gin.Context) { userID, ok := ensureDBAndUser(c) if !ok { return } var images []imageModels.Image if err := configs.DB.Where("user_id = ?", userID).Order("id desc").Find(&images).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "resimler listelenemedi"}) return } items := make([]ImageRecordResponse, 0, len(images)) for _, item := range images { items = append(items, toImageRecordResponse(c, item)) } c.JSON(http.StatusOK, ListImagesResponse{Count: len(items), Items: items}) } // GetImage godoc // @Summary Giris yapan kullanicinin tekil resim kaydini getirir // @Tags images // @Produce json // @Security BearerAuth // @Param id path int true "Image ID" // @Success 200 {object} ImageRecordResponse // @Failure 400 {object} ImageErrorResponse // @Failure 401 {object} ImageErrorResponse // @Failure 404 {object} ImageErrorResponse // @Failure 500 {object} ImageErrorResponse // @Router /api/v1/images/{id} [get] func GetImage(c *gin.Context) { userID, ok := ensureDBAndUser(c) if !ok { return } id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz image id"}) return } var image imageModels.Image err = configs.DB.Where("id = ? AND user_id = ?", uint(id), userID).First(&image).Error if err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "resim bulunamadi"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "resim getirilemedi"}) return } c.JSON(http.StatusOK, toImageRecordResponse(c, image)) } // Process godoc // @Summary Resmi en, boy, kalite ve formata gore isler // @Tags images // @Accept mpfd // @Produce json // @Security BearerAuth // @Param file formData file true "Yuklenecek resim" // @Param width formData int false "Hedef genislik (default: orijinal)" // @Param height formData int false "Hedef yukseklik (default: orijinal)" // @Param quality formData int false "Kalite 1-100 (default: 90)" // @Param format formData string false "avif|webp|png|jpg|jpeg (default: avif)" // @Param cover formData boolean false "true ise cover crop uygular" // @Success 200 {object} ProcessImageResponse // @Failure 400 {object} ImageErrorResponse // @Failure 401 {object} ImageErrorResponse // @Failure 500 {object} ImageErrorResponse // @Router /api/v1/images/process [post] func Process(c *gin.Context) { file, err := c.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "file alani zorunlu"}) return } width, err := parseIntForm(c, "width", 0) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } height, err := parseIntForm(c, "height", 0) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } quality, err := parseIntForm(c, "quality", imageProcessor.DefaultQuality) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } opts := imageProcessor.ProcessOptions{ Width: width, Height: height, Quality: quality, Format: c.PostForm("format"), Cover: parseBoolForm(c, "cover", false), } normalized, err := imageProcessor.NormalizeOptions(opts) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID, ok := ensureDBAndUser(c) if !ok { return } src, err := file.Open() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "dosya acilamadi"}) return } defer src.Close() buffer, err := io.ReadAll(src) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "dosya okunamadi"}) return } processed, err := imageProcessor.ProcessImage(buffer, normalized) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := os.MkdirAll(outputDir(), 0o755); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "output klasoru olusturulamadi"}) return } baseName := strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename)) outName := fmt.Sprintf("%s_%d_%s.%s", baseName, time.Now().Unix(), randomSuffix(), normalized.Format) absPath := filepath.Join(outputDir(), outName) if err := os.WriteFile(absPath, processed, 0o644); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "islenmis dosya kaydedilemedi"}) return } processedSize := int64(len(processed)) imgSize, _ := imageProcessor.GetSize(processed) publicPath := "/uploads/processed/" + outName url := requestBaseURL(c) + publicPath record := imageModels.Image{ UserID: userID, Filename: outName, PublicPath: publicPath, MimeType: mimeFromFormat(normalized.Format), Size: processedSize, Width: imgSize.Width, Height: imgSize.Height, Quality: normalized.Quality, Format: normalized.Format, Mode: map[bool]string{true: "cover", false: "fit"}[normalized.Cover], } if err := configs.DB.Create(&record).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db image kaydi olusturulamadi"}) return } c.JSON(http.StatusOK, ProcessImageResponse{ Message: "resim isleme tamamlandi", FileName: outName, PublicPath: publicPath, URL: url, MimeType: record.MimeType, Size: record.Size, Width: record.Width, Height: record.Height, Quality: record.Quality, Format: record.Format, }) }