fdf0ddd498
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
96 lines
3.5 KiB
Go
96 lines
3.5 KiB
Go
package lkgateway
|
|
|
|
// branding.go — настройка логотипа организации в шапке отчётов (загрузка своего
|
|
// изображения через UI). Хранится в настройках (Branding.LogoDataURI) как data-URI;
|
|
// пусто → встроенный логотип по умолчанию. См. orgLogoIMG в fontvielle_logo.go.
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
const maxLogoBytes = 512 << 10 // 512 КБ — логотип встраивается в каждый отчёт
|
|
|
|
// saveLogo принимает загруженное изображение и сохраняет его как логотип отчётов.
|
|
func (h *setupHandlers) saveLogo(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if err := r.ParseMultipartForm(maxLogoBytes + (64 << 10)); err != nil {
|
|
setupFlash(w, r, "Логотип: не удалось прочитать форму: "+err.Error())
|
|
return
|
|
}
|
|
file, header, err := r.FormFile("logo")
|
|
if err != nil {
|
|
setupFlash(w, r, "Логотип: файл не выбран")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
data, err := io.ReadAll(io.LimitReader(file, maxLogoBytes+1))
|
|
if err != nil {
|
|
setupFlash(w, r, "Логотип: ошибка чтения файла: "+err.Error())
|
|
return
|
|
}
|
|
if len(data) == 0 {
|
|
setupFlash(w, r, "Логотип: файл пустой")
|
|
return
|
|
}
|
|
if len(data) > maxLogoBytes {
|
|
setupFlash(w, r, "Логотип: файл слишком большой (максимум 512 КБ)")
|
|
return
|
|
}
|
|
|
|
mime := logoMIME(header.Filename, data)
|
|
if mime == "" {
|
|
setupFlash(w, r, "Логотип: нужен файл-изображение (PNG, JPG, GIF, WEBP или SVG)")
|
|
return
|
|
}
|
|
dataURI := "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(data)
|
|
if err := h.rc.UpdateBranding(BrandingSettings{LogoDataURI: dataURI, LogoName: header.Filename}); err != nil {
|
|
setupFlash(w, r, "Логотип: ошибка сохранения: "+err.Error())
|
|
return
|
|
}
|
|
setupFlash(w, r, "Логотип обновлён: "+header.Filename+" — появится в шапке отчётов справа.")
|
|
}
|
|
|
|
// resetLogo сбрасывает логотип на встроенный по умолчанию.
|
|
func (h *setupHandlers) resetLogo(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if err := h.rc.UpdateBranding(BrandingSettings{}); err != nil {
|
|
setupFlash(w, r, "Логотип: ошибка сброса: "+err.Error())
|
|
return
|
|
}
|
|
if builtinLogoAllowed() {
|
|
setupFlash(w, r, "Логотип сброшен на стандартный (встроенный Фонтвьель).")
|
|
return
|
|
}
|
|
setupFlash(w, r, "Логотип сброшен: в отчётах логотипа нет, пока не загрузите свой.")
|
|
}
|
|
|
|
// logoMIME определяет MIME изображения по расширению и содержимому; "" — не картинка.
|
|
func logoMIME(filename string, data []byte) string {
|
|
switch {
|
|
case strings.HasSuffix(strings.ToLower(filename), ".svg"):
|
|
return "image/svg+xml"
|
|
}
|
|
ct := http.DetectContentType(data)
|
|
head := data
|
|
if len(head) > 200 {
|
|
head = head[:200]
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(ct, "image/"):
|
|
return ct
|
|
case strings.Contains(ct, "xml") && strings.Contains(strings.ToLower(string(head)), "<svg"):
|
|
return "image/svg+xml"
|
|
}
|
|
return ""
|
|
}
|