|
|
@@ -0,0 +1,585 @@
|
|
|
+package web
|
|
|
+
|
|
|
+import (
|
|
|
+ "crypto/rand"
|
|
|
+ "encoding/hex"
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "log"
|
|
|
+ "net/http"
|
|
|
+ "os"
|
|
|
+ "path/filepath"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "ftp-server/config"
|
|
|
+ "ftp-server/database"
|
|
|
+ "ftp-server/ftp"
|
|
|
+
|
|
|
+ "github.com/golang-jwt/jwt/v5"
|
|
|
+)
|
|
|
+
|
|
|
+// Server Web管理服务器
|
|
|
+type Server struct {
|
|
|
+ config *config.Config
|
|
|
+ db *database.DB
|
|
|
+ ftpServer *ftp.Server
|
|
|
+ jwtSecret []byte
|
|
|
+}
|
|
|
+
|
|
|
+// NewServer 创建Web服务器
|
|
|
+func NewServer(cfg *config.Config, db *database.DB, ftpSrv *ftp.Server) *Server {
|
|
|
+ secret := make([]byte, 32)
|
|
|
+ rand.Read(secret)
|
|
|
+ return &Server{
|
|
|
+ config: cfg,
|
|
|
+ db: db,
|
|
|
+ ftpServer: ftpSrv,
|
|
|
+ jwtSecret: secret,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Start 启动Web服务
|
|
|
+func (s *Server) Start() error {
|
|
|
+ webCfg := s.config.Get().Web
|
|
|
+
|
|
|
+ mux := http.NewServeMux()
|
|
|
+
|
|
|
+ // 静态文件
|
|
|
+ fs := http.FileServer(http.Dir("./static"))
|
|
|
+ mux.Handle("/", fs)
|
|
|
+
|
|
|
+ // API路由
|
|
|
+ mux.HandleFunc("/api/login", s.handleLogin)
|
|
|
+ mux.HandleFunc("/api/dashboard", s.authMiddleware(s.handleDashboard))
|
|
|
+ mux.HandleFunc("/api/users", s.authMiddleware(s.handleUsers))
|
|
|
+ mux.HandleFunc("/api/users/", s.authMiddleware(s.handleUserOperation))
|
|
|
+ mux.HandleFunc("/api/files", s.authMiddleware(s.handleFileBrowse))
|
|
|
+ mux.HandleFunc("/api/files/", s.authMiddleware(s.handleFileBrowse))
|
|
|
+ mux.HandleFunc("/api/logs", s.authMiddleware(s.handleLogs))
|
|
|
+ mux.HandleFunc("/api/online", s.authMiddleware(s.handleOnline))
|
|
|
+ mux.HandleFunc("/api/config", s.authMiddleware(s.handleConfig))
|
|
|
+ mux.HandleFunc("/api/upload", s.authMiddleware(s.handleUpload))
|
|
|
+
|
|
|
+ addr := fmt.Sprintf("%s:%d", webCfg.Host, webCfg.Port)
|
|
|
+ log.Printf("Web管理界面已启动: http://localhost:%d", webCfg.Port)
|
|
|
+
|
|
|
+ server := &http.Server{
|
|
|
+ Addr: addr,
|
|
|
+ Handler: mux,
|
|
|
+ ReadTimeout: 30 * time.Second,
|
|
|
+ WriteTimeout: 30 * time.Second,
|
|
|
+ }
|
|
|
+
|
|
|
+ return server.ListenAndServe()
|
|
|
+}
|
|
|
+
|
|
|
+// --- 中间件 ---
|
|
|
+
|
|
|
+func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
|
+ return func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ tokenStr := r.Header.Get("Authorization")
|
|
|
+ if tokenStr == "" {
|
|
|
+ // 也支持 cookie
|
|
|
+ if cookie, err := r.Cookie("token"); err == nil {
|
|
|
+ tokenStr = cookie.Value
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if strings.HasPrefix(tokenStr, "Bearer ") {
|
|
|
+ tokenStr = tokenStr[7:]
|
|
|
+ }
|
|
|
+
|
|
|
+ if tokenStr == "" {
|
|
|
+ s.jsonError(w, "未登录", http.StatusUnauthorized)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
|
|
|
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
|
+ return nil, fmt.Errorf("无效的签名方法")
|
|
|
+ }
|
|
|
+ return s.jwtSecret, nil
|
|
|
+ })
|
|
|
+
|
|
|
+ if err != nil || !token.Valid {
|
|
|
+ s.jsonError(w, "登录已过期", http.StatusUnauthorized)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ next(w, r)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// --- 请求处理 ---
|
|
|
+
|
|
|
+// handleLogin 登录
|
|
|
+func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if r.Method != http.MethodPost {
|
|
|
+ s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var req struct {
|
|
|
+ Username string `json:"username"`
|
|
|
+ Password string `json:"password"`
|
|
|
+ }
|
|
|
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
+ s.jsonError(w, "请求格式错误", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ adminCfg := s.config.Get().Admin
|
|
|
+ if req.Username != adminCfg.Username || req.Password != adminCfg.Password {
|
|
|
+ s.jsonError(w, "用户名或密码错误", http.StatusUnauthorized)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
|
+ "username": req.Username,
|
|
|
+ "exp": time.Now().Add(24 * time.Hour).Unix(),
|
|
|
+ })
|
|
|
+
|
|
|
+ tokenStr, err := token.SignedString(s.jwtSecret)
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, "生成令牌失败", http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ s.jsonResponse(w, http.StatusOK, map[string]interface{}{
|
|
|
+ "token": tokenStr,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// handleDashboard 仪表盘
|
|
|
+func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
|
|
+ stats, err := s.db.GetLogStats()
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加在线用户数
|
|
|
+ if s.ftpServer != nil {
|
|
|
+ online := s.ftpServer.GetOnlineUsers()
|
|
|
+ stats["online_users"] = len(online)
|
|
|
+ } else {
|
|
|
+ stats["online_users"] = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ s.jsonResponse(w, http.StatusOK, stats)
|
|
|
+}
|
|
|
+
|
|
|
+// handleUsers 用户管理
|
|
|
+func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
|
|
|
+ switch r.Method {
|
|
|
+ case http.MethodGet:
|
|
|
+ users, err := s.db.ListUsers()
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if users == nil {
|
|
|
+ users = []database.FTPUser{}
|
|
|
+ }
|
|
|
+ s.jsonResponse(w, http.StatusOK, users)
|
|
|
+
|
|
|
+ case http.MethodPost:
|
|
|
+ var user database.FTPUser
|
|
|
+ if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
|
|
+ s.jsonError(w, "请求格式错误", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if user.Username == "" || user.Password == "" {
|
|
|
+ s.jsonError(w, "用户名和密码不能为空", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if user.HomeDir == "" {
|
|
|
+ ftpCfg := s.config.Get().FTP
|
|
|
+ user.HomeDir = filepath.Join(ftpCfg.RootDir, user.Username)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 自动创建用户目录(如果不存在)
|
|
|
+ if err := os.MkdirAll(user.HomeDir, 0755); err != nil {
|
|
|
+ s.jsonError(w, fmt.Sprintf("创建用户目录失败: %v", err), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if user.Permissions == "" {
|
|
|
+ user.Permissions = "read,write"
|
|
|
+ }
|
|
|
+ user.Enabled = true
|
|
|
+
|
|
|
+ if err := s.db.CreateUser(&user); err != nil {
|
|
|
+ s.jsonError(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ s.jsonResponse(w, http.StatusOK, user)
|
|
|
+
|
|
|
+ default:
|
|
|
+ s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// handleUserOperation 单个用户操作
|
|
|
+func (s *Server) handleUserOperation(w http.ResponseWriter, r *http.Request) {
|
|
|
+ // 解析路径中的用户名: /api/users/{username} 或 /api/users/{username}/password
|
|
|
+ pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/users/"), "/")
|
|
|
+ username := pathParts[0]
|
|
|
+
|
|
|
+ if username == "" {
|
|
|
+ s.jsonError(w, "用户名不能为空", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 密码修改: PUT /api/users/{username}/password
|
|
|
+ if len(pathParts) > 1 && pathParts[1] == "password" && r.Method == http.MethodPut {
|
|
|
+ var req struct {
|
|
|
+ Password string `json:"password"`
|
|
|
+ }
|
|
|
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
+ s.jsonError(w, "请求格式错误", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if req.Password == "" {
|
|
|
+ s.jsonError(w, "密码不能为空", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err := s.db.UpdateUserPassword(username, req.Password); err != nil {
|
|
|
+ s.jsonError(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ s.jsonResponse(w, http.StatusOK, map[string]string{"message": "密码已更新"})
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ switch r.Method {
|
|
|
+ case http.MethodGet:
|
|
|
+ user, err := s.db.GetUser(username)
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if user == nil {
|
|
|
+ s.jsonError(w, "用户不存在", http.StatusNotFound)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ s.jsonResponse(w, http.StatusOK, user)
|
|
|
+
|
|
|
+ case http.MethodPut:
|
|
|
+ var user database.FTPUser
|
|
|
+ if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
|
|
+ s.jsonError(w, "请求格式错误", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果目录有变化,自动创建新目录
|
|
|
+ if user.HomeDir != "" {
|
|
|
+ if err := os.MkdirAll(user.HomeDir, 0755); err != nil {
|
|
|
+ s.jsonError(w, fmt.Sprintf("创建目录失败: %v", err), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := s.db.UpdateUser(&user); err != nil {
|
|
|
+ s.jsonError(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ s.jsonResponse(w, http.StatusOK, map[string]string{"message": "用户已更新"})
|
|
|
+
|
|
|
+ case http.MethodDelete:
|
|
|
+ if err := s.db.DeleteUser(username); err != nil {
|
|
|
+ s.jsonError(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ s.jsonResponse(w, http.StatusOK, map[string]string{"message": "用户已删除"})
|
|
|
+
|
|
|
+ default:
|
|
|
+ s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// FileInfo 文件信息
|
|
|
+type FileInfo struct {
|
|
|
+ Name string `json:"name"`
|
|
|
+ Size int64 `json:"size"`
|
|
|
+ IsDir bool `json:"is_dir"`
|
|
|
+ ModTime time.Time `json:"mod_time"`
|
|
|
+ Path string `json:"path"`
|
|
|
+}
|
|
|
+
|
|
|
+// handleFileBrowse 文件浏览
|
|
|
+func (s *Server) handleFileBrowse(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if r.Method != http.MethodGet {
|
|
|
+ s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ dir := r.URL.Query().Get("path")
|
|
|
+ if dir == "" {
|
|
|
+ dir = s.config.Get().FTP.RootDir
|
|
|
+ }
|
|
|
+
|
|
|
+ // 安全检查:确保路径在FTP根目录内
|
|
|
+ rootDir, err := filepath.Abs(s.config.Get().FTP.RootDir)
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, "无效的根目录", http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ absPath, err := filepath.Abs(dir)
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, "无效的路径", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if !strings.HasPrefix(absPath, rootDir) {
|
|
|
+ s.jsonError(w, "路径超出允许范围", http.StatusForbidden)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ entries, err := os.ReadDir(absPath)
|
|
|
+ if err != nil {
|
|
|
+ if os.IsNotExist(err) {
|
|
|
+ s.jsonResponse(w, http.StatusOK, []FileInfo{})
|
|
|
+ return
|
|
|
+ }
|
|
|
+ s.jsonError(w, fmt.Sprintf("读取目录失败: %v", err), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var files []FileInfo
|
|
|
+ for _, entry := range entries {
|
|
|
+ info, err := entry.Info()
|
|
|
+ if err != nil {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ files = append(files, FileInfo{
|
|
|
+ Name: entry.Name(),
|
|
|
+ Size: info.Size(),
|
|
|
+ IsDir: entry.IsDir(),
|
|
|
+ ModTime: info.ModTime(),
|
|
|
+ Path: filepath.Join(absPath, entry.Name()),
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if files == nil {
|
|
|
+ files = []FileInfo{}
|
|
|
+ }
|
|
|
+
|
|
|
+ s.jsonResponse(w, http.StatusOK, map[string]interface{}{
|
|
|
+ "path": absPath,
|
|
|
+ "files": files,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// handleUpload 文件上传
|
|
|
+func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if r.Method != http.MethodPost {
|
|
|
+ s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ targetDir := r.URL.Query().Get("path")
|
|
|
+ if targetDir == "" {
|
|
|
+ targetDir = s.config.Get().FTP.RootDir
|
|
|
+ }
|
|
|
+
|
|
|
+ // 安全检查
|
|
|
+ rootDir, _ := filepath.Abs(s.config.Get().FTP.RootDir)
|
|
|
+ absDir, err := filepath.Abs(targetDir)
|
|
|
+ if err != nil || !strings.HasPrefix(absDir, rootDir) {
|
|
|
+ s.jsonError(w, "路径超出允许范围", http.StatusForbidden)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ r.ParseMultipartForm(100 << 20) // 100MB最大
|
|
|
+
|
|
|
+ file, header, err := r.FormFile("file")
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, "读取文件失败", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer file.Close()
|
|
|
+
|
|
|
+ targetPath := filepath.Join(absDir, header.Filename)
|
|
|
+ dst, err := os.Create(targetPath)
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, "创建文件失败", http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer dst.Close()
|
|
|
+
|
|
|
+ written, err := io.Copy(dst, file)
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, "写入文件失败", http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ s.jsonResponse(w, http.StatusOK, map[string]interface{}{
|
|
|
+ "message": "上传成功",
|
|
|
+ "size": written,
|
|
|
+ "path": targetPath,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// handleLogs 日志查询
|
|
|
+func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if r.Method != http.MethodGet {
|
|
|
+ s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ username := r.URL.Query().Get("username")
|
|
|
+ action := r.URL.Query().Get("action")
|
|
|
+ page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
|
|
+ pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
|
|
|
+
|
|
|
+ if page <= 0 {
|
|
|
+ page = 1
|
|
|
+ }
|
|
|
+ if pageSize <= 0 {
|
|
|
+ pageSize = 20
|
|
|
+ }
|
|
|
+
|
|
|
+ logs, total, err := s.db.QueryLogs(username, action, page, pageSize)
|
|
|
+ if err != nil {
|
|
|
+ s.jsonError(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if logs == nil {
|
|
|
+ logs = []database.FTPLog{}
|
|
|
+ }
|
|
|
+
|
|
|
+ s.jsonResponse(w, http.StatusOK, map[string]interface{}{
|
|
|
+ "logs": logs,
|
|
|
+ "total": total,
|
|
|
+ "page": page,
|
|
|
+ "page_size": pageSize,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// handleOnline 在线用户
|
|
|
+func (s *Server) handleOnline(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if r.Method != http.MethodGet {
|
|
|
+ s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var users []database.OnlineUser
|
|
|
+ if s.ftpServer != nil {
|
|
|
+ users = s.ftpServer.GetOnlineUsers()
|
|
|
+ }
|
|
|
+ if users == nil {
|
|
|
+ users = []database.OnlineUser{}
|
|
|
+ }
|
|
|
+
|
|
|
+ s.jsonResponse(w, http.StatusOK, users)
|
|
|
+}
|
|
|
+
|
|
|
+// handleConfig 配置管理
|
|
|
+func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|
|
+ switch r.Method {
|
|
|
+ case http.MethodGet:
|
|
|
+ cfg := s.config.Get()
|
|
|
+ // 隐藏敏感信息
|
|
|
+ safeCfg := map[string]interface{}{
|
|
|
+ "ftp": map[string]interface{}{
|
|
|
+ "host": cfg.FTP.Host,
|
|
|
+ "port": cfg.FTP.Port,
|
|
|
+ "passive_port_min": cfg.FTP.PassivePortMin,
|
|
|
+ "passive_port_max": cfg.FTP.PassivePortMax,
|
|
|
+ "root_dir": cfg.FTP.RootDir,
|
|
|
+ "enable_anonymous": cfg.FTP.EnableAnonymous,
|
|
|
+ "max_connections": cfg.FTP.MaxConnections,
|
|
|
+ "idle_timeout": cfg.FTP.IdleTimeout,
|
|
|
+ },
|
|
|
+ "web": map[string]interface{}{
|
|
|
+ "host": cfg.Web.Host,
|
|
|
+ "port": cfg.Web.Port,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ s.jsonResponse(w, http.StatusOK, safeCfg)
|
|
|
+
|
|
|
+ case http.MethodPut:
|
|
|
+ var update map[string]interface{}
|
|
|
+ if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
|
|
+ s.jsonError(w, "请求格式错误", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ s.config.Update(func(cfg *config.Config) {
|
|
|
+ if ftpCfg, ok := update["ftp"].(map[string]interface{}); ok {
|
|
|
+ if v, ok := ftpCfg["port"].(float64); ok {
|
|
|
+ cfg.FTP.Port = int(v)
|
|
|
+ }
|
|
|
+ if v, ok := ftpCfg["passive_port_min"].(float64); ok {
|
|
|
+ cfg.FTP.PassivePortMin = int(v)
|
|
|
+ }
|
|
|
+ if v, ok := ftpCfg["passive_port_max"].(float64); ok {
|
|
|
+ cfg.FTP.PassivePortMax = int(v)
|
|
|
+ }
|
|
|
+ if v, ok := ftpCfg["enable_anonymous"].(bool); ok {
|
|
|
+ cfg.FTP.EnableAnonymous = v
|
|
|
+ }
|
|
|
+ if v, ok := ftpCfg["max_connections"].(float64); ok {
|
|
|
+ cfg.FTP.MaxConnections = int(v)
|
|
|
+ }
|
|
|
+ if v, ok := ftpCfg["idle_timeout"].(float64); ok {
|
|
|
+ cfg.FTP.IdleTimeout = int(v)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if adminCfg, ok := update["admin"].(map[string]interface{}); ok {
|
|
|
+ if v, ok := adminCfg["username"].(string); ok && v != "" {
|
|
|
+ cfg.Admin.Username = v
|
|
|
+ }
|
|
|
+ if v, ok := adminCfg["password"].(string); ok && v != "" {
|
|
|
+ cfg.Admin.Password = v
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ if err := s.config.Save("config.json"); err != nil {
|
|
|
+ s.jsonError(w, fmt.Sprintf("保存配置失败: %v", err), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ s.jsonResponse(w, http.StatusOK, map[string]string{"message": "配置已保存,部分设置需要重启生效"})
|
|
|
+
|
|
|
+ default:
|
|
|
+ s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// --- 工具方法 ---
|
|
|
+
|
|
|
+func (s *Server) jsonResponse(w http.ResponseWriter, status int, data interface{}) {
|
|
|
+ w.Header().Set("Content-Type", "application/json")
|
|
|
+ w.WriteHeader(status)
|
|
|
+ json.NewEncoder(w).Encode(map[string]interface{}{
|
|
|
+ "code": status,
|
|
|
+ "data": data,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func (s *Server) jsonError(w http.ResponseWriter, msg string, status int) {
|
|
|
+ w.Header().Set("Content-Type", "application/json")
|
|
|
+ w.WriteHeader(status)
|
|
|
+ json.NewEncoder(w).Encode(map[string]interface{}{
|
|
|
+ "code": status,
|
|
|
+ "error": msg,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// GenerateToken 生成随机令牌(用于JWT密钥)
|
|
|
+func GenerateToken() string {
|
|
|
+ b := make([]byte, 16)
|
|
|
+ rand.Read(b)
|
|
|
+ return hex.EncodeToString(b)
|
|
|
+}
|