Files
bssapp/svc/routes/product_images.go
2026-03-09 13:19:26 +03:00

302 lines
5.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"`
}
//
// LIST PRODUCT IMAGES
//
// 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
}
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)
AND (
$2 = ''
OR b.file_name ILIKE '%' || '-' || $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 {
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()
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
}
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,
"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
}