first commit
This commit is contained in:
421
images/handlers.go
Normal file
421
images/handlers.go
Normal file
@@ -0,0 +1,421 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user