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 }