Files
bssapp/svc/routes/product_images.go
2026-03-11 17:53:47 +03:00

348 lines
6.8 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, 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
}