Files
3m-ui/backend/internal/system/api.go
T
Z User 5b9674987f fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
fix(backup): restore mihomo-config.yaml from backup zip
Backup zip contains 3m-ui.db + mihomo-config.yaml + backup-meta.txt.
RestoreDatabase only extracted 3m-ui.db — mihomo-config.yaml was
silently dropped. After restoring a backup, the mihomo config file
on disk was stale (from before the backup was taken).
Fix:
- extractDBFromZip now returns both dbContent and mihomoCfg
- RestoreDatabase takes mihomoCfgPath parameter
- After restoring the DB, also writes mihomo-config.yaml if the
zip contained it and the path is configured
- Mihomo config write failure does NOT fail the whole restore
(DB is already restored; operator can manually restore config)
- api.go passes h.mihomoCfg to RestoreDatabase
2026-08-31 03:42:32 +00:00

167 lines
5.2 KiB
Go

package system
import (
"fmt"
"log"
"net/http"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
)
const maxRestoreDatabaseBytes = 128 << 20
type Handler struct {
svc *Service
dbPath string
mihomoCfg string
}
func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc}
}
func (h *Handler) WithBackupPaths(dbPath, mihomoConfig string) *Handler {
h.dbPath = dbPath
h.mihomoCfg = mihomoConfig
return h
}
func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) {
rg.GET("/status", h.GetSystemStatus)
rg.GET("/backup", h.ExportBackup)
rg.POST("/backup/restore-db", h.RestoreDatabase)
rg.POST("/templates/reverse-proxy", h.ReverseProxy)
rg.POST("/templates/acme", h.ACME)
rg.POST("/geofiles/update", h.UpdateGeoFiles)
rg.POST("/templates/warp", h.WARP)
}
func (h *Handler) GetSystemStatus(c *gin.Context) {
stats := h.svc.GetStatus()
c.JSON(http.StatusOK, stats)
}
func (h *Handler) ExportBackup(c *gin.Context) {
name := fmt.Sprintf("3m-ui-backup-%s.zip", time.Now().UTC().Format("20060102-150405"))
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", name))
if err := WriteZip(c.Writer, BackupPaths{DatabasePath: h.dbPath, MihomoConfig: h.mihomoCfg}); err != nil {
_ = c.Error(err)
return
}
}
func (h *Handler) RestoreDatabase(c *gin.Context) {
if h.dbPath == "" {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database path is not configured"})
return
}
// FormFile otherwise permits arbitrarily large multipart requests to be
// written to disk, turning an authenticated endpoint into a storage DoS.
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxRestoreDatabaseBytes)
file, err := c.FormFile("database")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "multipart field database is required and must be <= 128 MiB"})
return
}
if file.Size > maxRestoreDatabaseBytes {
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "database backup exceeds 128 MiB limit"})
return
}
f, err := file.Open()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
defer f.Close()
if err := RestoreDatabase(h.dbPath, h.mihomoCfg, f); err != nil {
log.Printf("system restore-database failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
// The running panel still holds the old (now-unlinked) SQLite inode via
// GORM; we cannot cleanly close/reopen it in-place. The operator MUST
// restart the panel or every subsequent write goes to a stale file and
// is lost on next start. Log loudly so this is visible in journalctl.
log.Printf("[WARNING] Database restored from backup. The panel MUST be restarted (systemctl restart 3m-ui) for changes to take effect. Further writes will be lost.")
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"restart_required": true,
"message": "Database restored. *** RESTART REQUIRED *** The running panel still holds the old SQLite handle; run `systemctl restart 3m-ui` (or restart the panel process) now, otherwise all further DB writes will be lost.",
"path": filepath.Base(h.dbPath),
})
}
func (h *Handler) ReverseProxy(c *gin.Context) {
var req struct {
Kind string `json:"kind"`
Domain string `json:"domain"`
Upstream string `json:"upstream"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
out, err := ReverseProxyTemplate(req.Kind, req.Domain, req.Upstream)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"config": out})
}
func (h *Handler) ACME(c *gin.Context) {
var req struct {
Domain string `json:"domain"`
Email string `json:"email"`
Webroot string `json:"webroot"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Domain == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "domain is required"})
return
}
cmd, err := ACMECommand(req.Domain, req.Email, req.Webroot)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"command": cmd})
}
// UpdateGeoFiles downloads MetaCubeX GeoIP/GeoSite databases next to the Mihomo config .
func (h *Handler) UpdateGeoFiles(c *gin.Context) {
dir := filepath.Dir(h.mihomoCfg)
if h.mihomoCfg == "" {
dir = "/var/lib/3m-ui/mihomo"
}
result, err := UpdateGeoFiles(dir)
if err != nil {
log.Printf("system update-geofiles failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
c.JSON(http.StatusOK, gin.H{"dir": dir, "files": result})
}
// WARP returns a Mihomo WireGuard fragment for Cloudflare WARP .
func (h *Handler) WARP(c *gin.Context) {
var body struct {
PrivateKey string `json:"private_key"`
Address string `json:"address"`
Reserved string `json:"reserved"`
}
_ = c.ShouldBindJSON(&body)
yaml, err := WARPTemplate(body.PrivateKey, body.Address, body.Reserved)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"yaml": yaml})
}