Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-04 17:59:14 +03:00
parent b1150c5ef7
commit 94244b194a
7 changed files with 506 additions and 46 deletions

View File

@@ -4,12 +4,14 @@ import (
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/gorilla/mux"
)
@@ -24,10 +26,22 @@ type ProductImageItem struct {
// 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
}
@@ -43,14 +57,20 @@ JOIN mmitem i
ON i.id = b.src_id
WHERE b.typ = 'img'
AND b.src_table = 'mmitem'
AND i.code = $1
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 {
http.Error(w, "Görsel sorgu hatası: "+err.Error(), http.StatusInternalServerError)
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()
@@ -65,6 +85,13 @@ ORDER BY COALESCE(b.sort_order, 999999), b.zlins_dttm DESC, b.id DESC
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)
}
@@ -73,10 +100,22 @@ ORDER BY COALESCE(b.sort_order, 999999), b.zlins_dttm DESC, b.id DESC
// 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 {
http.Error(w, "Geçersiz görsel id", http.StatusBadRequest)
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
}
@@ -99,47 +138,118 @@ WHERE id = $1
`, 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
}
http.Error(w, "Görsel okunamadı: "+err.Error(), http.StatusInternalServerError)
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 := resolveStoragePath(storagePath)
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 {
func resolveStoragePath(storagePath string) (string, []string) {
raw := strings.TrimSpace(storagePath)
if raw == "" {
return ""
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.Clean(raw),
filepath.Join(".", raw),
filepath.Join("..", raw),
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))
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 {
@@ -147,9 +257,9 @@ func resolveStoragePath(storagePath string) string {
continue
}
if st, err := os.Stat(p); err == nil && !st.IsDir() {
return p
return p, candidates
}
}
return ""
return "", candidates
}