422 lines
12 KiB
Go
422 lines
12 KiB
Go
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()
|
||
}
|