363 lines
9.3 KiB
Go
363 lines
9.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
imageModels "ginimageApi/app/images/models"
|
|
"ginimageApi/configs"
|
|
imageProcessor "ginimageApi/pkg/images"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type ProcessImageResponse struct {
|
|
Message string `json:"message"`
|
|
FileName string `json:"file_name"`
|
|
PublicPath string `json:"public_path"`
|
|
URL string `json:"url"`
|
|
MimeType string `json:"mime_type"`
|
|
Size int64 `json:"size"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Quality int `json:"quality"`
|
|
Format string `json:"format"`
|
|
}
|
|
|
|
type ImageRecordResponse struct {
|
|
ID uint `json:"id"`
|
|
FileName string `json:"file_name"`
|
|
PublicPath string `json:"public_path"`
|
|
URL string `json:"url"`
|
|
MimeType string `json:"mime_type"`
|
|
Size int64 `json:"size"`
|
|
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"`
|
|
}
|
|
|
|
type ListImagesResponse struct {
|
|
Count int `json:"count"`
|
|
Items []ImageRecordResponse `json:"items"`
|
|
}
|
|
|
|
type ImageErrorResponse struct {
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
func parseIntForm(c *gin.Context, key string, defaultValue int) (int, error) {
|
|
raw := strings.TrimSpace(c.PostForm(key))
|
|
if raw == "" {
|
|
return defaultValue, nil
|
|
}
|
|
v, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s sayi olmali", key)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func parseBoolForm(c *gin.Context, key string, defaultValue bool) bool {
|
|
raw := strings.TrimSpace(strings.ToLower(c.PostForm(key)))
|
|
if raw == "" {
|
|
return defaultValue
|
|
}
|
|
return raw == "1" || raw == "true" || raw == "yes" || raw == "on"
|
|
}
|
|
|
|
func mimeFromFormat(format string) string {
|
|
switch format {
|
|
case "avif":
|
|
return "image/avif"
|
|
case "webp":
|
|
return "image/webp"
|
|
case "png":
|
|
return "image/png"
|
|
default:
|
|
return "image/jpeg"
|
|
}
|
|
}
|
|
|
|
func outputDir() string {
|
|
d := strings.TrimSpace(os.Getenv("IMAGE_OUTPUT_DIR"))
|
|
if d == "" {
|
|
return "uploads/processed"
|
|
}
|
|
return d
|
|
}
|
|
|
|
func randomSuffix() string {
|
|
b := make([]byte, 4)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "rand"
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func getUserID(c *gin.Context) (uint, bool) {
|
|
v, ok := c.Get("user_id")
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
switch t := v.(type) {
|
|
case uint:
|
|
return t, true
|
|
case int:
|
|
if t < 0 {
|
|
return 0, false
|
|
}
|
|
return uint(t), true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func requestBaseURL(c *gin.Context) string {
|
|
if base := strings.TrimSpace(os.Getenv("PUBLIC_BASE_URL")); base != "" {
|
|
return strings.TrimRight(base, "/")
|
|
}
|
|
scheme := "http"
|
|
if c.Request.TLS != nil {
|
|
scheme = "https"
|
|
}
|
|
if proto := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); proto != "" {
|
|
scheme = proto
|
|
}
|
|
return fmt.Sprintf("%s://%s", scheme, c.Request.Host)
|
|
}
|
|
|
|
func ensureDBAndUser(c *gin.Context) (uint, bool) {
|
|
if configs.DB == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
|
return 0, false
|
|
}
|
|
userID, ok := getUserID(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "kullanici bulunamadi"})
|
|
return 0, false
|
|
}
|
|
return userID, true
|
|
}
|
|
|
|
func toImageRecordResponse(c *gin.Context, img imageModels.Image) ImageRecordResponse {
|
|
return ImageRecordResponse{
|
|
ID: img.ID,
|
|
FileName: img.Filename,
|
|
PublicPath: img.PublicPath,
|
|
URL: requestBaseURL(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,
|
|
}
|
|
}
|
|
|
|
// ListImages godoc
|
|
// @Summary Giris yapan kullanicinin kayitli resimlerini listeler
|
|
// @Tags images
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Success 200 {object} ListImagesResponse
|
|
// @Failure 401 {object} ImageErrorResponse
|
|
// @Failure 500 {object} ImageErrorResponse
|
|
// @Router /api/v1/images [get]
|
|
func ListImages(c *gin.Context) {
|
|
userID, ok := ensureDBAndUser(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var images []imageModels.Image
|
|
if err := configs.DB.Where("user_id = ?", userID).Order("id desc").Find(&images).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "resimler listelenemedi"})
|
|
return
|
|
}
|
|
|
|
items := make([]ImageRecordResponse, 0, len(images))
|
|
for _, item := range images {
|
|
items = append(items, toImageRecordResponse(c, item))
|
|
}
|
|
|
|
c.JSON(http.StatusOK, ListImagesResponse{Count: len(items), Items: items})
|
|
}
|
|
|
|
// GetImage godoc
|
|
// @Summary Giris yapan kullanicinin tekil resim kaydini getirir
|
|
// @Tags images
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path int true "Image ID"
|
|
// @Success 200 {object} ImageRecordResponse
|
|
// @Failure 400 {object} ImageErrorResponse
|
|
// @Failure 401 {object} ImageErrorResponse
|
|
// @Failure 404 {object} ImageErrorResponse
|
|
// @Failure 500 {object} ImageErrorResponse
|
|
// @Router /api/v1/images/{id} [get]
|
|
func GetImage(c *gin.Context) {
|
|
userID, ok := ensureDBAndUser(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz image id"})
|
|
return
|
|
}
|
|
|
|
var image imageModels.Image
|
|
err = configs.DB.Where("id = ? AND user_id = ?", uint(id), userID).First(&image).Error
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "resim bulunamadi"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "resim getirilemedi"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, toImageRecordResponse(c, image))
|
|
}
|
|
|
|
// Process godoc
|
|
// @Summary Resmi en, boy, kalite ve formata gore isler
|
|
// @Tags images
|
|
// @Accept mpfd
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param file formData file true "Yuklenecek resim"
|
|
// @Param width formData int false "Hedef genislik (default: orijinal)"
|
|
// @Param height formData int false "Hedef yukseklik (default: orijinal)"
|
|
// @Param quality formData int false "Kalite 1-100 (default: 90)"
|
|
// @Param format formData string false "avif|webp|png|jpg|jpeg (default: avif)"
|
|
// @Param cover formData boolean false "true ise cover crop uygular"
|
|
// @Success 200 {object} ProcessImageResponse
|
|
// @Failure 400 {object} ImageErrorResponse
|
|
// @Failure 401 {object} ImageErrorResponse
|
|
// @Failure 500 {object} ImageErrorResponse
|
|
// @Router /api/v1/images/process [post]
|
|
func Process(c *gin.Context) {
|
|
file, err := c.FormFile("file")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "file alani zorunlu"})
|
|
return
|
|
}
|
|
|
|
width, err := parseIntForm(c, "width", 0)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
height, err := parseIntForm(c, "height", 0)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
quality, err := parseIntForm(c, "quality", imageProcessor.DefaultQuality)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
opts := imageProcessor.ProcessOptions{
|
|
Width: width,
|
|
Height: height,
|
|
Quality: quality,
|
|
Format: c.PostForm("format"),
|
|
Cover: parseBoolForm(c, "cover", false),
|
|
}
|
|
normalized, err := imageProcessor.NormalizeOptions(opts)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
userID, ok := ensureDBAndUser(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "dosya acilamadi"})
|
|
return
|
|
}
|
|
defer src.Close()
|
|
|
|
buffer, err := io.ReadAll(src)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "dosya okunamadi"})
|
|
return
|
|
}
|
|
|
|
processed, err := imageProcessor.ProcessImage(buffer, normalized)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := os.MkdirAll(outputDir(), 0o755); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "output klasoru olusturulamadi"})
|
|
return
|
|
}
|
|
|
|
baseName := strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename))
|
|
outName := fmt.Sprintf("%s_%d_%s.%s", baseName, time.Now().Unix(), randomSuffix(), normalized.Format)
|
|
absPath := filepath.Join(outputDir(), outName)
|
|
if err := os.WriteFile(absPath, processed, 0o644); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "islenmis dosya kaydedilemedi"})
|
|
return
|
|
}
|
|
|
|
processedSize := int64(len(processed))
|
|
imgSize, _ := imageProcessor.GetSize(processed)
|
|
publicPath := "/uploads/processed/" + outName
|
|
url := requestBaseURL(c) + publicPath
|
|
|
|
record := imageModels.Image{
|
|
UserID: userID,
|
|
Filename: outName,
|
|
PublicPath: publicPath,
|
|
MimeType: mimeFromFormat(normalized.Format),
|
|
Size: processedSize,
|
|
Width: imgSize.Width,
|
|
Height: imgSize.Height,
|
|
Quality: normalized.Quality,
|
|
Format: normalized.Format,
|
|
Mode: map[bool]string{true: "cover", false: "fit"}[normalized.Cover],
|
|
}
|
|
if err := configs.DB.Create(&record).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "db image kaydi olusturulamadi"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, ProcessImageResponse{
|
|
Message: "resim isleme tamamlandi",
|
|
FileName: outName,
|
|
PublicPath: publicPath,
|
|
URL: url,
|
|
MimeType: record.MimeType,
|
|
Size: record.Size,
|
|
Width: record.Width,
|
|
Height: record.Height,
|
|
Quality: record.Quality,
|
|
Format: record.Format,
|
|
})
|
|
}
|