765 lines
21 KiB
Go
765 lines
21 KiB
Go
package web
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"html/template"
|
||
"io/fs"
|
||
"net/http"
|
||
"net/url"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"3588AdminBackend/internal/models"
|
||
"3588AdminBackend/internal/service"
|
||
"github.com/go-chi/chi/v5"
|
||
)
|
||
|
||
type UI struct {
|
||
discovery *service.DiscoveryService
|
||
registry *service.RegistryService
|
||
agent *service.AgentClient
|
||
tasks *service.TaskService
|
||
templates *service.TemplateService
|
||
|
||
tpl *template.Template
|
||
}
|
||
|
||
type PageData struct {
|
||
Title string
|
||
ContentHTML template.HTML
|
||
Message string
|
||
Error string
|
||
|
||
DeviceCount int
|
||
OnlineCount int
|
||
OfflineCount int
|
||
FoundCount int
|
||
|
||
Devices []*models.Device
|
||
Found []*models.Device
|
||
Device *models.Device
|
||
Tasks []models.Task
|
||
Task *models.Task
|
||
Templates []service.Template
|
||
Template *service.Template
|
||
|
||
RawJSON string
|
||
RawText string
|
||
SchemaJSON string
|
||
StateJSON string
|
||
FaceGalleryJSON string
|
||
TaskID string
|
||
DeviceIDs string
|
||
}
|
||
|
||
func NewUI(discovery *service.DiscoveryService, registry *service.RegistryService, agent *service.AgentClient, tasks *service.TaskService, templates *service.TemplateService) (*UI, error) {
|
||
tpl, err := template.New("layout").Funcs(template.FuncMap{
|
||
"json": func(v any) string {
|
||
b, _ := json.MarshalIndent(v, "", " ")
|
||
return string(b)
|
||
},
|
||
"ago": func(ms int64) string {
|
||
if ms <= 0 {
|
||
return "-"
|
||
}
|
||
d := time.Since(time.UnixMilli(ms))
|
||
if d < 0 {
|
||
d = 0
|
||
}
|
||
s := int64(d.Seconds())
|
||
switch {
|
||
case s < 60:
|
||
return fmt.Sprintf("%d秒前", s)
|
||
case s < 3600:
|
||
return fmt.Sprintf("%d分钟前", s/60)
|
||
case s < 86400:
|
||
return fmt.Sprintf("%d小时前", s/3600)
|
||
default:
|
||
return fmt.Sprintf("%d天前", s/86400)
|
||
}
|
||
},
|
||
}).ParseFS(uiFS, "ui/templates/*.html")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &UI{
|
||
discovery: discovery,
|
||
registry: registry,
|
||
agent: agent,
|
||
tasks: tasks,
|
||
templates: templates,
|
||
tpl: tpl,
|
||
}, nil
|
||
}
|
||
|
||
func (u *UI) Routes() (chi.Router, error) {
|
||
r := chi.NewRouter()
|
||
|
||
assets, err := fs.Sub(uiFS, "ui/assets")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
assetHandler := http.StripPrefix("/assets/", http.FileServer(http.FS(assets)))
|
||
r.Handle("/assets/*", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||
p := req.URL.Path
|
||
switch {
|
||
case strings.HasSuffix(p, ".css"):
|
||
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
||
case strings.HasSuffix(p, ".js"):
|
||
w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
|
||
}
|
||
assetHandler.ServeHTTP(w, req)
|
||
}))
|
||
|
||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||
http.Redirect(w, r, "/ui/devices", http.StatusFound)
|
||
})
|
||
|
||
r.Get("/devices", u.pageDevices)
|
||
r.Post("/devices/batch-action", u.actionDevicesBatchAction)
|
||
r.Post("/discovery/search", u.actionDiscoverySearch)
|
||
r.Get("/devices/{id}", u.pageDevice)
|
||
r.Post("/devices/{id}/action", u.actionDeviceAction)
|
||
r.Get("/devices/{id}/logs", u.pageDeviceLogs)
|
||
r.Get("/devices/{id}/graphs", u.pageDeviceGraphs)
|
||
r.Post("/devices/{id}/config/apply", u.actionDeviceConfigApply)
|
||
r.Get("/devices/{id}/config-ui", u.pageDeviceConfigUI)
|
||
r.Get("/devices/{id}/config-friendly", u.pageDeviceConfigFriendly)
|
||
r.Post("/devices/{id}/config-ui/plan", u.actionDeviceConfigUIPlan)
|
||
r.Post("/devices/{id}/config-ui/apply", u.actionDeviceConfigUIApply)
|
||
r.Post("/devices/{id}/face-gallery/upload", u.actionDeviceFaceGalleryUpload)
|
||
r.Post("/devices/{id}/face-gallery/reload", u.actionDeviceFaceGalleryReload)
|
||
r.Post("/devices/{id}/models/upload", u.actionDeviceModelUpload)
|
||
r.Post("/devices/{id}/media-server/configs/upload", u.actionDeviceMediaServerConfigUpload)
|
||
r.Post("/devices/{id}/media-server/configs/upload-batch", u.actionDeviceMediaServerConfigUploadBatch)
|
||
|
||
r.Get("/tasks", u.pageTasks)
|
||
r.Post("/tasks", u.actionCreateTask)
|
||
r.Get("/tasks/{id}", u.pageTask)
|
||
|
||
r.Get("/templates", u.pageTemplates)
|
||
r.Get("/templates/{name}", u.pageTemplate)
|
||
|
||
r.Get("/api", u.pageAPIConsole)
|
||
|
||
return r, nil
|
||
}
|
||
|
||
func (u *UI) render(w http.ResponseWriter, r *http.Request, content string, data PageData) {
|
||
var buf bytes.Buffer
|
||
if err := u.tpl.ExecuteTemplate(&buf, content, data); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
data.ContentHTML = template.HTML(buf.String())
|
||
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
if err := u.tpl.ExecuteTemplate(w, "layout", data); err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
}
|
||
|
||
func (u *UI) findDevice(id string) (*models.Device, bool) {
|
||
devices := u.registry.GetDevices()
|
||
for _, d := range devices {
|
||
if d.DeviceID == id {
|
||
return d, true
|
||
}
|
||
}
|
||
return nil, false
|
||
}
|
||
|
||
func (u *UI) pageDevices(w http.ResponseWriter, r *http.Request) {
|
||
devices := u.registry.GetDevices()
|
||
online := 0
|
||
for _, d := range devices {
|
||
if d.Online {
|
||
online++
|
||
}
|
||
}
|
||
u.render(w, r, "devices", PageData{
|
||
Title: "设备",
|
||
Devices: devices,
|
||
DeviceCount: len(devices),
|
||
OnlineCount: online,
|
||
OfflineCount: len(devices) - online,
|
||
})
|
||
}
|
||
|
||
func (u *UI) actionDiscoverySearch(w http.ResponseWriter, r *http.Request) {
|
||
_ = r.ParseForm()
|
||
timeoutMs, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("timeout_ms")))
|
||
if timeoutMs <= 0 {
|
||
timeoutMs = 1200
|
||
}
|
||
|
||
found, err := u.discovery.Search(timeoutMs)
|
||
devices := u.registry.GetDevices()
|
||
online := 0
|
||
for _, d := range devices {
|
||
if d.Online {
|
||
online++
|
||
}
|
||
}
|
||
data := PageData{Title: "设备", Devices: devices, Found: found, FoundCount: len(found), DeviceCount: len(devices), OnlineCount: online, OfflineCount: len(devices) - online}
|
||
if err != nil {
|
||
data.Error = err.Error()
|
||
}
|
||
u.render(w, r, "devices", data)
|
||
}
|
||
|
||
func (u *UI) actionDevicesBatchAction(w http.ResponseWriter, r *http.Request) {
|
||
_ = r.ParseForm()
|
||
action := strings.TrimSpace(r.FormValue("action"))
|
||
deviceIDs := r.Form["device_id"]
|
||
if len(deviceIDs) == 0 {
|
||
devices := u.registry.GetDevices()
|
||
online := 0
|
||
for _, d := range devices {
|
||
if d.Online {
|
||
online++
|
||
}
|
||
}
|
||
u.render(w, r, "devices", PageData{Title: "设备", Devices: devices, DeviceCount: len(devices), OnlineCount: online, OfflineCount: len(devices) - online, Error: "请先选择设备"})
|
||
return
|
||
}
|
||
|
||
typeStr := ""
|
||
switch action {
|
||
case "media_start", "media_restart", "media_stop", "reload", "rollback":
|
||
typeStr = action
|
||
default:
|
||
devices := u.registry.GetDevices()
|
||
online := 0
|
||
for _, d := range devices {
|
||
if d.Online {
|
||
online++
|
||
}
|
||
}
|
||
u.render(w, r, "devices", PageData{Title: "设备", Devices: devices, DeviceCount: len(devices), OnlineCount: online, OfflineCount: len(devices) - online, Error: "不支持的操作: " + action})
|
||
return
|
||
}
|
||
|
||
if u.tasks == nil {
|
||
http.Error(w, "task service not initialized", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
var payload any
|
||
if typeStr == "media_start" || typeStr == "media_restart" {
|
||
cfgName := strings.TrimSpace(r.FormValue("config"))
|
||
if cfgName != "" {
|
||
payload = map[string]any{"config": cfgName}
|
||
}
|
||
}
|
||
|
||
task, err := u.tasks.CreateTask(typeStr, deviceIDs, payload)
|
||
if err != nil {
|
||
devices := u.registry.GetDevices()
|
||
online := 0
|
||
for _, d := range devices {
|
||
if d.Online {
|
||
online++
|
||
}
|
||
}
|
||
u.render(w, r, "devices", PageData{Title: "设备", Devices: devices, DeviceCount: len(devices), OnlineCount: online, OfflineCount: len(devices) - online, Error: err.Error()})
|
||
return
|
||
}
|
||
|
||
http.Redirect(w, r, "/ui/tasks/"+task.ID, http.StatusFound)
|
||
}
|
||
|
||
func (u *UI) pageDevice(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
u.render(w, r, "device", PageData{Title: "设备详情", Device: dev})
|
||
}
|
||
|
||
func (u *UI) actionDeviceAction(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
_ = r.ParseForm()
|
||
action := strings.TrimSpace(r.FormValue("action"))
|
||
|
||
method := "POST"
|
||
path := ""
|
||
switch action {
|
||
case "reload":
|
||
path = "/v1/media-server/reload"
|
||
case "rollback":
|
||
path = "/v1/media-server/rollback"
|
||
case "media_start":
|
||
path = "/v1/media-server/start"
|
||
case "media_restart":
|
||
path = "/v1/media-server/restart"
|
||
case "media_stop":
|
||
path = "/v1/media-server/stop"
|
||
case "media_status":
|
||
method = "GET"
|
||
path = "/v1/media-server/status"
|
||
case "info":
|
||
method = "GET"
|
||
path = "/v1/info"
|
||
default:
|
||
http.Error(w, "unknown action", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
body, code, err := u.agent.Do(method, dev.IP, dev.AgentPort, path, nil)
|
||
msg := fmt.Sprintf("%s %s -> %d", method, path, code)
|
||
data := PageData{Title: "设备详情", Device: dev, Message: msg, RawText: string(body)}
|
||
if err != nil {
|
||
data.Error = err.Error()
|
||
}
|
||
u.render(w, r, "device", data)
|
||
}
|
||
|
||
func (u *UI) pageDeviceLogs(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
limit := strings.TrimSpace(r.URL.Query().Get("limit"))
|
||
path := "/v1/logs/recent"
|
||
if limit != "" {
|
||
path += "?limit=" + urlQueryEscape(limit)
|
||
}
|
||
|
||
body, code, err := u.agent.Do("GET", dev.IP, dev.AgentPort, path, nil)
|
||
data := PageData{Title: "设备日志", Device: dev, Message: fmt.Sprintf("GET %s -> %d", path, code), RawText: string(body)}
|
||
if err != nil {
|
||
data.Error = err.Error()
|
||
}
|
||
u.render(w, r, "device_logs", data)
|
||
}
|
||
|
||
func (u *UI) pageDeviceGraphs(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
body, code, err := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/graphs", nil)
|
||
data := PageData{Title: "设备图表", Device: dev, Message: fmt.Sprintf("GET /v1/graphs -> %d", code), RawText: string(body)}
|
||
if err != nil {
|
||
data.Error = err.Error()
|
||
}
|
||
u.render(w, r, "device_graphs", data)
|
||
}
|
||
|
||
func (u *UI) actionDeviceConfigApply(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
_ = r.ParseForm()
|
||
raw := strings.TrimSpace(r.FormValue("json"))
|
||
if raw == "" {
|
||
raw = `{"config":{}}`
|
||
}
|
||
|
||
body, code, err := u.agent.Do("PUT", dev.IP, dev.AgentPort, "/v1/config", []byte(raw))
|
||
data := PageData{Title: "设备详情", Device: dev, Message: fmt.Sprintf("PUT /v1/config -> %d", code), RawText: string(body), RawJSON: raw}
|
||
if err != nil {
|
||
data.Error = err.Error()
|
||
}
|
||
u.render(w, r, "device", data)
|
||
}
|
||
|
||
func (u *UI) actionDeviceModelUpload(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
if err := r.ParseMultipartForm(100 << 20); err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
name := strings.TrimSpace(r.FormValue("name"))
|
||
file, hdr, err := r.FormFile("file")
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
defer file.Close()
|
||
if name == "" {
|
||
http.Error(w, "name is required", http.StatusBadRequest)
|
||
return
|
||
}
|
||
path := fmt.Sprintf("/v1/models/%s", name)
|
||
resp, code, derr := u.agent.DoStream("PUT", dev.IP, dev.AgentPort, path, file, "application/octet-stream", hdr.Size)
|
||
out := PageData{Title: "设备详情", Device: dev, Message: fmt.Sprintf("PUT %s -> %d", path, code), RawText: string(resp)}
|
||
if derr != nil {
|
||
out.Error = derr.Error()
|
||
}
|
||
u.render(w, r, "device", out)
|
||
}
|
||
|
||
func (u *UI) actionDeviceMediaServerConfigUpload(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
if err := r.ParseMultipartForm(50 << 20); err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
name, err := normalizeConfigName(r.FormValue("name"))
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
file, hdr, err := r.FormFile("file")
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
path := "/v1/media-server/configs/" + url.PathEscape(name)
|
||
resp, code, derr := u.agent.DoStream("PUT", dev.IP, dev.AgentPort, path, file, "application/json", hdr.Size)
|
||
data := PageData{Title: "设备详情", Device: dev, Message: fmt.Sprintf("PUT %s -> %d", path, code), RawText: string(resp)}
|
||
if derr != nil {
|
||
data.Error = derr.Error()
|
||
}
|
||
u.render(w, r, "device", data)
|
||
}
|
||
|
||
func (u *UI) actionDeviceMediaServerConfigUploadBatch(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
|
||
if err := r.ParseMultipartForm(200 << 20); err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
if r.MultipartForm == nil || len(r.MultipartForm.File) == 0 {
|
||
http.Error(w, "files is required", http.StatusBadRequest)
|
||
return
|
||
}
|
||
files := r.MultipartForm.File["files"]
|
||
if len(files) == 0 {
|
||
http.Error(w, "files is required", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
var sb strings.Builder
|
||
errCount := 0
|
||
for _, hdr := range files {
|
||
name, nerr := normalizeConfigName(filepath.Base(hdr.Filename))
|
||
if nerr != nil {
|
||
errCount++
|
||
sb.WriteString(fmt.Sprintf("%s -> invalid name: %v\n", hdr.Filename, nerr))
|
||
continue
|
||
}
|
||
file, err := hdr.Open()
|
||
if err != nil {
|
||
errCount++
|
||
sb.WriteString(fmt.Sprintf("%s -> open failed: %v\n", name, err))
|
||
continue
|
||
}
|
||
path := "/v1/media-server/configs/" + url.PathEscape(name)
|
||
resp, code, derr := u.agent.DoStream("PUT", dev.IP, dev.AgentPort, path, file, "application/json", hdr.Size)
|
||
_ = file.Close()
|
||
if derr != nil {
|
||
errCount++
|
||
sb.WriteString(fmt.Sprintf("%s -> %d error: %v\n", name, code, derr))
|
||
continue
|
||
}
|
||
if len(resp) > 0 {
|
||
sb.WriteString(fmt.Sprintf("%s -> %d %s\n", name, code, strings.TrimSpace(string(resp))))
|
||
} else {
|
||
sb.WriteString(fmt.Sprintf("%s -> %d\n", name, code))
|
||
}
|
||
}
|
||
|
||
data := PageData{Title: "设备详情", Device: dev, Message: "批量上传完成", RawText: sb.String()}
|
||
if errCount > 0 {
|
||
data.Error = fmt.Sprintf("部分失败: %d/%d", errCount, len(files))
|
||
}
|
||
u.render(w, r, "device", data)
|
||
}
|
||
|
||
func (u *UI) pageTasks(w http.ResponseWriter, r *http.Request) {
|
||
u.render(w, r, "tasks", PageData{Title: "任务", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices()})
|
||
}
|
||
|
||
func (u *UI) actionCreateTask(w http.ResponseWriter, r *http.Request) {
|
||
_ = r.ParseForm()
|
||
typeStr := strings.TrimSpace(r.FormValue("type"))
|
||
if typeStr == "" {
|
||
typeStr = "config_apply"
|
||
}
|
||
ids := strings.TrimSpace(r.FormValue("device_ids"))
|
||
var deviceIDs []string
|
||
for _, p := range strings.Split(ids, ",") {
|
||
p = strings.TrimSpace(p)
|
||
if p != "" {
|
||
deviceIDs = append(deviceIDs, p)
|
||
}
|
||
}
|
||
raw := strings.TrimSpace(r.FormValue("payload_json"))
|
||
if raw == "" {
|
||
raw = `{"config":{}}`
|
||
}
|
||
var payload any
|
||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||
u.render(w, r, "tasks", PageData{Title: "任务", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: "payload_json 无效: " + err.Error(), RawJSON: raw, DeviceIDs: ids})
|
||
return
|
||
}
|
||
|
||
task, err := u.tasks.CreateTask(typeStr, deviceIDs, payload)
|
||
if err != nil {
|
||
u.render(w, r, "tasks", PageData{Title: "任务", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: err.Error(), RawJSON: raw, DeviceIDs: ids})
|
||
return
|
||
}
|
||
http.Redirect(w, r, "/ui/tasks/"+task.ID, http.StatusFound)
|
||
}
|
||
|
||
func (u *UI) pageTask(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
items := u.tasks.ListTasks()
|
||
var task *models.Task
|
||
for i := range items {
|
||
if items[i].ID == id {
|
||
t := items[i]
|
||
task = &t
|
||
break
|
||
}
|
||
}
|
||
if task == nil {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
u.render(w, r, "task", PageData{Title: "任务详情", Task: task, TaskID: id})
|
||
}
|
||
|
||
func (u *UI) pageTemplates(w http.ResponseWriter, r *http.Request) {
|
||
list, err := u.templates.ListTemplates()
|
||
data := PageData{Title: "模板", Templates: list}
|
||
if err != nil {
|
||
data.Error = err.Error()
|
||
}
|
||
u.render(w, r, "templates", data)
|
||
}
|
||
|
||
func (u *UI) pageTemplate(w http.ResponseWriter, r *http.Request) {
|
||
name := chi.URLParam(r, "name")
|
||
t, err := u.templates.GetTemplate(name)
|
||
if err != nil {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
u.render(w, r, "template", PageData{Title: "模板详情", Template: t})
|
||
}
|
||
|
||
func (u *UI) pageAPIConsole(w http.ResponseWriter, r *http.Request) {
|
||
u.render(w, r, "api", PageData{Title: "接口调试"})
|
||
}
|
||
|
||
func urlQueryEscape(s string) string {
|
||
r := strings.NewReplacer("%", "%25", " ", "%20", "+", "%2B", "&", "%26", "=", "%3D", "?", "%3F")
|
||
return r.Replace(s)
|
||
}
|
||
|
||
func normalizeConfigName(name string) (string, error) {
|
||
name = strings.TrimSpace(name)
|
||
if name == "" {
|
||
return "", fmt.Errorf("name is required")
|
||
}
|
||
if strings.ContainsAny(name, "/\\") || strings.Contains(name, "..") {
|
||
return "", fmt.Errorf("name contains invalid path")
|
||
}
|
||
if !strings.HasSuffix(strings.ToLower(name), ".json") {
|
||
name += ".json"
|
||
}
|
||
return name, nil
|
||
}
|
||
|
||
func prettyJSON(raw []byte) string {
|
||
var out bytes.Buffer
|
||
if err := json.Indent(&out, raw, "", " "); err != nil {
|
||
return string(raw)
|
||
}
|
||
return out.String()
|
||
}
|
||
|
||
func (u *UI) loadConfigUIData(dev *models.Device) PageData {
|
||
schemaBody, schemaCode, schemaErr := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/config/ui/schema", nil)
|
||
stateBody, stateCode, stateErr := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/config/ui/state", nil)
|
||
faceBody, faceCode, faceErr := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/face-gallery", nil)
|
||
|
||
data := PageData{
|
||
Title: "通道配置(instances)",
|
||
Device: dev,
|
||
SchemaJSON: fmt.Sprintf("GET /v1/config/ui/schema -> %d\n%s", schemaCode, prettyJSON(schemaBody)),
|
||
StateJSON: fmt.Sprintf("GET /v1/config/ui/state -> %d\n%s", stateCode, prettyJSON(stateBody)),
|
||
FaceGalleryJSON: fmt.Sprintf("GET /v1/face-gallery -> %d\n%s", faceCode, prettyJSON(faceBody)),
|
||
RawJSON: strings.TrimSpace(prettyJSON(stateBody)),
|
||
}
|
||
if schemaErr != nil {
|
||
data.Error = schemaErr.Error()
|
||
} else if stateErr != nil {
|
||
data.Error = stateErr.Error()
|
||
} else if faceErr != nil {
|
||
data.Error = faceErr.Error()
|
||
}
|
||
return data
|
||
}
|
||
|
||
func (u *UI) pageDeviceConfigUI(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
u.render(w, r, "config_ui", u.loadConfigUIData(dev))
|
||
}
|
||
|
||
func (u *UI) pageDeviceConfigFriendly(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
u.render(w, r, "config_friendly", PageData{Title: "友好配置", Device: dev})
|
||
}
|
||
|
||
func (u *UI) actionDeviceConfigUIPlan(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
_ = r.ParseForm()
|
||
raw := strings.TrimSpace(r.FormValue("json"))
|
||
if raw == "" {
|
||
raw = `{"instances":[]}`
|
||
}
|
||
if err := json.Unmarshal([]byte(raw), new(any)); err != nil {
|
||
data := u.loadConfigUIData(dev)
|
||
data.Error = "json 无效: " + err.Error()
|
||
data.RawJSON = raw
|
||
u.render(w, r, "config_ui", data)
|
||
return
|
||
}
|
||
|
||
body, code, err := u.agent.Do("POST", dev.IP, dev.AgentPort, "/v1/config/ui/plan", []byte(raw))
|
||
data := u.loadConfigUIData(dev)
|
||
data.Message = fmt.Sprintf("POST /v1/config/ui/plan -> %d", code)
|
||
data.RawText = prettyJSON(body)
|
||
data.RawJSON = raw
|
||
if err != nil {
|
||
data.Error = err.Error()
|
||
}
|
||
u.render(w, r, "config_ui", data)
|
||
}
|
||
|
||
func (u *UI) actionDeviceConfigUIApply(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
_ = r.ParseForm()
|
||
raw := strings.TrimSpace(r.FormValue("json"))
|
||
if raw == "" {
|
||
raw = `{"instances":[]}`
|
||
}
|
||
if err := json.Unmarshal([]byte(raw), new(any)); err != nil {
|
||
data := u.loadConfigUIData(dev)
|
||
data.Error = "json 无效: " + err.Error()
|
||
data.RawJSON = raw
|
||
u.render(w, r, "config_ui", data)
|
||
return
|
||
}
|
||
|
||
body, code, err := u.agent.Do("POST", dev.IP, dev.AgentPort, "/v1/config/ui/apply", []byte(raw))
|
||
data := u.loadConfigUIData(dev)
|
||
data.Message = fmt.Sprintf("POST /v1/config/ui/apply -> %d", code)
|
||
data.RawText = prettyJSON(body)
|
||
data.RawJSON = raw
|
||
if err != nil {
|
||
data.Error = err.Error()
|
||
}
|
||
u.render(w, r, "config_ui", data)
|
||
}
|
||
|
||
func (u *UI) actionDeviceFaceGalleryUpload(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
if err := r.ParseMultipartForm(500 << 20); err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
file, hdr, err := r.FormFile("file")
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
resp, code, derr := u.agent.DoStream("PUT", dev.IP, dev.AgentPort, "/v1/face-gallery", file, "application/octet-stream", hdr.Size)
|
||
data := u.loadConfigUIData(dev)
|
||
data.Message = fmt.Sprintf("PUT /v1/face-gallery -> %d", code)
|
||
data.RawText = prettyJSON(resp)
|
||
if derr != nil {
|
||
data.Error = derr.Error()
|
||
}
|
||
u.render(w, r, "config_ui", data)
|
||
}
|
||
|
||
func (u *UI) actionDeviceFaceGalleryReload(w http.ResponseWriter, r *http.Request) {
|
||
id := chi.URLParam(r, "id")
|
||
dev, ok := u.findDevice(id)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
resp, code, err := u.agent.Do("POST", dev.IP, dev.AgentPort, "/v1/face-gallery/reload", nil)
|
||
data := u.loadConfigUIData(dev)
|
||
data.Message = fmt.Sprintf("POST /v1/face-gallery/reload -> %d", code)
|
||
data.RawText = prettyJSON(resp)
|
||
if err != nil {
|
||
data.Error = err.Error()
|
||
}
|
||
u.render(w, r, "config_ui", data)
|
||
}
|