first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:48:15 +03:00
commit e6f3268c28
50 changed files with 4930 additions and 0 deletions

421
images/handlers.go Normal file
View 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()
}