package service import ( "archive/zip" "crypto/md5" "ego/internal/model" "ego/internal/serializer" "ego/pkg/logger" "fmt" "io" "os" "path/filepath" "strings" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "gorm.io/gorm" ) // SysUploadService 文件上传服务 type SysUploadService struct { Db *gorm.DB } // NewSysUploadService 构建文件上传服务 func NewSysUploadService(db *gorm.DB) *SysUploadService { return &SysUploadService{ Db: db, } } // UploadZip 上传压缩包 func (s *SysUploadService) UploadZip(c *gin.Context) serializer.Response { // 获取上传的文件 file, err := c.FormFile("file") if err != nil { logger.Error(c, "获取上传文件失败!") return serializer.ParamErr("获取上传文件失败!", err) } // 校验文件类型 if !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") { logger.Error(c, "只支持zip格式文件!") return serializer.ParamErr("只支持zip格式文件!", nil) } // 获取文件名(不包含扩展名) filename := strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename)) // 检查目标文件夹是否存在 targetDir := filepath.Join("/home", filename) if _, err := os.Stat(targetDir); !os.IsNotExist(err) { logger.Error(c, "目标文件夹已存在!") return serializer.ParamErr("目标文件夹已存在!", nil) } // 打开上传的文件 src, err := file.Open() if err != nil { logger.Error(c, "打开上传文件失败!") return serializer.ParamErr("打开上传文件失败!", err) } defer src.Close() // 创建临时文件 tempFile, err := os.CreateTemp("", "upload-*.zip") if err != nil { logger.Error(c, "创建临时文件失败!") return serializer.ParamErr("创建临时文件失败!", err) } defer os.Remove(tempFile.Name()) defer tempFile.Close() // 复制文件内容到临时文件 if _, err := io.Copy(tempFile, src); err != nil { logger.Error(c, "复制文件内容失败!") return serializer.ParamErr("复制文件内容失败!", err) } // 校验压缩包是否包含index.html hasIndexHtml, err := s.validateZipFile(tempFile.Name()) if err != nil { logger.Error(c, "校验压缩包失败!") return serializer.ParamErr("校验压缩包失败!", err) } if !hasIndexHtml { logger.Error(c, "压缩包必须包含index.html文件!") return serializer.ParamErr("压缩包必须包含index.html文件!", nil) } // 创建目标目录 if err := os.MkdirAll(targetDir, 0755); err != nil { logger.Error(c, "创建目标目录失败!") return serializer.ParamErr("创建目标目录失败!", err) } // 解压文件 if err := s.extractZip(tempFile.Name(), targetDir); err != nil { // 如果解压失败,删除已创建的目录 os.RemoveAll(targetDir) logger.Error(c, "解压文件失败!") return serializer.ParamErr("解压文件失败!", err) } // 返回域名格式的结果 domain := fmt.Sprintf("%s.unbug.cn", filename) // 创建部署文件记录 deployFile := model.SysDeployFile{ FileName: file.Filename, ProjectName: filename, Domain: domain, DeployPath: targetDir, FileSize: file.Size, Status: model.DeployFileStatusNormal, DeployStatus: model.DeployStatusSuccess, Description: fmt.Sprintf("自动部署项目: %s", filename), } // 生成部署ID if id, err := SysSequenceServiceBuilder(deployFile.TableName()).GenerateId(); err == nil { deployFile.DeployId = id } else { logger.Error(c, "生成部署ID失败!", zap.Error(err)) return serializer.DBErr("生成部署ID失败!", err) } // 计算文件哈希值 if fileHash, err := s.calculateFileHash(tempFile.Name()); err == nil { deployFile.FileHash = fileHash } // 设置时间 now := time.Now() deployFile.CreateTime = &now deployFile.DeployTime = &now deployFile.DelFlag = "0" // 获取当前用户 if createBy := c.GetString("id"); createBy != "" { deployFile.CreateBy = createBy } // 保存到数据库 if err := s.Db.Create(&deployFile).Error; err != nil { logger.Error(c, "保存部署文件记录失败!", zap.Error(err)) // 即使保存记录失败,也不影响文件部署 } return serializer.Succ("上传成功!", map[string]interface{}{ "domain": domain, "path": targetDir, "deployId": deployFile.DeployId, }) } // validateZipFile 校验压缩包是否包含index.html func (s *SysUploadService) validateZipFile(zipPath string) (bool, error) { reader, err := zip.OpenReader(zipPath) if err != nil { return false, err } defer reader.Close() for _, file := range reader.File { if strings.ToLower(filepath.Base(file.Name)) == "index.html" { return true, nil } } return false, nil } // extractZip 解压zip文件到指定目录 func (s *SysUploadService) extractZip(zipPath, destDir string) error { reader, err := zip.OpenReader(zipPath) if err != nil { return err } defer reader.Close() // 创建目标目录 if err := os.MkdirAll(destDir, 0755); err != nil { return err } // 提取文件 for _, file := range reader.File { rc, err := file.Open() if err != nil { return err } defer rc.Close() // 构建文件路径 path := filepath.Join(destDir, file.Name) // 检查路径是否安全(防止路径遍历攻击) if !strings.HasPrefix(path, filepath.Clean(destDir)+string(os.PathSeparator)) { return fmt.Errorf("invalid file path: %s", file.Name) } if file.FileInfo().IsDir() { // 创建目录 if err := os.MkdirAll(path, file.FileInfo().Mode()); err != nil { return err } } else { // 创建文件的父目录 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } // 创建文件 outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode()) if err != nil { return err } defer outFile.Close() // 复制文件内容 if _, err := io.Copy(outFile, rc); err != nil { return err } } } return nil } // calculateFileHash 计算文件哈希值 func (s *SysUploadService) calculateFileHash(filePath string) (string, error) { file, err := os.Open(filePath) if err != nil { return "", err } defer file.Close() hash := md5.New() if _, err := io.Copy(hash, file); err != nil { return "", err } return fmt.Sprintf("%x", hash.Sum(nil)), nil }