Files
tyapi-server/internal/application/product/ui_component_file_service.go
2025-12-23 17:17:41 +08:00

460 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
// 根据组件编码和上传时间智能删除组件相关文件
DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) 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 {
// 记录尝试删除的文件夹路径
s.logger.Info("尝试删除文件夹", zap.String("folderPath", folderPath))
// 获取文件夹信息,用于调试
if info, err := os.Stat(folderPath); err == nil {
s.logger.Info("文件夹信息",
zap.String("folderPath", folderPath),
zap.Bool("isDir", info.IsDir()),
zap.Int64("size", info.Size()),
zap.Time("modTime", info.ModTime()))
} else {
s.logger.Error("获取文件夹信息失败",
zap.Error(err),
zap.String("folderPath", folderPath))
}
// 检查文件夹是否存在
if !s.FolderExists(folderPath) {
s.logger.Info("文件夹不存在", zap.String("folderPath", folderPath))
return nil // 文件夹不存在,不视为错误
}
// 尝试删除文件夹
s.logger.Info("开始删除文件夹", zap.String("folderPath", folderPath))
if err := os.RemoveAll(folderPath); err != nil {
s.logger.Error("删除文件夹失败",
zap.Error(err),
zap.String("folderPath", folderPath))
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
}
// DeleteFilesByComponentCode 根据组件编码和上传时间智能删除组件相关文件
func (s *UIComponentFileServiceImpl) DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error {
// 记录基础路径和组件编码
s.logger.Info("开始删除组件文件",
zap.String("basePath", s.basePath),
zap.String("componentCode", componentCode),
zap.Any("uploadTime", uploadTime))
// 1. 查找名为组件编码的文件夹
componentDir := filepath.Join(s.basePath, componentCode)
s.logger.Info("检查组件文件夹", zap.String("componentDir", componentDir))
if s.FolderExists(componentDir) {
s.logger.Info("找到组件文件夹,开始删除", zap.String("componentDir", componentDir))
if err := s.DeleteFolder(componentDir); err != nil {
s.logger.Error("删除组件文件夹失败",
zap.Error(err),
zap.String("componentCode", componentCode),
zap.String("componentDir", componentDir))
return fmt.Errorf("删除组件文件夹失败: %w", err)
}
s.logger.Info("成功删除组件文件夹", zap.String("componentCode", componentCode))
return nil
} else {
s.logger.Info("组件文件夹不存在", zap.String("componentDir", componentDir))
}
// 2. 查找文件名包含组件编码的文件
pattern := filepath.Join(s.basePath, "*"+componentCode+"*")
s.logger.Info("查找匹配文件", zap.String("pattern", pattern))
files, err := filepath.Glob(pattern)
if err != nil {
s.logger.Error("查找组件文件失败",
zap.Error(err),
zap.String("pattern", pattern))
return fmt.Errorf("查找组件文件失败: %w", err)
}
s.logger.Info("找到匹配文件",
zap.Strings("files", files),
zap.Int("count", len(files)))
// 3. 如果没有上传时间,删除所有匹配的文件
if uploadTime == nil {
for _, file := range files {
if err := os.Remove(file); err != nil {
s.logger.Error("删除文件失败", zap.String("file", file), zap.Error(err))
} else {
s.logger.Info("成功删除文件", zap.String("file", file))
}
}
return nil
}
// 4. 如果有上传时间,根据文件修改时间和上传时间的匹配度来删除文件
var deletedFiles []string
for _, file := range files {
// 获取文件信息
fileInfo, err := os.Stat(file)
if err != nil {
s.logger.Warn("获取文件信息失败", zap.String("file", file), zap.Error(err))
continue
}
// 计算文件修改时间与上传时间的差异(以秒为单位)
timeDiff := fileInfo.ModTime().Sub(*uploadTime).Seconds()
// 如果时间差在60秒内认为是最匹配的文件
if timeDiff < 60 && timeDiff > -60 {
if err := os.Remove(file); err != nil {
s.logger.Warn("删除文件失败", zap.String("file", file), zap.Error(err))
} else {
deletedFiles = append(deletedFiles, file)
s.logger.Info("成功删除文件", zap.String("file", file),
zap.Time("uploadTime", *uploadTime),
zap.Time("fileModTime", fileInfo.ModTime()))
}
}
}
// 如果没有找到匹配的文件,记录警告但返回成功
if len(deletedFiles) == 0 && len(files) > 0 {
s.logger.Warn("没有找到匹配时间戳的文件",
zap.String("componentCode", componentCode),
zap.Time("uploadTime", *uploadTime),
zap.Int("foundFiles", len(files)))
}
return nil
}