Files
goimgApi/images/handlers.go
Beyhan Oğur e6f3268c28 first commit
2026-04-26 21:48:15 +03:00

422 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}