package images import ( "fmt" "io" "net/url" "os" "strconv" "strings" "time" "github.com/gofiber/fiber/v3" "github.com/google/uuid" "github.com/h2non/bimg" "goimgApi/accounts" "goimgApi/configs" ) // Upload godoc // @Summary Upload an image // @Description Uploads an image and registers it to the user. // @Tags Images // @Accept multipart/form-data // @Produce json // @Security BearerAuth // @Param image formData file true "Image file" // @Param w formData int false "Width" // @Param h formData int false "Height" // @Param q formData int false "Quality (1-100)" // @Param f formData string false "Format (webp, avif, png, jpg)" // @Param mode formData string false "Mode (e.g. cover)" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]interface{} // @Failure 401 {object} map[string]interface{} // @Router /images [post] // Upload saves an image file to local storage and creates a DB record func Upload(c fiber.Ctx) error { userIDVal := c.Locals("user_id") if userIDVal == nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"}) } userID := userIDVal.(uint) file, err := c.FormFile("image") if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Failed to upload image file"}) } // Resim dosyasını belleğe okuyoruz fileHeader, err := file.Open() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to open image file"}) } defer func() { _ = fileHeader.Close() }() buffer, err := io.ReadAll(fileHeader) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to read image file"}) } // Parametreleri Form verisinden alıyoruz opts := ProcessOptions{} opts.Width, _ = strconv.Atoi(c.FormValue("w")) opts.Height, _ = strconv.Atoi(c.FormValue("h")) opts.Quality, _ = strconv.Atoi(c.FormValue("q")) opts.Format = c.FormValue("f") if c.FormValue("mode") == "cover" { opts.Cover = true } // Eğer herhangi bir düzenleme parametresi geldiyse, önce işliyoruz if opts.Width > 0 || opts.Height > 0 || opts.Format != "" || opts.Quality > 0 { processedBuffer, err := ProcessImage(buffer, opts) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Image processing failed"}) } buffer = processedBuffer } // Generate a unique filename using UUID parts := strings.Split(file.Filename, ".") ext := "" if len(parts) > 1 { ext = "." + parts[len(parts)-1] } if opts.Format != "" { ext = "." + strings.ToLower(opts.Format) } newFilename := fmt.Sprintf("%s%s", uuid.New().String(), ext) uploadPath := imageDiskPath(newFilename) publicPath := imagePublicPath(newFilename) if err := os.MkdirAll("uploads", 0755); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to prepare upload directory"}) } if err := os.WriteFile(uploadPath, buffer, 0644); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to save file"}) } sizeInfo, _ := bimg.NewImage(buffer).Size() imgFormat := bimg.NewImage(buffer).Type() quality := opts.Quality if quality == 0 { quality = 85 // default bimg quality } mode := c.FormValue("mode") if mode == "" { mode = "original" } sizeInKB := int64(len(buffer)) / 1024 if sizeInKB == 0 && len(buffer) > 0 { sizeInKB = 1 // 1 KB'dan küçükse minimum 1 KB göster } img := Image{ UserID: userID, Filename: newFilename, PublicPath: publicPath, MimeType: file.Header.Get("Content-Type"), Size: sizeInKB, Width: sizeInfo.Width, Height: sizeInfo.Height, Quality: quality, Format: imgFormat, Mode: mode, } if err := configs.DB.Create(&img).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to save image record"}) } return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "message": "Image uploaded successfully", "image_id": img.ID, "filename": img.Filename, "public_path": img.PublicPath, "image_url": buildImageURL(c, img.PublicPath), }) } // ListImages godoc // @Summary List images // @Description Returns a paginated list of images belonging to the authenticated user. // @Tags Images // @Produce json // @Security BearerAuth // @Param page query int false "Page number (default 1)" // @Param limit query int false "Items per page (default 20, max 100)" // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]interface{} // @Router /images [get] func ListImages(c fiber.Ctx) error { userIDVal := c.Locals("user_id") if userIDVal == nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"}) } userID := userIDVal.(uint) page, limit := parsePagination(c) offset := (page - 1) * limit var total int64 if err := configs.DB.Model(&Image{}).Where("user_id = ?", userID).Count(&total).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to count images"}) } var imgs []Image if err := configs.DB. Where("user_id = ?", userID). Order("created_at DESC"). Limit(limit). Offset(offset). Find(&imgs).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch images"}) } return c.JSON(fiber.Map{ "data": enrichImages(c, imgs), "total": total, "page": page, "limit": limit, }) } // GetImage godoc // @Summary Get image by ID // @Description Returns a single image record owned by the authenticated user. // @Tags Images // @Produce json // @Security BearerAuth // @Param id path int true "Image ID" // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]interface{} // @Failure 404 {object} map[string]interface{} // @Router /images/{id} [get] func GetImage(c fiber.Ctx) error { userIDVal := c.Locals("user_id") if userIDVal == nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"}) } userID := userIDVal.(uint) var img Image if err := configs.DB.Where("id = ? AND user_id = ?", c.Params("id"), userID).First(&img).Error; err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Image not found"}) } return c.JSON(enrichImage(c, img)) } // AdminListImages godoc // @Summary List all images (admin) // @Description Returns a paginated list of all images across all users. // @Tags Admin // @Produce json // @Security BearerAuth // @Param page query int false "Page number (default 1)" // @Param limit query int false "Items per page (default 20, max 100)" // @Param user_id query int false "Filter by user ID" // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]interface{} // @Failure 403 {object} map[string]interface{} // @Router /admin/images [get] func AdminListImages(c fiber.Ctx) error { page, limit := parsePagination(c) offset := (page - 1) * limit query := configs.DB.Model(&Image{}) if uid := c.Query("user_id"); uid != "" { query = query.Where("user_id = ?", uid) } var total int64 if err := query.Count(&total).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to count images"}) } var imgs []Image if err := query. Order("created_at DESC"). Limit(limit). Offset(offset). Find(&imgs).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch images"}) } return c.JSON(fiber.Map{ "data": enrichImages(c, imgs), "total": total, "page": page, "limit": limit, }) } // ---------- yardımcılar ---------- func parsePagination(c fiber.Ctx) (page, limit int) { page, _ = strconv.Atoi(c.Query("page", "1")) limit, _ = strconv.Atoi(c.Query("limit", "20")) if page < 1 { page = 1 } if limit < 1 || limit > 100 { limit = 20 } return } type imageResponse struct { ID uint `json:"id"` UserID uint `json:"user_id"` Filename string `json:"filename"` PublicPath string `json:"public_path"` ImageURL string `json:"image_url"` MimeType string `json:"mime_type"` Size int64 `json:"size_kb"` 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"` } func enrichImage(c fiber.Ctx, img Image) imageResponse { return imageResponse{ ID: img.ID, UserID: img.UserID, Filename: img.Filename, PublicPath: img.PublicPath, ImageURL: buildImageURL(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, } } func enrichImages(c fiber.Ctx, imgs []Image) []imageResponse { result := make([]imageResponse, 0, len(imgs)) for _, img := range imgs { result = append(result, enrichImage(c, img)) } return result } // Process godoc // @Summary Process Image // @Description Processes an image (resize, crop, cover, format) using the generated token. // @Tags Images // @Produce image/jpeg,image/png,image/webp,image/avif // @Param id path int true "Image ID" // @Param token query string true "Global API Token" // @Param w query int false "Width" // @Param h query int false "Height" // @Param q query int false "Quality (1-100)" // @Param f query string false "Format (webp, avif, png, jpg)" // @Param mode query string false "Mode (e.g. cover)" // @Success 200 {file} file // @Failure 401 {object} map[string]interface{} // @Failure 404 {object} map[string]interface{} // @Failure 500 {object} map[string]interface{} // @Router /images/{id}/process [get] // Process handles image manipulation via bimg func Process(c fiber.Ctx) error { token := c.Query("token") if token == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "API Token required"}) } imageIDStr := c.Params("id") // Validate the API Token belongs to a registered User var tokenUser accounts.User if err := configs.DB.Where("api_token = ?", token).First(&tokenUser).Error; err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid API Token"}) } // Check Expiration if tokenUser.ApiTokenExpiresAt != nil && tokenUser.ApiTokenExpiresAt.Before(time.Now()) { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "API Token is expired"}) } var img Image if err := configs.DB.Where("id = ?", imageIDStr).First(&img).Error; err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Image not found"}) } // Read image file from disk filePath := imageDiskPath(img.Filename) buffer, err := os.ReadFile(filePath) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to read image file from disk"}) } // Parse query params opts := ProcessOptions{} opts.Width, _ = strconv.Atoi(c.Query("w")) opts.Height, _ = strconv.Atoi(c.Query("h")) opts.Quality, _ = strconv.Atoi(c.Query("q")) opts.Format = c.Query("f") if c.Query("mode") == "cover" { opts.Cover = true } processedBuffer, err := ProcessImage(buffer, opts) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Image processing failed"}) } // Set content type and return contentType := "image/jpeg" switch strings.ToLower(opts.Format) { case "webp": contentType = "image/webp" case "png": contentType = "image/png" case "avif": contentType = "image/avif" } c.Set("Content-Type", contentType) return c.Send(processedBuffer) } func imagePublicPath(filename string) string { return "/uploads/" + strings.TrimLeft(filename, "/") } func imageDiskPath(filename string) string { return "uploads/" + strings.TrimLeft(filename, "/") } func buildImageURL(c fiber.Ctx, publicPath string) string { host := strings.TrimSpace(c.Get("X-Forwarded-Host")) if host == "" { host = strings.TrimSpace(c.Get("Host")) } if host == "" { return publicPath } scheme := strings.TrimSpace(c.Get("X-Forwarded-Proto")) if scheme == "" { scheme = c.Protocol() } return (&url.URL{Scheme: scheme, Host: host, Path: publicPath}).String() }