mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
dev day #99
This commit is contained in:
@@ -1,296 +1,354 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
"mime/multipart"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fotbal-club/internal/config"
|
||||
"fotbal-club/internal/models"
|
||||
"fotbal-club/internal/config"
|
||||
"fotbal-club/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func uploadsBaseDir() string {
|
||||
dir := config.AppConfig.UploadDir
|
||||
if strings.TrimSpace(dir) == "" {
|
||||
dir = "./uploads"
|
||||
}
|
||||
return dir
|
||||
dir := config.AppConfig.UploadDir
|
||||
if strings.TrimSpace(dir) == "" {
|
||||
dir = "./uploads"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// sanitizeAndWriteLogo trims white/transparent borders and resizes to fixed height (64px), then writes PNG to outPath.
|
||||
func sanitizeAndWriteLogo(data []byte, outPath string) error {
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := img.Bounds()
|
||||
minX, minY := b.Max.X, b.Max.Y
|
||||
maxX, maxY := b.Min.X, b.Min.Y
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
r, g, bl, a := img.At(x, y).RGBA()
|
||||
if a <= 0x10 { // near transparent
|
||||
continue
|
||||
}
|
||||
rr, gg, bb := uint8(r>>8), uint8(g>>8), uint8(bl>>8)
|
||||
if rr > 245 && gg > 245 && bb > 245 { // nearly white background
|
||||
continue
|
||||
}
|
||||
if x < minX { minX = x }
|
||||
if y < minY { minY = y }
|
||||
if x > maxX { maxX = x }
|
||||
if y > maxY { maxY = y }
|
||||
}
|
||||
}
|
||||
if minX >= maxX || minY >= maxY {
|
||||
// fallback to full image
|
||||
minX, minY = b.Min.X, b.Min.Y
|
||||
maxX, maxY = b.Max.X-1, b.Max.Y-1
|
||||
}
|
||||
cw, ch := maxX-minX+1, maxY-minY+1
|
||||
nrgba := image.NewNRGBA(image.Rect(0, 0, cw, ch))
|
||||
for y := 0; y < ch; y++ {
|
||||
for x := 0; x < cw; x++ {
|
||||
nrgba.Set(x, y, img.At(minX+x, minY+y))
|
||||
}
|
||||
}
|
||||
// resize to 64px height using nearest-neighbor
|
||||
targetH := 64
|
||||
if ch != targetH {
|
||||
targetW := int(float64(cw) * float64(targetH) / float64(ch))
|
||||
if targetW < 1 { targetW = 1 }
|
||||
resized := image.NewNRGBA(image.Rect(0, 0, targetW, targetH))
|
||||
for y2 := 0; y2 < targetH; y2++ {
|
||||
srcY := y2 * ch / targetH
|
||||
for x2 := 0; x2 < targetW; x2++ {
|
||||
srcX := x2 * cw / targetW
|
||||
c := nrgba.NRGBAAt(srcX, srcY)
|
||||
resized.SetNRGBA(x2, y2, c)
|
||||
}
|
||||
}
|
||||
nrgba = resized
|
||||
}
|
||||
// write PNG
|
||||
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil { return err }
|
||||
f, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return png.Encode(f, nrgba)
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := img.Bounds()
|
||||
minX, minY := b.Max.X, b.Max.Y
|
||||
maxX, maxY := b.Min.X, b.Min.Y
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
r, g, bl, a := img.At(x, y).RGBA()
|
||||
if a <= 0x10 { // near transparent
|
||||
continue
|
||||
}
|
||||
rr, gg, bb := uint8(r>>8), uint8(g>>8), uint8(bl>>8)
|
||||
if rr > 245 && gg > 245 && bb > 245 { // nearly white background
|
||||
continue
|
||||
}
|
||||
if x < minX {
|
||||
minX = x
|
||||
}
|
||||
if y < minY {
|
||||
minY = y
|
||||
}
|
||||
if x > maxX {
|
||||
maxX = x
|
||||
}
|
||||
if y > maxY {
|
||||
maxY = y
|
||||
}
|
||||
}
|
||||
}
|
||||
if minX >= maxX || minY >= maxY {
|
||||
// fallback to full image
|
||||
minX, minY = b.Min.X, b.Min.Y
|
||||
maxX, maxY = b.Max.X-1, b.Max.Y-1
|
||||
}
|
||||
cw, ch := maxX-minX+1, maxY-minY+1
|
||||
nrgba := image.NewNRGBA(image.Rect(0, 0, cw, ch))
|
||||
for y := 0; y < ch; y++ {
|
||||
for x := 0; x < cw; x++ {
|
||||
nrgba.Set(x, y, img.At(minX+x, minY+y))
|
||||
}
|
||||
}
|
||||
// resize to 64px height using nearest-neighbor
|
||||
targetH := 64
|
||||
if ch != targetH {
|
||||
targetW := int(float64(cw) * float64(targetH) / float64(ch))
|
||||
if targetW < 1 {
|
||||
targetW = 1
|
||||
}
|
||||
resized := image.NewNRGBA(image.Rect(0, 0, targetW, targetH))
|
||||
for y2 := 0; y2 < targetH; y2++ {
|
||||
srcY := y2 * ch / targetH
|
||||
for x2 := 0; x2 < targetW; x2++ {
|
||||
srcX := x2 * cw / targetW
|
||||
c := nrgba.NRGBAAt(srcX, srcY)
|
||||
resized.SetNRGBA(x2, y2, c)
|
||||
}
|
||||
}
|
||||
nrgba = resized
|
||||
}
|
||||
// write PNG
|
||||
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return png.Encode(f, nrgba)
|
||||
}
|
||||
|
||||
// ensureUniqueFilename ensures name does not collide within dir, adding -1, -2 etc.
|
||||
func ensureUniqueFilename(dir, name string) string {
|
||||
base := name
|
||||
ext := ""
|
||||
if i := strings.LastIndex(name, "."); i >= 0 {
|
||||
base = name[:i]
|
||||
ext = name[i:]
|
||||
}
|
||||
try := name
|
||||
idx := 1
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, try)); os.IsNotExist(err) {
|
||||
return try
|
||||
}
|
||||
try = fmt.Sprintf("%s-%d%s", base, idx, ext)
|
||||
idx++
|
||||
}
|
||||
base := name
|
||||
ext := ""
|
||||
if i := strings.LastIndex(name, "."); i >= 0 {
|
||||
base = name[:i]
|
||||
ext = name[i:]
|
||||
}
|
||||
try := name
|
||||
idx := 1
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, try)); os.IsNotExist(err) {
|
||||
return try
|
||||
}
|
||||
try = fmt.Sprintf("%s-%d%s", base, idx, ext)
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
// ListSponsors returns list of sponsor logo URLs under /uploads/sponsors
|
||||
func (c *ScoreboardController) ListSponsors(ctx *gin.Context) {
|
||||
sponsorDir := filepath.Join(uploadsBaseDir(), "sponsors")
|
||||
entries, err := os.ReadDir(sponsorDir)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusOK, []string{})
|
||||
return
|
||||
}
|
||||
out := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.IsDir() { continue }
|
||||
name := e.Name()
|
||||
lower := strings.ToLower(name)
|
||||
if strings.HasSuffix(lower, ".png") || strings.HasSuffix(lower, ".jpg") || strings.HasSuffix(lower, ".jpeg") || strings.HasSuffix(lower, ".gif") || strings.HasSuffix(lower, ".webp") || strings.HasSuffix(lower, ".svg") {
|
||||
out = append(out, "/uploads/sponsors/"+name)
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, out)
|
||||
sponsorDir := filepath.Join(uploadsBaseDir(), "sponsors")
|
||||
entries, err := os.ReadDir(sponsorDir)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusOK, []string{})
|
||||
return
|
||||
}
|
||||
out := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
lower := strings.ToLower(name)
|
||||
if strings.HasSuffix(lower, ".png") || strings.HasSuffix(lower, ".jpg") || strings.HasSuffix(lower, ".jpeg") || strings.HasSuffix(lower, ".gif") || strings.HasSuffix(lower, ".webp") || strings.HasSuffix(lower, ".svg") {
|
||||
out = append(out, "/uploads/sponsors/"+name)
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// UploadSponsors accepts multipart form files under field name "files" (or single "file")
|
||||
func (c *ScoreboardController) UploadSponsors(ctx *gin.Context) {
|
||||
if err := ctx.Request.ParseMultipartForm(200 << 20); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid upload"})
|
||||
return
|
||||
}
|
||||
sponsorDir := filepath.Join(uploadsBaseDir(), "sponsors")
|
||||
_ = os.MkdirAll(sponsorDir, 0o755)
|
||||
if err := ctx.Request.ParseMultipartForm(200 << 20); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid upload"})
|
||||
return
|
||||
}
|
||||
sponsorDir := filepath.Join(uploadsBaseDir(), "sponsors")
|
||||
_ = os.MkdirAll(sponsorDir, 0o755)
|
||||
|
||||
saved := 0
|
||||
created := make([]string, 0, 8)
|
||||
if ctx.Request.MultipartForm != nil {
|
||||
files := ctx.Request.MultipartForm.File["files"]
|
||||
if len(files) == 0 {
|
||||
if f, hdr, err := ctx.Request.FormFile("file"); err == nil {
|
||||
_ = f.Close()
|
||||
files = []*multipart.FileHeader{hdr}
|
||||
}
|
||||
}
|
||||
for _, hdr := range files {
|
||||
if hdr == nil { continue }
|
||||
src, err := hdr.Open()
|
||||
if err != nil { continue }
|
||||
// do not defer: loop
|
||||
name := sanitizeFilename(hdr.Filename)
|
||||
if name == "" { name = fmt.Sprintf("sponsor-%d", time.Now().UnixNano()) }
|
||||
base := name
|
||||
if i := strings.LastIndex(name, "."); i >= 0 { base = name[:i] }
|
||||
outName := ensureUniqueFilename(sponsorDir, base+".png")
|
||||
outPath := filepath.Join(sponsorDir, outName)
|
||||
saved := 0
|
||||
created := make([]string, 0, 8)
|
||||
if ctx.Request.MultipartForm != nil {
|
||||
files := ctx.Request.MultipartForm.File["files"]
|
||||
if len(files) == 0 {
|
||||
if f, hdr, err := ctx.Request.FormFile("file"); err == nil {
|
||||
_ = f.Close()
|
||||
files = []*multipart.FileHeader{hdr}
|
||||
}
|
||||
}
|
||||
for _, hdr := range files {
|
||||
if hdr == nil {
|
||||
continue
|
||||
}
|
||||
src, err := hdr.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// do not defer: loop
|
||||
name := sanitizeFilename(hdr.Filename)
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("sponsor-%d", time.Now().UnixNano())
|
||||
}
|
||||
base := name
|
||||
if i := strings.LastIndex(name, "."); i >= 0 {
|
||||
base = name[:i]
|
||||
}
|
||||
outName := ensureUniqueFilename(sponsorDir, base+".png")
|
||||
outPath := filepath.Join(sponsorDir, outName)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, src); err == nil {
|
||||
if err := sanitizeAndWriteLogo(buf.Bytes(), outPath); err == nil {
|
||||
saved++
|
||||
created = append(created, "/uploads/sponsors/"+outName)
|
||||
} else {
|
||||
// Fallback: write original bytes with original extension
|
||||
rawName := ensureUniqueFilename(sponsorDir, name)
|
||||
rawPath := filepath.Join(sponsorDir, rawName)
|
||||
_ = os.WriteFile(rawPath, buf.Bytes(), 0o644)
|
||||
saved++
|
||||
created = append(created, "/uploads/sponsors/"+rawName)
|
||||
}
|
||||
}
|
||||
_ = src.Close()
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"saved": saved, "files": created})
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, src); err == nil {
|
||||
if err := sanitizeAndWriteLogo(buf.Bytes(), outPath); err == nil {
|
||||
saved++
|
||||
created = append(created, "/uploads/sponsors/"+outName)
|
||||
} else {
|
||||
// Fallback: write original bytes with original extension
|
||||
rawName := ensureUniqueFilename(sponsorDir, name)
|
||||
rawPath := filepath.Join(sponsorDir, rawName)
|
||||
_ = os.WriteFile(rawPath, buf.Bytes(), 0o644)
|
||||
saved++
|
||||
created = append(created, "/uploads/sponsors/"+rawName)
|
||||
}
|
||||
}
|
||||
_ = src.Close()
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"saved": saved, "files": created})
|
||||
}
|
||||
|
||||
// DeleteSponsor deletes a sponsor logo by filename (?name=)
|
||||
func (c *ScoreboardController) DeleteSponsor(ctx *gin.Context) {
|
||||
name := sanitizeFilename(ctx.Query("name"))
|
||||
if name == "" {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "missing name"})
|
||||
return
|
||||
}
|
||||
p := filepath.Join(uploadsBaseDir(), "sponsors", name)
|
||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
if err := os.Remove(p); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot delete"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
name := sanitizeFilename(ctx.Query("name"))
|
||||
if name == "" {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "missing name"})
|
||||
return
|
||||
}
|
||||
p := filepath.Join(uploadsBaseDir(), "sponsors", name)
|
||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
if err := os.Remove(p); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot delete"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// DeleteQR deletes the QR image (uploads/qr.png) if present
|
||||
func (c *ScoreboardController) DeleteQR(ctx *gin.Context) {
|
||||
path := filepath.Join(uploadsBaseDir(), "qr.png")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot delete"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// GetQR returns the current QR image URL if present
|
||||
func (c *ScoreboardController) GetQR(ctx *gin.Context) {
|
||||
path := filepath.Join(uploadsBaseDir(), "qr.png")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"qr": "/uploads/qr.png"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"qr": ""})
|
||||
path := filepath.Join(uploadsBaseDir(), "qr.png")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"qr": "/uploads/qr.png"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"qr": ""})
|
||||
}
|
||||
|
||||
// UploadQR accepts a single file and stores/overwrites uploads/qr.png
|
||||
func (c *ScoreboardController) UploadQR(ctx *gin.Context) {
|
||||
file, _, err := ctx.Request.FormFile("file")
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "file not provided (field 'file')"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
dir := uploadsBaseDir()
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
out, err := os.Create(filepath.Join(dir, "qr.png"))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot save"})
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
if _, err := io.Copy(out, file); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "write failed"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
file, _, err := ctx.Request.FormFile("file")
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "file not provided (field 'file')"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
dir := uploadsBaseDir()
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
out, err := os.Create(filepath.Join(dir, "qr.png"))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot save"})
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
if _, err := io.Copy(out, file); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "write failed"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// PrefillSponsorsFromPage copies logo images from existing Sponsors into uploads/sponsors for overlay use.
|
||||
// Optional JSON body: { "ids": [1,2,3] } to limit to specific sponsors.
|
||||
func (c *ScoreboardController) PrefillSponsorsFromPage(ctx *gin.Context) {
|
||||
var body struct{ IDs []uint `json:"ids"` }
|
||||
_ = ctx.ShouldBindJSON(&body)
|
||||
var list []models.Sponsor
|
||||
q := c.DB.Model(&models.Sponsor{})
|
||||
if len(body.IDs) > 0 {
|
||||
q = q.Where("id IN ?", body.IDs)
|
||||
} else {
|
||||
q = q.Where("is_active = ?", true)
|
||||
}
|
||||
if err := q.Find(&list).Error; err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "db error"})
|
||||
return
|
||||
}
|
||||
sponsorDir := filepath.Join(uploadsBaseDir(), "sponsors")
|
||||
_ = os.MkdirAll(sponsorDir, 0o755)
|
||||
created := make([]string, 0, len(list))
|
||||
for _, s := range list {
|
||||
logo := strings.TrimSpace(s.LogoURL)
|
||||
if logo == "" { continue }
|
||||
var data []byte
|
||||
if strings.HasPrefix(logo, "/uploads/") {
|
||||
p := filepath.Join(config.AppConfig.UploadDir, strings.TrimPrefix(logo, "/uploads/"))
|
||||
if b, err := os.ReadFile(p); err == nil { data = b } else { continue }
|
||||
} else if strings.HasPrefix(strings.ToLower(logo), "http://") || strings.HasPrefix(strings.ToLower(logo), "https://") {
|
||||
resp, err := http.Get(logo)
|
||||
if err != nil { continue }
|
||||
func() {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return }
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if len(b) > 0 { data = b }
|
||||
}()
|
||||
if len(data) == 0 { continue }
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
base := sanitizeFilename(s.Name)
|
||||
if base == "" {
|
||||
seg := logo
|
||||
if i := strings.LastIndex(seg, "/"); i >= 0 { seg = seg[i+1:] }
|
||||
if j := strings.LastIndex(seg, "."); j >= 0 { seg = seg[:j] }
|
||||
base = sanitizeFilename(seg)
|
||||
if base == "" { base = fmt.Sprintf("sponsor-%d", time.Now().UnixNano()) }
|
||||
}
|
||||
outName := ensureUniqueFilename(sponsorDir, base+".png")
|
||||
outPath := filepath.Join(sponsorDir, outName)
|
||||
if err := sanitizeAndWriteLogo(data, outPath); err != nil {
|
||||
// fallback to raw write
|
||||
rawName := ensureUniqueFilename(sponsorDir, base+".png")
|
||||
_ = os.WriteFile(filepath.Join(sponsorDir, rawName), data, 0o644)
|
||||
created = append(created, "/uploads/sponsors/"+rawName)
|
||||
} else {
|
||||
created = append(created, "/uploads/sponsors/"+outName)
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"saved": len(created), "files": created})
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
_ = ctx.ShouldBindJSON(&body)
|
||||
var list []models.Sponsor
|
||||
q := c.DB.Model(&models.Sponsor{})
|
||||
if len(body.IDs) > 0 {
|
||||
q = q.Where("id IN ?", body.IDs)
|
||||
} else {
|
||||
q = q.Where("is_active = ?", true)
|
||||
}
|
||||
if err := q.Find(&list).Error; err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "db error"})
|
||||
return
|
||||
}
|
||||
sponsorDir := filepath.Join(uploadsBaseDir(), "sponsors")
|
||||
_ = os.MkdirAll(sponsorDir, 0o755)
|
||||
created := make([]string, 0, len(list))
|
||||
for _, s := range list {
|
||||
logo := strings.TrimSpace(s.LogoURL)
|
||||
if logo == "" {
|
||||
continue
|
||||
}
|
||||
var data []byte
|
||||
if strings.HasPrefix(logo, "/uploads/") {
|
||||
p := filepath.Join(config.AppConfig.UploadDir, strings.TrimPrefix(logo, "/uploads/"))
|
||||
if b, err := os.ReadFile(p); err == nil {
|
||||
data = b
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if strings.HasPrefix(strings.ToLower(logo), "http://") || strings.HasPrefix(strings.ToLower(logo), "https://") {
|
||||
resp, err := http.Get(logo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
func() {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return
|
||||
}
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if len(b) > 0 {
|
||||
data = b
|
||||
}
|
||||
}()
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
base := sanitizeFilename(s.Name)
|
||||
if base == "" {
|
||||
seg := logo
|
||||
if i := strings.LastIndex(seg, "/"); i >= 0 {
|
||||
seg = seg[i+1:]
|
||||
}
|
||||
if j := strings.LastIndex(seg, "."); j >= 0 {
|
||||
seg = seg[:j]
|
||||
}
|
||||
base = sanitizeFilename(seg)
|
||||
if base == "" {
|
||||
base = fmt.Sprintf("sponsor-%d", time.Now().UnixNano())
|
||||
}
|
||||
}
|
||||
outName := ensureUniqueFilename(sponsorDir, base+".png")
|
||||
outPath := filepath.Join(sponsorDir, outName)
|
||||
if err := sanitizeAndWriteLogo(data, outPath); err != nil {
|
||||
// fallback to raw write
|
||||
rawName := ensureUniqueFilename(sponsorDir, base+".png")
|
||||
_ = os.WriteFile(filepath.Join(sponsorDir, rawName), data, 0o644)
|
||||
created = append(created, "/uploads/sponsors/"+rawName)
|
||||
} else {
|
||||
created = append(created, "/uploads/sponsors/"+outName)
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"saved": len(created), "files": created})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user