feat(deploy): 支持 tar 和 gz 文件格式

- 修改文件格式校验逻辑,增加对 tar 和 gz 文件的支持
- 添加文件 ID 生成逻辑,确保唯一性
- 实现文件内容校验功能,确保压缩包中包含 index.html
- 优化项目创建流程,支持关联部署文件
-改进项目删除逻辑,使用事务确保数据一致性
This commit is contained in:
zhangtao 2025-08-06 12:22:53 +08:00
parent d024f0c9e0
commit 73492a4add
3 changed files with 132 additions and 27 deletions

View File

@ -8,6 +8,7 @@ type SysDeployProject struct {
ProjectName string `gorm:"column:project_name;type:varchar(100);not null;comment:项目名称" json:"projectName" binding:"required"`
Domain string `gorm:"column:domain;type:varchar(255);not null;comment:访问域名" json:"domain" binding:"required"`
DeployPath string `gorm:"column:deploy_path;type:varchar(500);not null;comment:部署路径" json:"deployPath"`
FileId string `gorm:"-" json:"fileId" binding:"required"`
Status string `gorm:"column:status;type:char(1);default:1;comment:状态0停用 1正常 2部署中 3部署失败" json:"status"`
DeployStatus string `gorm:"column:deploy_status;type:char(1);default:0;comment:部署状态0未部署 1部署成功 2部署失败" json:"deployStatus"`
ErrorMsg string `gorm:"column:error_msg;type:text;comment:错误信息" json:"errorMsg"`

View File

@ -9,7 +9,9 @@ import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
@ -39,9 +41,17 @@ func (s *SysDeployFileService) Create(c *gin.Context) serializer.Response {
}
// 校验文件类型
if !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") {
logger.Error(c, "只支持zip格式文件!")
return serializer.ParamErr("只支持zip格式文件!", nil)
supportedExtensions := []string{".zip", ".tar", ".gz"}
valid := false
for _, ext := range supportedExtensions {
if strings.HasSuffix(strings.ToLower(file.Filename), ext) {
valid = true
break
}
}
if !valid {
logger.Error(c, "只支持zip、tar、gz格式文件!")
return serializer.ParamErr("只支持zip、tar、gz格式文件!", nil)
}
// 获取文件名(不包含扩展名)
@ -49,15 +59,12 @@ func (s *SysDeployFileService) Create(c *gin.Context) serializer.Response {
var deployFile model.SysDeployFile
deployFile.FileName = filename
// 生成文件ID
if id, err := SysSequenceServiceBuilder(deployFile.TableName()).GenerateId(); err == nil {
deployFile.FileId = id
} else {
return serializer.DBErr("序列生成失败!", err)
}
// 获取文件扩展名
fileExt := filepath.Ext(file.Filename)
// 生成新的文件名使用文件ID + 原始文件扩展名
newFileName := deployFile.FileId + fileExt
// 生成新的文件名使用文件ID + .zip 后缀
newFileName := deployFile.FileId + ".zip"
// 设置文件保存路径
deployFile.FilePath = filepath.Join("/data", newFileName)
@ -67,6 +74,20 @@ func (s *SysDeployFileService) Create(c *gin.Context) serializer.Response {
return serializer.ParamErr("保存文件失败!", err)
}
// 校验文件
err = s.ValidateArchiveFile(deployFile.FilePath, fileExt, file.Filename)
if err != nil {
logger.Error(c, "校验文件失败!")
return serializer.ParamErr("校验文件失败!", err)
}
// 生成文件ID
if id, err := SysSequenceServiceBuilder(deployFile.TableName()).GenerateId(); err == nil {
deployFile.FileId = id
} else {
return serializer.DBErr("序列生成失败!", err)
}
// 打开保存的文件以计算哈希值
savedFile, err := os.Open(deployFile.FilePath)
if err != nil {
@ -359,3 +380,43 @@ func (s *SysDeployFileService) CalculateFileHash(reader io.Reader) (string, erro
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}
// ValidateArchiveFile 校验压缩文件是否包含 index.html
func (s *SysDeployFileService) ValidateArchiveFile(filePath, fileExt, originalFilename string) error {
// 只在linux下检查
if runtime.GOOS != "linux" {
return nil // 非Linux系统跳过校验
}
var cmd *exec.Cmd
switch fileExt {
case ".zip":
cmd = exec.Command("unzip", "-l", filePath)
case ".tar":
cmd = exec.Command("tar", "-tf", filePath)
case ".gz":
// 对于 .tar.gz 文件,需要特殊处理
if strings.HasSuffix(strings.ToLower(originalFilename), ".tar.gz") {
cmd = exec.Command("tar", "-tzf", filePath)
} else {
// 单独的 .gz 文件不支持列表查看
// 这里假设是 .tar.gz 文件
cmd = exec.Command("tar", "-tzf", filePath)
}
default:
return fmt.Errorf("不支持的文件格式")
}
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("检查文件内容失败: %v", err)
}
// 检查输出中是否包含 index.html
outputStr := string(output)
if !strings.Contains(outputStr, "index.html") {
return fmt.Errorf("压缩包中必须包含 index.html 文件")
}
return nil
}

View File

@ -7,7 +7,6 @@ import (
"ego/pkg/logger"
"fmt"
"os"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
@ -54,6 +53,9 @@ func (s *SysDeployProjectService) Create(c *gin.Context) serializer.Response {
return serializer.DBErr("序列生成失败!", err)
}
// 设置默认部署路径
deployProject.DeployPath = fmt.Sprintf("/home/%s", deployProject.Domain)
// 设置默认值
now := time.Now()
deployProject.CreateTime = &now
@ -66,11 +68,44 @@ func (s *SysDeployProjectService) Create(c *gin.Context) serializer.Response {
deployProject.CreateBy = createBy
}
if err := s.Db.Create(&deployProject).Error; err != nil {
// 开始事务处理
tx := s.Db.Begin()
// 创建部署项目记录
if err := tx.Create(&deployProject).Error; err != nil {
tx.Rollback()
logger.Error(c, "创建部署项目记录失败!")
return serializer.DBErr("创建部署项目记录失败!", err)
}
// 如果提供了FileId则更新关联的部署文件记录的ParentId
if deployProject.FileId != "" {
// 检查文件是否存在且属于当前用户
currentUserId := c.GetString("id")
var deployFile model.SysDeployFile
if err := tx.Where("file_id = ? AND del_flag = ? AND create_by = ?",
deployProject.FileId, "0", currentUserId).First(&deployFile).Error; err != nil {
tx.Rollback()
logger.Error(c, "关联的部署文件不存在或无权限访问!")
return serializer.ParamErr("关联的部署文件不存在或无权限访问!", err)
}
// 更新部署文件的ParentId为当前项目的DeployId
if err := tx.Model(&model.SysDeployFile{}).Where("file_id = ?", deployProject.FileId).
Updates(map[string]interface{}{
"parent_id": deployProject.DeployId,
"update_time": time.Now(),
"update_by": currentUserId,
}).Error; err != nil {
tx.Rollback()
logger.Error(c, "更新部署文件关联信息失败!")
return serializer.DBErr("更新部署文件关联信息失败!", err)
}
}
// 提交事务
tx.Commit()
return serializer.Succ("创建部署项目记录成功!", deployProject)
}
@ -145,44 +180,52 @@ func (s *SysDeployProjectService) DeleteByID(c *gin.Context) serializer.Response
return serializer.ParamErr("用户信息获取失败!", nil)
}
// 软删除
data := map[string]any{
"del_flag": "1",
"update_time": time.Now(),
"update_by": currentUserId,
}
// 开始事务处理
tx := s.Db.Begin()
// 删除已经部署的文件夹
// 获取要删除的项目信息
deployProject := model.SysDeployProject{}
if err := s.Db.Where("deploy_id = ? AND create_by = ?", id, currentUserId).First(&deployProject).Error; err != nil {
if err := tx.Where("deploy_id = ? AND create_by = ?", id, currentUserId).First(&deployProject).Error; err != nil {
tx.Rollback()
logger.Error(c, "获取部署项目记录失败或无权限访问!")
return serializer.DBErr("获取部署项目记录失败或无权限访问!", err)
}
// 删除 /home/:projectName
err := os.RemoveAll(filepath.Join("/home", deployProject.Domain))
// 删除 /home/:domain 目录
err := os.RemoveAll(deployProject.DeployPath)
if err != nil {
tx.Rollback()
logger.Error(c, "删除部署文件夹失败!")
return serializer.DBErr("删除部署文件夹失败!", err)
}
// 同时软删除关联的文件记录
// 软删除关联的文件记录
fileData := map[string]any{
"del_flag": "1",
"update_time": time.Now(),
"update_by": currentUserId,
}
if err := s.Db.Model(&model.SysDeployFile{}).Where("parent_id = ?", id).Updates(fileData).Error; err != nil {
if err := tx.Model(&model.SysDeployFile{}).Where("parent_id = ?", id).Updates(fileData).Error; err != nil {
tx.Rollback()
logger.Error(c, "删除关联文件记录失败!")
return serializer.DBErr("删除关联文件记录失败!", err)
}
// 删除数据库记录
if err := s.Db.Model(&model.SysDeployProject{}).Where("deploy_id = ? AND create_by = ?", id, currentUserId).Updates(data).Error; err != nil {
// 软删除项目记录
projectData := map[string]any{
"del_flag": "1",
"update_time": time.Now(),
"update_by": currentUserId,
}
if err := tx.Model(&model.SysDeployProject{}).Where("deploy_id = ? AND create_by = ?", id, currentUserId).Updates(projectData).Error; err != nil {
tx.Rollback()
logger.Error(c, "删除部署项目记录失败!")
return serializer.DBErr("删除部署项目记录失败!", err)
}
// 提交事务
tx.Commit()
return serializer.Succ("删除部署项目记录成功!", nil)
}