Files
aresv2/services/image_service.go
Beyhan Oğur 4362c3b83f first commit
2026-04-26 21:33:39 +03:00

239 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
configs "ares/config"
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/textproto"
"strconv"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v3"
"go.uber.org/zap"
)
// ImageOptions defines the parameters for image processing
type ImageOptions struct {
Width int
Height int
Quality int
Format string // "avif", "webp", "png", "jpg"
Folder string // e.g. "settings", "heroes"
}
// --- Token cache for external image API ---
var (
imageAPIToken string
imageAPITokenExp time.Time
imageAPITokenMu sync.Mutex
)
func getImageAPIToken() (string, error) {
if apiKey := strings.TrimSpace(configs.AppConfig.ImageAPIKey); apiKey != "" {
configs.Logger.Debug("using image API key from config")
return apiKey, nil
}
imageAPITokenMu.Lock()
defer imageAPITokenMu.Unlock()
if imageAPIToken != "" && time.Now().Before(imageAPITokenExp) {
configs.Logger.Debug("using cached image API token", zap.Time("expires_at", imageAPITokenExp))
return imageAPIToken, nil
}
payload, _ := json.Marshal(map[string]string{
"email": configs.AppConfig.ImageAPIEmail,
"password": configs.AppConfig.ImageAPIPassword,
})
resp, err := http.Post(
configs.AppConfig.ImageAPIURL+"/api/v1/auth/login",
"application/json",
bytes.NewReader(payload),
)
if err != nil {
return "", fmt.Errorf("image API login failed: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("image API login body read error: %v", err)
}
configs.Logger.Info(
"image API login response",
zap.Int("status_code", resp.StatusCode),
zap.String("body", string(respBody)),
)
var result struct {
Data struct {
AccessToken string `json:"accessToken"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("image API login decode error: %v", err)
}
if result.Data.AccessToken == "" {
return "", fmt.Errorf("image API returned empty token")
}
imageAPIToken = result.Data.AccessToken
imageAPITokenExp = time.Now().Add(6 * 24 * time.Hour) // 6 days (token valid for 7)
return imageAPIToken, nil
}
func callImageAPI(buffer []byte, filename string, opts ImageOptions) (string, error) {
token, err := getImageAPIToken()
if err != nil {
return "", err
}
configs.Logger.Debug(
"callImageAPI started",
zap.String("filename", filename),
zap.Int("bytes", len(buffer)),
zap.Int("width", opts.Width),
zap.Int("height", opts.Height),
zap.Int("quality", opts.Quality),
zap.String("format", opts.Format),
)
var body bytes.Buffer
w := multipart.NewWriter(&body)
mimeType := http.DetectContentType(buffer)
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, filename))
h.Set("Content-Type", mimeType)
part, err := w.CreatePart(h)
if err != nil {
return "", err
}
if _, err := part.Write(buffer); err != nil {
return "", err
}
if opts.Width > 0 {
_ = w.WriteField("width", strconv.Itoa(opts.Width))
}
if opts.Height > 0 {
_ = w.WriteField("height", strconv.Itoa(opts.Height))
}
if opts.Quality > 0 {
_ = w.WriteField("quality", strconv.Itoa(opts.Quality))
}
format := strings.ToLower(strings.TrimSpace(opts.Format))
if format == "" {
format = "avif"
}
_ = w.WriteField("format", format)
w.Close()
req, err := http.NewRequest("POST", configs.AppConfig.ImageAPIURL+"/api/v1/images/upload", &body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", w.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("image API upload failed: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("image API upload body read error: %v", err)
}
configs.Logger.Info(
"image API upload response",
zap.Int("status_code", resp.StatusCode),
zap.String("body", string(respBody)),
)
if resp.StatusCode == http.StatusUnauthorized {
return "", fmt.Errorf("image API key gecersiz veya suresi dolmus (HTTP 401)")
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("image API upload basarisiz (HTTP %d): %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
}
var result struct {
Data struct {
Image struct {
URL string `json:"url"`
} `json:"image"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("image API response decode error: %v", err)
}
if result.Data.Image.URL == "" {
return "", fmt.Errorf("image API returned empty URL (HTTP %d)", resp.StatusCode)
}
return result.Data.Image.URL, nil
}
// ProcessAndSaveImage handles the file upload, processing, and saving
func ProcessAndSaveImage(c fiber.Ctx, fieldName string, opts ImageOptions) (string, error) {
configs.Logger.Info(
"ProcessAndSaveImage called",
zap.String("field", fieldName),
zap.Int("width", opts.Width),
zap.Int("height", opts.Height),
zap.Int("quality", opts.Quality),
zap.String("format", opts.Format),
)
file, err := c.FormFile(fieldName)
if err != nil {
configs.Logger.Warn("no file uploaded for field", zap.Error(err), zap.String("field", fieldName))
return "", nil
}
f, err := file.Open()
if err != nil {
configs.Logger.Error("failed to open uploaded file", zap.Error(err), zap.String("filename", file.Filename))
return "", err
}
defer f.Close()
buffer, err := io.ReadAll(f)
if err != nil {
configs.Logger.Error("failed to read uploaded file bytes", zap.Error(err), zap.String("filename", file.Filename))
return "", err
}
configs.Logger.Debug("read uploaded file bytes", zap.String("filename", file.Filename), zap.Int("bytes", len(buffer)))
remoteURL, err := callImageAPI(buffer, file.Filename, opts)
if err != nil {
configs.Logger.Error("image API call failed", zap.Error(err))
return "", fmt.Errorf("resim işleme hatası: %v", err)
}
configs.Logger.Info("image saved", zap.String("url", remoteURL), zap.Int("width", opts.Width), zap.Int("height", opts.Height), zap.String("format", opts.Format), zap.Int("quality", opts.Quality))
return remoteURL, nil
}
// ProcessAndSaveImageFromBytes processes raw image bytes and saves the file
func ProcessAndSaveImageFromBytes(buffer []byte, opts ImageOptions) (string, error) {
configs.Logger.Debug("ProcessAndSaveImageFromBytes called", zap.Int("input_bytes", len(buffer)), zap.Any("options", opts))
remoteURL, err := callImageAPI(buffer, "image.jpg", opts)
if err != nil {
configs.Logger.Error("image API call failed (from bytes)", zap.Error(err))
return "", fmt.Errorf("resim işleme hatası: %v", err)
}
configs.Logger.Info("image saved from bytes", zap.String("url", remoteURL), zap.Int("width", opts.Width), zap.Int("height", opts.Height), zap.String("format", opts.Format), zap.Int("quality", opts.Quality))
return remoteURL, nil
}