package httpfile import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "ftp-server/config" ) // Server represents the HTTP file server type Server struct { config *config.Config } // NewServer creates a new HTTP file server func NewServer(cfg *config.Config) *Server { return &Server{config: cfg} } // Start starts the HTTP file server func (s *Server) Start() error { if !s.config.HTTPFile.Enable { return nil } rootDir := s.config.HTTPFile.RootDir if err := os.MkdirAll(rootDir, 0755); err != nil { return fmt.Errorf("failed to create root directory: %v", err) } mux := http.NewServeMux() // Serve HTML page mux.HandleFunc("/", s.handleFileBrowser) // API endpoints mux.HandleFunc("/api/list", s.apiListDir) if s.config.HTTPFile.Upload { mux.HandleFunc("/api/upload", s.apiUpload) mux.HandleFunc("/api/create-folder", s.apiCreateFolder) } mux.HandleFunc("/api/download", s.apiDownload) mux.HandleFunc("/api/delete", s.apiDelete) addr := fmt.Sprintf("%s:%d", s.config.HTTPFile.Host, s.config.HTTPFile.Port) log.Printf("HTTP file server listening on http://%s", addr) return http.ListenAndServe(addr, mux) } func (s *Server) handleFileBrowser(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(fileBrowserHTML)) } func (s *Server) apiListDir(w http.ResponseWriter, r *http.Request) { dir := r.URL.Query().Get("path") if dir == "" { dir = "/" } realPath := s.toRealPath(dir) entries, err := os.ReadDir(realPath) if err != nil { http.Error(w, `{"error":"Cannot read directory"}`, http.StatusInternalServerError) return } var files []map[string]interface{} for _, entry := range entries { info, _ := entry.Info() modTime := info.ModTime().Format("2006-01-02 15:04:05") size := info.Size() file := map[string]interface{}{ "name": entry.Name(), "modTime": modTime, "size": formatSize(size), "isDir": entry.IsDir(), } if !entry.IsDir() { file["ext"] = strings.ToLower(filepath.Ext(entry.Name())) } files = append(files, file) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "files": files, }) } func (s *Server) apiDownload(w http.ResponseWriter, r *http.Request) { file := r.URL.Query().Get("file") if file == "" { http.Error(w, "Missing file parameter", http.StatusBadRequest) return } realPath := s.toRealPath(file) if info, err := os.Stat(realPath); err != nil || info.IsDir() { http.Error(w, "File not found", http.StatusNotFound) return } http.ServeFile(w, r, realPath) log.Printf("HTTP download: %s from %s", file, r.RemoteAddr) } func (s *Server) apiUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse multipart form (max 500MB) err := r.ParseMultipartForm(500 << 20) if err != nil { http.Error(w, `{"error":"Failed to parse upload"}`, http.StatusBadRequest) return } dir := r.FormValue("path") if dir == "" { dir = "/" } files := r.MultipartForm.File["files"] if len(files) == 0 { http.Error(w, `{"error":"No files uploaded"}`, http.StatusBadRequest) return } realDir := s.toRealPath(dir) for _, fileHeader := range files { file, err := fileHeader.Open() if err != nil { continue } destPath := filepath.Join(realDir, fileHeader.Filename) destFile, err := os.Create(destPath) if err != nil { file.Close() continue } io.Copy(destFile, file) destFile.Close() file.Close() log.Printf("HTTP upload: %s (%d bytes) from %s", fileHeader.Filename, fileHeader.Size, r.RemoteAddr) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func (s *Server) apiCreateFolder(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed) return } var req struct { Path string `json:"path"` Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest) return } if req.Name == "" { http.Error(w, `{"error":"Folder name required"}`, http.StatusBadRequest) return } realPath := filepath.Join(s.toRealPath(req.Path), req.Name) if err := os.MkdirAll(realPath, 0755); err != nil { http.Error(w, `{"error":"Failed to create folder"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func (s *Server) apiDelete(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed) return } var req struct { Path string `json:"path"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest) return } realPath := s.toRealPath(req.Path) if err := os.RemoveAll(realPath); err != nil { http.Error(w, `{"error":"Delete failed"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func (s *Server) toRealPath(httpPath string) string { rootDir := s.config.HTTPFile.RootDir cleanPath := strings.TrimPrefix(httpPath, "/") return filepath.Join(rootDir, cleanPath) } func formatSize(size int64) string { switch { case size < 1024: return fmt.Sprintf("%d B", size) case size < 1024*1024: return fmt.Sprintf("%.1f KB", float64(size)/1024) case size < 1024*1024*1024: return fmt.Sprintf("%.1f MB", float64(size)/(1024*1024)) default: return fmt.Sprintf("%.1f GB", float64(size)/(1024*1024*1024)) } } // fileBrowserHTML is the embedded HTML page const fileBrowserHTML = `
在线浏览、下载、上传文件
| 名称 | 大小 | 修改时间 | 操作 |
|---|