Files
bssapp/svc/routes/product_images.go
2026-03-13 12:30:04 +03:00

398 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,'')) AS dimval1,
UPPER(COALESCE(b.dimval2,'')) AS dimval2,
UPPER(COALESCE(b.dimval3,'')) 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, dim1, dim3)
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 len(matchedByNameDim1Only) > 0 {
// dim3 eski/uyumsuz kayitlarda tutulmuyorsa en azindan 1.renk ile daralt.
items = matchedByNameDim1Only
} else {
// Filtre verildi ama eslesme yoksa tum listeyi donmeyelim.
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
}