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"` } func tokenizeImageFileName(fileName string) []string { up := strings.ToUpper(strings.TrimSpace(fileName)) if up == "" { return nil } return strings.FieldsFunc(up, func(r rune) bool { isUpper := r >= 'A' && r <= 'Z' isDigit := r >= '0' && r <= '9' if isUpper || isDigit || r == '_' { return false } return true }) } func imageFileMatches(fileName, dim1, dim3 string) bool { dim1 = strings.ToUpper(strings.TrimSpace(dim1)) dim3 = strings.ToUpper(strings.TrimSpace(dim3)) if dim1 == "" && dim3 == "" { return true } tokens := tokenizeImageFileName(fileName) if len(tokens) == 0 { return false } hasToken := func(target string) bool { if target == "" { return true } for _, t := range tokens { if t == target { return true } } return false } return hasToken(dim1) && hasToken(dim3) } // // LIST PRODUCT IMAGES // // GET /api/product-images?code=...&dim1=...&dim3=... 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")) dim1 := strings.TrimSpace(r.URL.Query().Get("dim1")) if dim1 == "" { dim1 = strings.TrimSpace(r.URL.Query().Get("color")) } dim3 := strings.TrimSpace(r.URL.Query().Get("dim3")) if dim3 == "" { dim3 = strings.TrimSpace(r.URL.Query().Get("yaka")) } if dim3 == "" { dim3 = strings.TrimSpace(r.URL.Query().Get("renk2")) } 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, UPPER(COALESCE(b.dimval1::text,'')) AS dimval1, UPPER(COALESCE(b.dimval2::text,'')) AS dimval2, UPPER(COALESCE(b.dimval3::text,'')) AS dimval3 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) OR UPPER(i.code) = UPPER('S001-' || $1) OR UPPER(i.code) LIKE '%-' || UPPER($1) ) ORDER BY COALESCE(b.sort_order,999999), b.zlins_dttm DESC, b.id DESC ` rows, err := pg.Query(query, code) if err != nil { slog.Error("product_images.list.query_failed", "req_id", reqID, "code", code, "dim1", dim1, "dim3", dim3, "err", err.Error(), ) http.Error(w, "Gorsel sorgu hatasi: "+err.Error(), http.StatusInternalServerError) return } defer rows.Close() items := make([]ProductImageItem, 0, 16) matchedByDim := make([]ProductImageItem, 0, 16) matchedByName := make([]ProductImageItem, 0, 16) matchedByNameDim1Only := make([]ProductImageItem, 0, 16) for rows.Next() { var it ProductImageItem var rowDim1, rowDim2, rowDim3 string if err := rows.Scan( &it.ID, &it.FileName, &it.FileSize, &it.Storage, &rowDim1, &rowDim2, &rowDim3, ); err != nil { continue } it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID) items = append(items, it) dimMatched := true if dim1 != "" { // Bazı eski kayıtlarda dimval1 gerçek renk kodu yerine numeric id tutulmuş olabilir. // Bu yüzden dimval karşılaştırması yardımcı; asıl fallback file_name token eşleşmesidir. dimMatched = dimMatched && (rowDim1 == strings.ToUpper(dim1)) } if dim3 != "" { dimMatched = dimMatched && (rowDim3 == strings.ToUpper(dim3) || rowDim2 == strings.ToUpper(dim3)) } if dimMatched { matchedByDim = append(matchedByDim, it) } if imageFileMatches(it.FileName, dim1, dim3) { matchedByName = append(matchedByName, it) } if dim1 != "" && imageFileMatches(it.FileName, dim1, "") { matchedByNameDim1Only = append(matchedByNameDim1Only, it) } } if dim1 != "" || dim3 != "" { if len(matchedByDim) > 0 { items = matchedByDim } else if len(matchedByName) > 0 { items = matchedByName } else if dim3 == "" && len(matchedByNameDim1Only) > 0 { // Sadece 1. renk filtreleniyorsa dim1-only fallback kabul. items = matchedByNameDim1Only } else { // dim3 (2. renk) verildiyse yanlis varyanta dusmemek icin bos don. items = []ProductImageItem{} } } slog.Info("product_images.list.ok", "req_id", reqID, "code", code, "dim1", dim1, "dim3", dim3, "count", len(items), ) w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(items) } } // // GET IMAGE CONTENT // // 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 } // DB içinde binary saklıysa if storedInDB && len(binData) > 0 { 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, "file_name", fileName, "storage_path", storagePath, "tried", tried, ) http.NotFound(w, r) return } w.Header().Set("Cache-Control", "public, max-age=3600") http.ServeFile(w, r, resolved) } } // // FILE PATH RESOLVER // func resolveStoragePath(storagePath string) (string, []string) { raw := strings.TrimSpace(storagePath) if raw == "" { return "", nil } 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)) candidates := []string{ filepath.Clean(storagePath), filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))), filepath.FromSlash(filepath.Clean(raw)), relUploads, filepath.Join(".", relUploads), filepath.Join("..", relUploads), filepath.Join("..", "..", relUploads), } if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" { candidates = append(candidates, filepath.Join(root, raw), filepath.Join(root, relUploads), filepath.Join(root, "uploads", raw), ) } for _, p := range candidates { if p == "" { continue } if st, err := os.Stat(p); err == nil && !st.IsDir() { return p, candidates } } return "", candidates }