Files
2026-07-10 00:56:03 +03:30

100 lines
4.1 KiB
Go

// Package static سرویس‌دهیِ تصاویرِ کارت (هر اسکین یک پوشه) را انجام می‌دهد.
// دو منبع: ۱) پوشه‌ی دیسکیِ ماندگار (diskDir) برای اسکین‌هایی که از پنل ادمین
// آپلود می‌شوند، ۲) فایل‌سیستمِ embed‌شده برای اسکین‌های پیش‌فرض (simple/epic).
// ابتدا دیسک بررسی می‌شود، سپس embed؛ پس آپلودها بدونِ کامپایلِ دوباره سرو می‌شوند.
// مسیر: GET /cards/<skin>/<code>.png و /cards/<skin>/back.jpg
package static
import (
"embed"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
)
//go:embed cards
var cardsFS embed.FS
//go:embed carpets
var carpetsFS embed.FS
// CardsHandler یک http.Handler برای سرویس‌دهیِ فایل‌های کارت زیر /cards/ است.
// diskDir پوشه‌ی اسکین‌های آپلودی است (می‌تواند خالی/ناموجود باشد).
// CORS باز است تا نسخهٔ وب (Flutter web / canvaskit) هم بتواند تصاویر را بکشد.
func CardsHandler(diskDir string) http.Handler {
sub, err := fs.Sub(cardsFS, "cards")
if err != nil {
panic(err)
}
embedded := http.StripPrefix("/cards/", http.FileServer(http.FS(sub)))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Cache-Control", "public, max-age=604800") // یک هفته کش
// تلاشِ اول: پوشه‌ی دیسکی (اسکین‌های آپلودشده از پنل ادمین).
if diskDir != "" {
rel := strings.TrimPrefix(r.URL.Path, "/cards/")
rel = filepath.Clean("/" + rel) // جلوگیری از path traversal
full := filepath.Join(diskDir, rel)
if st, err := os.Stat(full); err == nil && !st.IsDir() {
http.ServeFile(w, r, full)
return
}
}
// تلاشِ دوم: اسکین‌های embed‌شده (simple/epic).
embedded.ServeHTTP(w, r)
})
}
// AvatarsHandler تصاویرِ شخصیتِ آواتار را سرو می‌کند: GET /avatars/<id>.png
// فقط از پوشه‌ی دیسکیِ آپلودِ ادمین (بدونِ embed)؛ اگر نبود ۴۰۴ (اپ به آواتارِ
// تصادفیِ پیش‌فرض برمی‌گردد).
func AvatarsHandler(diskDir string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Cache-Control", "public, max-age=604800")
if diskDir != "" {
rel := filepath.Clean("/" + strings.TrimPrefix(r.URL.Path, "/avatars/"))
full := filepath.Join(diskDir, rel)
if st, err := os.Stat(full); err == nil && !st.IsDir() {
// SVG را با content-type درست بده تا مرورگر/پنل هم آن را نمایش دهد.
if strings.HasSuffix(strings.ToLower(full), ".svg") {
w.Header().Set("Content-Type", "image/svg+xml")
}
http.ServeFile(w, r, full)
return
}
}
http.NotFound(w, r)
})
}
// CarpetsHandler تصاویرِ فرش را سرو می‌کند: GET /carpets/<id>.jpg
// ابتدا از پوشه‌ی دیسکی (آپلودِ ادمین)، سپس از فرش‌های embed‌شده‌ی پیش‌فرض.
// اگر هیچ‌کدام نبود ۴۰۴ (اپ به میزِ ترسیمیِ classic برمی‌گردد).
func CarpetsHandler(diskDir string) http.Handler {
sub, err := fs.Sub(carpetsFS, "carpets")
if err != nil {
panic(err)
}
embedded := http.StripPrefix("/carpets/", http.FileServer(http.FS(sub)))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Cache-Control", "public, max-age=604800")
// تلاشِ اول: پوشه‌ی دیسکی (آپلودِ ادمین).
if diskDir != "" {
rel := filepath.Clean("/" + strings.TrimPrefix(r.URL.Path, "/carpets/"))
full := filepath.Join(diskDir, rel)
if st, err := os.Stat(full); err == nil && !st.IsDir() {
http.ServeFile(w, r, full)
return
}
}
// تلاشِ دوم: فرش‌های embed‌شده.
embedded.ServeHTTP(w, r)
})
}