package routes import ( "database/sql" "encoding/json" "fmt" "log/slog" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/google/uuid" "github.com/gorilla/mux" ) type ProductImageItem struct { ID int64 `json:"id"` FileName string `json:"file_name"` FileSize int64 `json:"file_size"` Storage string `json:"storage_path"` ContentURL string `json:"content_url"` } // GET /api/product-images?code=...&color=... func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { reqID := strings.TrimSpace(r.Header.Get("X-Request-ID")) if reqID == "" { reqID = uuid.NewString() } w.Header().Set("X-Request-ID", reqID) code := strings.TrimSpace(r.URL.Query().Get("code")) color := strings.TrimSpace(r.URL.Query().Get("color")) if code == "" { slog.Warn("product_images.list.bad_request", "req_id", reqID, "path", r.URL.Path, "query", r.URL.RawQuery, "reason", "missing_code", ) http.Error(w, "Eksik parametre: code gerekli", http.StatusBadRequest) return } query := ` SELECT b.id, b.file_name, COALESCE(b.file_size, 0) AS file_size, COALESCE(b.storage_path, '') AS storage_path FROM dfblob b JOIN mmitem i ON i.id = b.src_id WHERE b.typ = 'img' AND b.src_table = 'mmitem' AND UPPER(i.code) = UPPER($1) AND ($2 = '' OR b.file_name ILIKE '%' || '-' || $2 || '-%') ORDER BY COALESCE(b.sort_order, 999999), b.zlins_dttm DESC, b.id DESC ` rows, err := pg.Query(query, code, color) if err != nil { slog.Error("product_images.list.query_failed", "req_id", reqID, "code", code, "color", color, "err", err.Error(), ) http.Error(w, "Gorsel sorgu hatasi: "+err.Error(), http.StatusInternalServerError) return } defer rows.Close() items := make([]ProductImageItem, 0, 16) for rows.Next() { var it ProductImageItem if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil { continue } it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID) items = append(items, it) } slog.Info("product_images.list.ok", "req_id", reqID, "code", code, "color", color, "count", len(items), ) w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(items) } } // GET /api/product-images/{id}/content func GetProductImageContentHandler(pg *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { reqID := strings.TrimSpace(r.Header.Get("X-Request-ID")) if reqID == "" { reqID = uuid.NewString() } w.Header().Set("X-Request-ID", reqID) idStr := mux.Vars(r)["id"] id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { slog.Warn("product_images.content.bad_request", "req_id", reqID, "id_raw", idStr, "path", r.URL.Path, "reason", "invalid_id", ) http.Error(w, "Gecersiz gorsel id", http.StatusBadRequest) return } var ( fileName string storagePath string storedInDB bool binData []byte ) err = pg.QueryRow(` SELECT COALESCE(file_name, ''), COALESCE(storage_path, ''), COALESCE(stored_in_db, false), bin FROM dfblob WHERE id = $1 AND typ = 'img' `, id).Scan(&fileName, &storagePath, &storedInDB, &binData) if err != nil { if err == sql.ErrNoRows { slog.Warn("product_images.content.not_found_row", "req_id", reqID, "id", id, ) http.NotFound(w, r) return } slog.Error("product_images.content.query_failed", "req_id", reqID, "id", id, "err", err.Error(), ) http.Error(w, "Gorsel okunamadi: "+err.Error(), http.StatusInternalServerError) return } if storedInDB && len(binData) > 0 { slog.Info("product_images.content.served_from_db", "req_id", reqID, "id", id, "file_name", fileName, "bytes", len(binData), ) w.Header().Set("Content-Type", http.DetectContentType(binData)) w.Header().Set("Cache-Control", "public, max-age=3600") _, _ = w.Write(binData) return } resolved, tried := resolveStoragePath(storagePath) if resolved == "" { slog.Warn("product_images.content.file_not_found", "req_id", reqID, "id", id, "stored_in_db", storedInDB, "file_name", fileName, "storage_path", storagePath, "tried", tried, ) http.NotFound(w, r) return } slog.Info("product_images.content.served_from_file", "req_id", reqID, "id", id, "file_name", fileName, "storage_path", storagePath, "resolved_path", resolved, ) w.Header().Set("Cache-Control", "public, max-age=3600") http.ServeFile(w, r, resolved) } } func resolveStoragePath(storagePath string) (string, []string) { raw := strings.TrimSpace(storagePath) if raw == "" { return "", nil } // URL/query temizligi ve platforma uygun normalize if i := strings.Index(raw, "?"); i >= 0 { raw = raw[:i] } raw = strings.ReplaceAll(raw, "\\", "/") if scheme := strings.Index(raw, "://"); scheme >= 0 { rest := raw[scheme+3:] if i := strings.Index(rest, "/"); i >= 0 { raw = rest[i:] } } raw = strings.TrimPrefix(raw, "./") raw = strings.TrimPrefix(raw, "/") raw = strings.TrimPrefix(raw, "uploads/") raw = filepath.ToSlash(filepath.Clean(raw)) relUploads := filepath.FromSlash(filepath.Join("uploads", raw)) rawT300 := raw relUploadsT300 := relUploads if strings.Contains(filepath.ToSlash(relUploads), "uploads/image/") && !strings.Contains(filepath.ToSlash(relUploads), "uploads/image/t300/") { rawT300 = strings.Replace(filepath.ToSlash(raw), "image/", "image/t300/", 1) relUploadsT300 = filepath.FromSlash( strings.Replace(filepath.ToSlash(relUploads), "uploads/image/", "uploads/image/t300/", 1), ) } candidates := []string{ filepath.Clean(storagePath), filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))), filepath.FromSlash(filepath.Clean(raw)), relUploads, relUploadsT300, filepath.Join(".", relUploads), filepath.Join(".", relUploadsT300), filepath.Join("..", relUploads), filepath.Join("..", relUploadsT300), filepath.Join("..", "..", relUploads), filepath.Join("..", "..", relUploadsT300), } if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" { candidates = append(candidates, filepath.Join(root, raw), filepath.Join(root, filepath.FromSlash(rawT300)), filepath.Join(root, relUploads), filepath.Join(root, relUploadsT300), filepath.Join(root, "uploads", raw), filepath.Join(root, "uploads", filepath.FromSlash(rawT300)), ) } for _, p := range candidates { if p == "" { continue } if st, err := os.Stat(p); err == nil && !st.IsDir() { return p, candidates } } return "", candidates }