package main import ( "archive/zip" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "github.com/fsnotify/fsnotify" "github.com/gin-gonic/gin" ) type FileInfo struct { Name string `json:"name"` Path string `json:"path"` IsDir bool `json:"isDir"` Size int64 `json:"size"` ModTime time.Time `json:"modTime"` CanPreview bool `json:"canPreview"` CanDownload bool `json:"canDownload"` } type UploadResponse struct { Success bool `json:"success"` Message string `json:"message"` } type MoveRequest struct { SrcPath string `json:"srcPath"` DestPath string `json:"destPath"` } type RenameRequest struct { OldPath string `json:"oldPath"` NewName string `json:"newName"` } type CreateDirRequest struct { Path string `json:"path"` Name string `json:"name"` } type ErrorResponse struct { Error string `json:"error"` } type WatchEvent struct { Type string `json:"type"` Path string `json:"path"` Name string `json:"name"` } var ( rootDir string watcher *fsnotify.Watcher watchChan chan WatchEvent ) func init() { watchChan = make(chan WatchEvent, 100) } func getFileType(filename string) string { ext := strings.ToLower(filepath.Ext(filename)) switch ext { case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp": return "image" case ".txt", ".md", ".json", ".xml", ".html", ".css", ".js", ".go", ".py", ".java", ".c", ".cpp", ".h", ".sh": return "text" case ".pdf": return "pdf" default: return "other" } } func formatFileSize(size int64) string { const unit = 1024 if size < unit { return fmt.Sprintf("%d B", size) } div, exp := int64(unit), 0 for n := size / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp]) } func listFiles(c *gin.Context) { path := c.Query("path") if path == "" { path = "." } fullPath := filepath.Join(rootDir, path) if !strings.HasPrefix(fullPath, rootDir) { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"}) return } file, err := os.Open(fullPath) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } defer file.Close() entries, err := file.Readdir(-1) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } var files []FileInfo for _, entry := range entries { name := entry.Name() if name == "." || name == ".." || strings.HasPrefix(name, ".") { continue } entryPath := filepath.Join(path, name) fileType := getFileType(name) canPreview := false if !entry.IsDir() { canPreview = fileType == "image" || fileType == "text" || fileType == "pdf" } files = append(files, FileInfo{ Name: name, Path: entryPath, IsDir: entry.IsDir(), Size: entry.Size(), ModTime: entry.ModTime(), CanPreview: canPreview, CanDownload: true, }) } c.JSON(http.StatusOK, gin.H{ "files": files, "path": path, }) } func previewFile(c *gin.Context) { path := c.Query("path") if path == "" { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "缺少path参数"}) return } fullPath := filepath.Join(rootDir, path) if !strings.HasPrefix(fullPath, rootDir) { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"}) return } file, err := os.Open(fullPath) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } defer file.Close() info, err := file.Stat() if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } fileType := getFileType(info.Name()) if fileType == "image" { data, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } contentType := http.DetectContentType(data) c.Data(http.StatusOK, contentType, data) } else if fileType == "text" { data, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } c.Data(http.StatusOK, "text/plain; charset=utf-8", data) } else if fileType == "pdf" { data, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } c.Data(http.StatusOK, "application/pdf", data) } else { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "不支持预览此类型文件"}) } } func downloadFile(c *gin.Context) { pathsParam := c.Query("paths") if pathsParam == "" { path := c.Query("path") if path == "" { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "缺少path参数"}) return } downloadSingle(c, path) return } var paths []string if err := json.Unmarshal([]byte(pathsParam), &paths); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的paths参数"}) return } if len(paths) == 1 { downloadSingle(c, paths[0]) return } downloadMultiple(c, paths) } func downloadSingle(c *gin.Context, path string) { fullPath := filepath.Join(rootDir, path) if !strings.HasPrefix(fullPath, rootDir) { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"}) return } file, err := os.Open(fullPath) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } defer file.Close() info, err := file.Stat() if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } if info.IsDir() { downloadDirectory(c, path) return } c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", info.Name())) c.Header("Content-Type", "application/octet-stream") c.Header("Content-Length", fmt.Sprintf("%d", info.Size())) io.Copy(c.Writer, file) } func downloadDirectory(c *gin.Context, path string) { fullPath := filepath.Join(rootDir, path) if !strings.HasPrefix(fullPath, rootDir) { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"}) return } dirName := filepath.Base(path) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip", dirName)) c.Header("Content-Type", "application/zip") c.Header("Content-Transfer-Encoding", "binary") zw := zip.NewWriter(c.Writer) defer zw.Close() filepath.Walk(fullPath, func(filePath string, info os.FileInfo, err error) error { if err != nil { return err } relPath, _ := filepath.Rel(rootDir, filePath) if relPath == "." { return nil } header, err := zip.FileInfoHeader(info) if err != nil { return err } header.Name = relPath header.Method = zip.Deflate if info.IsDir() { header.Name += "/" } else { writer, err := zw.CreateHeader(header) if err != nil { return err } if !info.IsDir() { file, err := os.Open(filePath) if err != nil { return err } defer file.Close() io.Copy(writer, file) } } return nil }) } func downloadMultiple(c *gin.Context, paths []string) { c.Header("Content-Disposition", "attachment; filename=files.zip") c.Header("Content-Type", "application/zip") c.Header("Content-Transfer-Encoding", "binary") zw := zip.NewWriter(c.Writer) defer zw.Close() for _, path := range paths { fullPath := filepath.Join(rootDir, path) if !strings.HasPrefix(fullPath, rootDir) { continue } info, err := os.Stat(fullPath) if err != nil { continue } if info.IsDir() { filepath.Walk(fullPath, func(filePath string, fileInfo os.FileInfo, err error) error { if err != nil { return nil } relPath, _ := filepath.Rel(rootDir, filePath) header, err := zip.FileInfoHeader(fileInfo) if err != nil { return nil } header.Name = relPath header.Method = zip.Deflate if fileInfo.IsDir() { header.Name += "/" } else { writer, err := zw.CreateHeader(header) if err != nil { return nil } file, err := os.Open(filePath) if err != nil { return nil } defer file.Close() io.Copy(writer, file) } return nil }) } else { file, err := os.Open(fullPath) if err != nil { continue } defer file.Close() header, err := zip.FileInfoHeader(info) if err != nil { continue } header.Name = filepath.Base(path) header.Method = zip.Deflate writer, err := zw.CreateHeader(header) if err != nil { continue } io.Copy(writer, file) } } } func uploadFile(c *gin.Context) { path := c.DefaultQuery("path", "") if path == "" { path = "." } dzuuid := c.PostForm("dzuuid") chunkIndex := c.PostForm("dzchunkindex") totalChunkCount := c.PostForm("dztotalchunkcount") fileName := c.PostForm("dzanonymousfilename") isChunked := dzuuid != "" && chunkIndex != "" tmpDir := filepath.Join(rootDir, ".tmp", "chunks") if err := os.MkdirAll(tmpDir, 0755); err != nil { c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Message: err.Error(), }) return } file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Message: err.Error(), }) return } defer file.Close() targetFileName := header.Filename if fileName != "" { targetFileName = fileName } fullPath := filepath.Join(rootDir, path, targetFileName) if !strings.HasPrefix(fullPath, rootDir) { c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Message: "无效的路径", }) return } if isChunked { idx := 0 fmt.Sscanf(chunkIndex, "%d", &idx) total := 0 if totalChunkCount != "" { fmt.Sscanf(totalChunkCount, "%d", &total) } chunkDir := filepath.Join(tmpDir, dzuuid) if err := os.MkdirAll(chunkDir, 0755); err != nil { c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Message: err.Error(), }) return } chunkPath := filepath.Join(chunkDir, fmt.Sprintf("chunk_%d", idx)) outFile, err := os.Create(chunkPath) if err != nil { c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Message: err.Error(), }) return } defer outFile.Close() io.Copy(outFile, file) if idx == total-1 && total > 0 { finalFile, err := os.Create(fullPath) if err != nil { os.RemoveAll(chunkDir) c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Message: err.Error(), }) return } defer finalFile.Close() for i := 0; i < total; i++ { chunkPath := filepath.Join(chunkDir, fmt.Sprintf("chunk_%d", i)) chunkFile, err := os.Open(chunkPath) if err != nil { finalFile.Close() os.RemoveAll(chunkDir) c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Message: err.Error(), }) return } io.Copy(finalFile, chunkFile) chunkFile.Close() os.Remove(chunkPath) } os.RemoveAll(chunkDir) watchChan <- WatchEvent{Type: "create", Path: fullPath, Name: targetFileName} c.JSON(http.StatusOK, UploadResponse{ Success: true, Message: "上传成功", }) } else { c.JSON(http.StatusOK, UploadResponse{ Success: true, Message: "分块上传成功", }) } } else { outFile, err := os.Create(fullPath) if err != nil { c.JSON(http.StatusBadRequest, UploadResponse{ Success: false, Message: err.Error(), }) return } defer outFile.Close() io.Copy(outFile, file) watchChan <- WatchEvent{Type: "create", Path: fullPath, Name: targetFileName} c.JSON(http.StatusOK, UploadResponse{ Success: true, Message: "上传成功", }) } } func deleteFiles(c *gin.Context) { var paths []string if err := c.ShouldBindJSON(&paths); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的请求体"}) return } for _, path := range paths { fullPath := filepath.Join(rootDir, path) if !strings.HasPrefix(fullPath, rootDir) { continue } info, err := os.Stat(fullPath) if err != nil { continue } if info.IsDir() { os.RemoveAll(fullPath) } else { os.Remove(fullPath) } watchChan <- WatchEvent{Type: "delete", Path: fullPath, Name: filepath.Base(path)} } c.JSON(http.StatusOK, gin.H{"success": true}) } func moveFile(c *gin.Context) { var req MoveRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的请求体"}) return } srcPath := filepath.Join(rootDir, req.SrcPath) destPath := filepath.Join(rootDir, req.DestPath) if !strings.HasPrefix(srcPath, rootDir) || !strings.HasPrefix(destPath, rootDir) { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"}) return } if err := os.Rename(srcPath, destPath); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } watchChan <- WatchEvent{Type: "move", Path: destPath, Name: filepath.Base(destPath)} c.JSON(http.StatusOK, gin.H{"success": true}) } func renameFile(c *gin.Context) { var req RenameRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的请求体"}) return } srcPath := filepath.Join(rootDir, req.OldPath) newPath := filepath.Join(rootDir, filepath.Dir(req.OldPath), req.NewName) if !strings.HasPrefix(srcPath, rootDir) || !strings.HasPrefix(newPath, rootDir) { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"}) return } if err := os.Rename(srcPath, newPath); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } watchChan <- WatchEvent{Type: "rename", Path: newPath, Name: req.NewName} c.JSON(http.StatusOK, gin.H{"success": true}) } func createDir(c *gin.Context) { var req CreateDirRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的请求体"}) return } fullPath := filepath.Join(rootDir, req.Path, req.Name) if !strings.HasPrefix(fullPath, rootDir) { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"}) return } if err := os.MkdirAll(fullPath, 0755); err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } watchChan <- WatchEvent{Type: "create", Path: fullPath, Name: req.Name} c.JSON(http.StatusOK, gin.H{"success": true}) } func watchFiles(c *gin.Context) { c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("Access-Control-Allow-Origin", "*") c.Writer.Flush() ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case event := <-watchChan: data, _ := json.Marshal(event) c.SSEvent("message", string(data)) c.Writer.Flush() case <-ticker.C: c.SSEvent("ping", time.Now().Format("2006-01-02 15:04:05")) c.Writer.Flush() case <-c.Request.Context().Done(): return } } } func startWatcher() { var err error watcher, err = fsnotify.NewWatcher() if err != nil { fmt.Printf("警告: 无法创建文件监控器: %v\n", err) return } go func() { for { select { case event, ok := <-watcher.Events: if !ok { return } if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename || event.Op&fsnotify.Chmod == fsnotify.Chmod { relPath, _ := filepath.Rel(rootDir, event.Name) if relPath == "." { continue } eventType := "modify" if event.Op&fsnotify.Create == fsnotify.Create { eventType = "create" } else if event.Op&fsnotify.Remove == fsnotify.Remove { eventType = "delete" } else if event.Op&fsnotify.Rename == fsnotify.Rename { eventType = "rename" } select { case watchChan <- WatchEvent{Type: eventType, Path: event.Name, Name: filepath.Base(event.Name)}: default: } } case err, ok := <-watcher.Errors: if !ok { return } fmt.Printf("监控错误: %v\n", err) } } }() watcher.Add(rootDir) filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { if err == nil && info.IsDir() { watcher.Add(path) } return nil }) } func main() { port := "8080" dir := "./files" for i := 1; i < len(os.Args); i++ { switch os.Args[i] { case "-port": if i+1 < len(os.Args) { port = os.Args[i+1] i++ } case "-root": if i+1 < len(os.Args) { dir = os.Args[i+1] i++ } } } if dir == "" { dir = "." } if !filepath.IsAbs(dir) { cwd, _ := os.Getwd() dir = filepath.Join(cwd, dir) } rootDir = dir if err := os.MkdirAll(rootDir, 0755); err != nil { fmt.Printf("错误: 无法创建根目录: %v\n", err) os.Exit(1) } gin.SetMode(gin.ReleaseMode) router := gin.New() router.Use(gin.Recovery()) router.Use(corsMiddleware()) router.GET("/", func(c *gin.Context) { c.Header("Cache-Control", "no-cache, no-store, must-revalidate") c.Header("Content-Type", "text/html; charset=utf-8") c.Header("X-Content-Type-Options", "nosniff") c.Header("X-Frame-Options", "DENY") c.Header("X-XSS-Protection", "1; mode=block") html, err := os.ReadFile("/var/www/static/index.html") if err != nil { c.String(http.StatusInternalServerError, "无法加载页面") return } c.String(http.StatusOK, "%s", html) }) router.Static("/static", "/var/www/static") api := router.Group("/api") { api.GET("/files", listFiles) api.GET("/preview", previewFile) api.GET("/download", downloadFile) api.POST("/upload", uploadFile) api.DELETE("/files", deleteFiles) api.PUT("/move", moveFile) api.PUT("/rename", renameFile) api.POST("/dir", createDir) api.GET("/watch", watchFiles) } go startWatcher() addr := fmt.Sprintf(":%s", port) fmt.Printf("文件管理器已启动: http://localhost:%s\n", port) fmt.Printf("根目录: %s\n", rootDir) if err := router.Run(addr); err != nil { fmt.Printf("错误: %v\n", err) } } func corsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(http.StatusOK) return } c.Next() } }