Files
tyapi-server/internal/shared/component_report/zip_generator.go

266 lines
6.9 KiB
Go
Raw Normal View History

2025-12-19 17:05:09 +08:00
package component_report
import (
"archive/zip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"go.uber.org/zap"
)
// ZipGenerator ZIP文件生成器
type ZipGenerator struct {
logger *zap.Logger
}
// NewZipGenerator 创建ZIP文件生成器
func NewZipGenerator(logger *zap.Logger) *ZipGenerator {
return &ZipGenerator{
logger: logger,
}
}
// GenerateZipFile 生成ZIP文件包含 example.json 和匹配的组件文件
// productID: 产品ID
// subProductCodes: 子产品编号列表(如果为空,则处理所有子产品)
// exampleJSONGenerator: 示例JSON生成器
// outputPath: 输出ZIP文件路径如果为空则使用默认路径
func (g *ZipGenerator) GenerateZipFile(
ctx context.Context,
productID string,
subProductCodes []string,
exampleJSONGenerator *ExampleJSONGenerator,
outputPath string,
) (string, error) {
// 1. 生成 example.json 内容
exampleJSON, err := exampleJSONGenerator.GenerateExampleJSON(ctx, productID, subProductCodes)
if err != nil {
return "", fmt.Errorf("生成example.json失败: %w", err)
}
// 2. 确定输出路径
if outputPath == "" {
// 使用默认路径storage/component-reports/{productID}.zip
outputDir := "storage/component-reports"
if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("创建输出目录失败: %w", err)
}
outputPath = filepath.Join(outputDir, fmt.Sprintf("%s_example.json.zip", productID))
}
// 3. 创建ZIP文件
zipFile, err := os.Create(outputPath)
if err != nil {
return "", fmt.Errorf("创建ZIP文件失败: %w", err)
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
// 4. 添加 example.json 到 public 目录
exampleWriter, err := zipWriter.Create("public/example.json")
if err != nil {
return "", fmt.Errorf("创建example.json文件失败: %w", err)
}
_, err = exampleWriter.Write(exampleJSON)
if err != nil {
return "", fmt.Errorf("写入example.json失败: %w", err)
}
// 5. 添加整个 src 目录,但过滤 ui 目录下的文件
srcBasePath := filepath.Join("resources", "Pure Component", "src")
uiBasePath := filepath.Join(srcBasePath, "ui")
// 收集所有匹配的组件名称(文件夹名或文件名)
matchedNames := make(map[string]bool)
for _, productCode := range subProductCodes {
path, _, err := exampleJSONGenerator.MatchProductCodeToPath(ctx, productCode)
if err == nil && path != "" {
// 获取组件名称(文件夹名或文件名)
componentName := filepath.Base(path)
matchedNames[componentName] = true
}
}
// 遍历整个 src 目录
err = filepath.Walk(srcBasePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 计算相对于 src 的路径
relPath, err := filepath.Rel(srcBasePath, path)
if err != nil {
return err
}
// 转换为ZIP路径格式
zipPath := filepath.ToSlash(filepath.Join("src", relPath))
// 检查是否在 ui 目录下
uiRelPath, err := filepath.Rel(uiBasePath, path)
isInUIDir := err == nil && !strings.HasPrefix(uiRelPath, "..")
if isInUIDir {
// 如果是 ui 目录本身,直接添加
if uiRelPath == "." || uiRelPath == "" {
if info.IsDir() {
_, err = zipWriter.Create(zipPath + "/")
return err
}
return nil
}
// 获取文件/文件夹名称
fileName := info.Name()
// 检查是否应该保留:
// 1. CBehaviorRiskScan.vue 文件(无论在哪里)
// 2. 匹配到的组件文件夹/文件
shouldInclude := false
// 检查是否是 CBehaviorRiskScan.vue
if fileName == "CBehaviorRiskScan.vue" {
shouldInclude = true
} else {
// 检查是否是匹配的组件(检查组件名称)
if matchedNames[fileName] {
shouldInclude = true
} else {
// 检查是否在匹配的组件文件夹内
// 获取相对于 ui 的路径的第一部分(组件文件夹名)
parts := strings.Split(filepath.ToSlash(uiRelPath), "/")
if len(parts) > 0 && parts[0] != "" && parts[0] != "." {
if matchedNames[parts[0]] {
shouldInclude = true
}
}
}
}
if !shouldInclude {
// 跳过不匹配的文件/文件夹
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
}
// 如果是目录,创建目录项
if info.IsDir() {
_, err = zipWriter.Create(zipPath + "/")
return err
}
// 添加文件
return g.AddFileToZip(zipWriter, path, zipPath)
})
if err != nil {
g.logger.Warn("添加src目录失败", zap.Error(err))
}
g.logger.Info("成功生成ZIP文件",
zap.String("product_id", productID),
zap.String("output_path", outputPath),
zap.Int("example_json_size", len(exampleJSON)),
zap.Int("sub_product_count", len(subProductCodes)),
)
return outputPath, nil
}
// AddFileToZip 添加文件到ZIP
func (g *ZipGenerator) AddFileToZip(zipWriter *zip.Writer, filePath string, zipPath string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("打开文件失败: %w", err)
}
defer file.Close()
writer, err := zipWriter.Create(zipPath)
if err != nil {
return fmt.Errorf("创建ZIP文件项失败: %w", err)
}
_, err = io.Copy(writer, file)
if err != nil {
return fmt.Errorf("复制文件内容失败: %w", err)
}
return nil
}
// AddFolderToZip 递归添加文件夹到ZIP
func (g *ZipGenerator) AddFolderToZip(zipWriter *zip.Writer, folderPath string, basePath string) error {
return filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
// 计算相对路径
relPath, err := filepath.Rel(basePath, path)
if err != nil {
return err
}
// 转换为ZIP路径格式使用正斜杠
zipPath := filepath.ToSlash(relPath)
return g.AddFileToZip(zipWriter, path, zipPath)
})
}
// AddFileToZipWithTarget 将单个文件添加到ZIP的指定目标路径
func (g *ZipGenerator) AddFileToZipWithTarget(zipWriter *zip.Writer, filePath string, targetPath string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("打开文件失败: %w", err)
}
defer file.Close()
writer, err := zipWriter.Create(filepath.ToSlash(targetPath))
if err != nil {
return fmt.Errorf("创建ZIP文件项失败: %w", err)
}
_, err = io.Copy(writer, file)
if err != nil {
return fmt.Errorf("复制文件内容失败: %w", err)
}
return nil
}
// AddFolderToZipWithPrefix 递归添加文件夹到ZIP并在ZIP内添加路径前缀
func (g *ZipGenerator) AddFolderToZipWithPrefix(zipWriter *zip.Writer, folderPath string, basePath string, prefix string) error {
return filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
relPath, err := filepath.Rel(basePath, path)
if err != nil {
return err
}
zipPath := filepath.ToSlash(filepath.Join(prefix, relPath))
return g.AddFileToZip(zipWriter, path, zipPath)
})
}