package controller import ( "archive/zip" "dashboard/utils" "errors" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "github.com/gin-gonic/gin" ) type FileInfo struct { Name string `json:"name"` IsDir bool `json:"is_dir"` Size int64 `json:"size"` ModTime time.Time `json:"mod_time"` } func FileUploadHandle(c *gin.Context) error { log, _ := utils.GetLogFromContext(c) file, err := c.FormFile("file") if err != nil { log.Sugar().Errorf("Failed to get file: %v", err) return err } targetDir := c.PostForm("target_dir") if targetDir == "" { log.Sugar().Errorf("Target directory not specified") return errors.New("Target directory not specified") } // 防止路径穿越攻击 cleanDir := filepath.Clean(targetDir) if strings.Contains(cleanDir, "..") || !strings.HasPrefix(cleanDir, "/") { log.Sugar().Errorf("Invalid target directory: %s", cleanDir) return errors.New("Invalid target directory") } // 限制文件大小(10MB) const maxFileSize = 10 << 20 // 10MB if file.Size > maxFileSize { log.Sugar().Errorf("File too large: %d bytes", file.Size) return errors.New("File size exceeds 10MB") } // 确保目标目录存在 if err := os.MkdirAll(cleanDir, 0755); err != nil { log.Sugar().Errorf("Failed to create directory %s: %v", cleanDir, err) return err } // 保存文件 targetPath := filepath.Join(cleanDir, file.Filename) if err := c.SaveUploadedFile(file, targetPath); err != nil { log.Sugar().Errorf("Failed to save file to %s: %v", targetPath, err) return err } log.Sugar().Debug("File uploaded successfully to %s", targetPath) c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("File uploaded to %s", targetPath)}) return nil } func FileListHandle(c *gin.Context) error { log, _ := utils.GetLogFromContext(c) dirPath := c.Query("path") if dirPath == "" { log.Sugar().Errorf("Directory path not specified") return errors.New("Directory path not specified") } // 清理路径并规范化 cleanPath := filepath.Clean(dirPath) if !strings.HasPrefix(cleanPath, "/") { log.Sugar().Errorf("Invalid directory path (not absolute): %s", cleanPath) return errors.New("Invalid directory path") } // 防止路径穿越攻击 if strings.Contains(cleanPath, "/../") { log.Sugar().Errorf("Invalid directory path (contains ..): %s", cleanPath) return errors.New("Invalid directory path") } // 读取目录 entries, err := os.ReadDir(cleanPath) if err != nil { log.Sugar().Errorf("Failed to read directory %s: %v", cleanPath, err) return err } var files []FileInfo for _, entry := range entries { info, err := entry.Info() if err != nil { log.Sugar().Errorf("Failed to get info for %s: %v", entry.Name(), err) continue } files = append(files, FileInfo{ Name: entry.Name(), IsDir: entry.IsDir(), Size: info.Size(), ModTime: info.ModTime(), }) } log.Sugar().Debug("Listed files in %s: %d entries", cleanPath, len(files)) c.JSON(http.StatusOK, gin.H{"files": files}) return nil } func FileDownloadHandle(c *gin.Context) error { log, _ := utils.GetLogFromContext(c) filePath := c.Query("path") if filePath == "" { log.Sugar().Errorf("File path not specified") return errors.New("File path not specified") } // 清理路径并规范化 cleanPath := filepath.Clean(filePath) if !strings.HasPrefix(cleanPath, "/") { log.Sugar().Errorf("Invalid file path (not absolute): %s", cleanPath) return errors.New("Invalid file path") } // 防止路径穿越攻击 if strings.Contains(cleanPath, "/../") { log.Sugar().Errorf("Invalid file path (contains ..): %s", cleanPath) return errors.New("Invalid file path") } // 检查路径是否存在 fileInfo, err := os.Stat(cleanPath) if err != nil { log.Sugar().Errorf("Path not found %s: %v", cleanPath, err) return errors.New("Path not found") } if !fileInfo.IsDir() { // 处理文件下载 const maxFileSize = 10 << 20 // 10MB if fileInfo.Size() > maxFileSize { log.Sugar().Errorf("File too large: %d bytes", fileInfo.Size()) return errors.New("File size exceeds 10MB") } c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(cleanPath))) c.Header("Content-Type", "application/octet-stream") c.File(cleanPath) return nil } // 处理目录下载(创建ZIP) tmpFile, err := os.CreateTemp("", "dir-*.zip") if err != nil { log.Sugar().Errorf("Failed to create temp file: %v", err) return errors.New("Failed to create zip file") } defer os.Remove(tmpFile.Name()) // 清理临时文件 zipWriter := zip.NewWriter(tmpFile) totalSize := int64(0) const maxZipSize = 10 << 20 // 10MB err = filepath.Walk(cleanPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 创建ZIP中的文件头 header, err := zip.FileInfoHeader(info) if err != nil { return err } // 设置ZIP中的相对路径 relPath, err := filepath.Rel(cleanPath, path) if err != nil { return err } header.Name = relPath if info.IsDir() { header.Name += "/" } else { header.Method = zip.Deflate totalSize += info.Size() if totalSize > maxZipSize { return fmt.Errorf("directory size exceeds 10MB") } } writer, err := zipWriter.CreateHeader(header) if err != nil { return err } if !info.IsDir() { file, err := os.Open(path) if err != nil { return err } defer file.Close() _, err = io.Copy(writer, file) if err != nil { return err } } return nil }) if err != nil { zipWriter.Close() log.Sugar().Errorf("Failed to create zip for %s: %v", cleanPath, err) return err } if err := zipWriter.Close(); err != nil { log.Sugar().Errorf("Failed to close zip writer: %v", err) return err } // 设置下载头 c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(cleanPath)+".zip")) c.Header("Content-Type", "application/zip") c.File(tmpFile.Name()) log.Sugar().Debug("Directory %s downloaded as zip", cleanPath) return nil }