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, color, secondColor string) bool { color = strings.ToUpper(strings.TrimSpace(color)) secondColor = strings.ToUpper(strings.TrimSpace(secondColor)) if color == "" && secondColor == "" { 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(color) && hasToken(secondColor) } // // LIST PRODUCT IMAGES // // GET /api/product-images?code=...&color=...&yaka=... 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")) secondColor := strings.TrimSpace(r.URL.Query().Get("yaka")) if secondColor == "" { secondColor = 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 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) 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, "color", color, "second_color", secondColor, "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 } if !imageFileMatches(it.FileName, color, secondColor) { 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, "second_color", secondColor, "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 }