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