package product import ( "archive/zip" "context" "fmt" "io" "os" "path/filepath" "strings" "time" "go.uber.org/zap" ) // UIComponentFileService UI组件文件服务接口 type UIComponentFileService interface { // 上传并解压UI组件文件 UploadAndExtract(ctx context.Context, componentID, componentCode string, file io.Reader, filename string) error // 批量上传UI组件文件(支持文件夹结构) UploadMultipleFiles(ctx context.Context, componentID, componentCode string, files []io.Reader, filenames []string, paths []string) error // 根据组件编码创建文件夹 CreateFolderByCode(componentCode string) (string, error) // 删除组件文件夹 DeleteFolder(folderPath string) error // 检查文件夹是否存在 FolderExists(folderPath string) bool // 获取文件夹内容 GetFolderContent(folderPath string) ([]FileInfo, error) } // FileInfo 文件信息 type FileInfo struct { Name string `json:"name"` Path string `json:"path"` Size int64 `json:"size"` Type string `json:"type"` // "file" or "folder" Modified time.Time `json:"modified"` } // UIComponentFileServiceImpl UI组件文件服务实现 type UIComponentFileServiceImpl struct { basePath string logger *zap.Logger } // NewUIComponentFileService 创建UI组件文件服务 func NewUIComponentFileService(basePath string, logger *zap.Logger) UIComponentFileService { // 确保基础路径存在 if err := os.MkdirAll(basePath, 0755); err != nil { logger.Error("创建基础存储目录失败", zap.Error(err), zap.String("path", basePath)) } return &UIComponentFileServiceImpl{ basePath: basePath, logger: logger, } } // UploadAndExtract 上传并解压UI组件文件 func (s *UIComponentFileServiceImpl) UploadAndExtract(ctx context.Context, componentID, componentCode string, file io.Reader, filename string) error { // 直接使用基础路径作为文件夹路径,不再创建组件编码子文件夹 folderPath := s.basePath // 确保基础目录存在 if err := os.MkdirAll(folderPath, 0755); err != nil { return fmt.Errorf("创建基础目录失败: %w", err) } // 保存上传的文件 filePath := filepath.Join(folderPath, filename) savedFile, err := os.Create(filePath) if err != nil { return fmt.Errorf("创建文件失败: %w", err) } defer savedFile.Close() // 复制文件内容 if _, err := io.Copy(savedFile, file); err != nil { // 删除部分写入的文件 _ = os.Remove(filePath) return fmt.Errorf("保存文件失败: %w", err) } // 仅对ZIP文件执行解压逻辑 if strings.HasSuffix(strings.ToLower(filename), ".zip") { // 解压文件到基础目录 if err := s.extractZipFile(filePath, folderPath); err != nil { // 删除ZIP文件 _ = os.Remove(filePath) return fmt.Errorf("解压文件失败: %w", err) } // 删除ZIP文件 _ = os.Remove(filePath) s.logger.Info("UI组件文件上传并解压成功", zap.String("componentID", componentID), zap.String("componentCode", componentCode), zap.String("folderPath", folderPath)) } else { s.logger.Info("UI组件文件上传成功(未解压)", zap.String("componentID", componentID), zap.String("componentCode", componentCode), zap.String("filePath", filePath)) } return nil } // UploadMultipleFiles 批量上传UI组件文件(支持文件夹结构) func (s *UIComponentFileServiceImpl) UploadMultipleFiles(ctx context.Context, componentID, componentCode string, files []io.Reader, filenames []string, paths []string) error { // 直接使用基础路径作为文件夹路径,不再创建组件编码子文件夹 folderPath := s.basePath // 确保基础目录存在 if err := os.MkdirAll(folderPath, 0755); err != nil { return fmt.Errorf("创建基础目录失败: %w", err) } // 处理每个文件 for i, file := range files { filename := filenames[i] path := paths[i] // 如果有路径信息,创建对应的子文件夹 if path != "" && path != filename { // 获取文件所在目录 dir := filepath.Dir(path) if dir != "." { // 创建子文件夹 subDirPath := filepath.Join(folderPath, dir) if err := os.MkdirAll(subDirPath, 0755); err != nil { return fmt.Errorf("创建子文件夹失败: %w", err) } } } // 确定文件保存路径 var filePath string if path != "" && path != filename { // 有路径信息,使用完整路径 filePath = filepath.Join(folderPath, path) } else { // 没有路径信息,直接保存在根目录 filePath = filepath.Join(folderPath, filename) } // 保存上传的文件 savedFile, err := os.Create(filePath) if err != nil { return fmt.Errorf("创建文件失败: %w", err) } defer savedFile.Close() // 复制文件内容 if _, err := io.Copy(savedFile, file); err != nil { // 删除部分写入的文件 _ = os.Remove(filePath) return fmt.Errorf("保存文件失败: %w", err) } // 对ZIP文件执行解压逻辑 if strings.HasSuffix(strings.ToLower(filename), ".zip") { // 确定解压目录 var extractDir string if path != "" && path != filename { // 有路径信息,解压到对应目录 dir := filepath.Dir(path) if dir != "." { extractDir = filepath.Join(folderPath, dir) } else { extractDir = folderPath } } else { // 没有路径信息,解压到根目录 extractDir = folderPath } // 解压文件 if err := s.extractZipFile(filePath, extractDir); err != nil { // 删除ZIP文件 _ = os.Remove(filePath) return fmt.Errorf("解压文件失败: %w", err) } // 删除ZIP文件 _ = os.Remove(filePath) s.logger.Info("UI组件文件上传并解压成功", zap.String("componentID", componentID), zap.String("componentCode", componentCode), zap.String("filePath", filePath), zap.String("extractDir", extractDir)) } else { s.logger.Info("UI组件文件上传成功(未解压)", zap.String("componentID", componentID), zap.String("componentCode", componentCode), zap.String("filePath", filePath)) } } return nil } // CreateFolderByCode 根据组件编码创建文件夹 func (s *UIComponentFileServiceImpl) CreateFolderByCode(componentCode string) (string, error) { folderPath := filepath.Join(s.basePath, componentCode) // 创建文件夹(如果不存在) if err := os.MkdirAll(folderPath, 0755); err != nil { return "", fmt.Errorf("创建文件夹失败: %w", err) } return folderPath, nil } // DeleteFolder 删除组件文件夹 func (s *UIComponentFileServiceImpl) DeleteFolder(folderPath string) error { if !s.FolderExists(folderPath) { return nil // 文件夹不存在,不视为错误 } if err := os.RemoveAll(folderPath); err != nil { return fmt.Errorf("删除文件夹失败: %w", err) } s.logger.Info("删除组件文件夹成功", zap.String("folderPath", folderPath)) return nil } // FolderExists 检查文件夹是否存在 func (s *UIComponentFileServiceImpl) FolderExists(folderPath string) bool { info, err := os.Stat(folderPath) if err != nil { return false } return info.IsDir() } // GetFolderContent 获取文件夹内容 func (s *UIComponentFileServiceImpl) GetFolderContent(folderPath string) ([]FileInfo, error) { var files []FileInfo err := filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 跳过根目录 if path == folderPath { return nil } // 获取相对路径 relPath, err := filepath.Rel(folderPath, path) if err != nil { return err } fileType := "file" if info.IsDir() { fileType = "folder" } files = append(files, FileInfo{ Name: info.Name(), Path: relPath, Size: info.Size(), Type: fileType, Modified: info.ModTime(), }) return nil }) if err != nil { return nil, fmt.Errorf("扫描文件夹失败: %w", err) } return files, nil } // extractZipFile 解压ZIP文件 func (s *UIComponentFileServiceImpl) extractZipFile(zipPath, destPath string) error { reader, err := zip.OpenReader(zipPath) if err != nil { return fmt.Errorf("打开ZIP文件失败: %w", err) } defer reader.Close() for _, file := range reader.File { path := filepath.Join(destPath, file.Name) // 防止路径遍历攻击 if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(destPath)+string(os.PathSeparator)) { return fmt.Errorf("无效的文件路径: %s", file.Name) } if file.FileInfo().IsDir() { // 创建目录 if err := os.MkdirAll(path, file.Mode()); err != nil { return fmt.Errorf("创建目录失败: %w", err) } continue } // 创建文件 fileReader, err := file.Open() if err != nil { return fmt.Errorf("打开ZIP内文件失败: %w", err) } // 确保父目录存在 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { fileReader.Close() return fmt.Errorf("创建父目录失败: %w", err) } destFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) if err != nil { fileReader.Close() return fmt.Errorf("创建目标文件失败: %w", err) } _, err = io.Copy(destFile, fileReader) fileReader.Close() destFile.Close() if err != nil { return fmt.Errorf("写入文件失败: %w", err) } } return nil }