398 lines
8.4 KiB
Go
398 lines
8.4 KiB
Go
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
|
||
}
|