DeployHelper/internal/service/sys_upload_service.go

247 lines
6.2 KiB
Go
Raw Normal View History

2025-08-01 16:38:08 +08:00
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
}