feat(deploy): 增加部署文件下载和部署功能
- 新增 DownloadByID 和 DeployByID 方法,实现文件下载和部署 - 优化文件校验逻辑,仅支持 zip 格式 - 添加解压 zip 文件到指定目录的功能 - 修复查询部署项目时关联文件查询的问题 - 优化代码结构,提高可维护性
This commit is contained in:
parent
73492a4add
commit
41a46a42d3
|
@ -118,3 +118,31 @@ func (h *SysDeployFileHandler) SetActiveFile(c *gin.Context) {
|
|||
logger.Info(c, "设置活跃文件", zap.String("id", c.Param("id")))
|
||||
c.JSON(200, h.deployFileService.SetActiveFile(c))
|
||||
}
|
||||
|
||||
// DownloadByID 根据ID下载文件
|
||||
// @Summary 下载文件
|
||||
// @Description 根据ID下载部署文件
|
||||
// @Tags 部署文件管理
|
||||
// @Accept json
|
||||
// @Produce application/octet-stream
|
||||
// @Param id path string true "文件ID"
|
||||
// @Success 200 {file} file "文件内容"
|
||||
// @Router /deploy-files/{id}/download [get]
|
||||
func (h *SysDeployFileHandler) DownloadByID(c *gin.Context) {
|
||||
logger.Info(c, "下载文件", zap.String("id", c.Param("id")))
|
||||
h.deployFileService.DownloadByID(c)
|
||||
}
|
||||
|
||||
// DeployByID 根据ID部署文件
|
||||
// @Summary 部署文件
|
||||
// @Description 根据ID部署部署文件
|
||||
// @Tags 部署文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "文件ID"
|
||||
// @Success 200 {object} serializer.Response
|
||||
// @Router /deploy-files/{id}/deploy [put]
|
||||
func (h *SysDeployFileHandler) DeployByID(c *gin.Context) {
|
||||
logger.Info(c, "部署文件", zap.String("id", c.Param("id")))
|
||||
c.JSON(200, h.deployFileService.DeployByID(c))
|
||||
}
|
||||
|
|
|
@ -4,24 +4,25 @@ import "time"
|
|||
|
||||
// SysDeployProject 部署项目记录表
|
||||
type SysDeployProject struct {
|
||||
DeployId string `gorm:"column:deploy_id;type:varchar(64);primary_key;comment:部署ID" json:"deployId"`
|
||||
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"`
|
||||
Version string `gorm:"column:version;type:varchar(50);comment:版本号" json:"version"`
|
||||
Description string `gorm:"column:description;type:varchar(500);comment:描述" json:"description"`
|
||||
DelFlag string `gorm:"column:del_flag;type:char(1);default:0;comment:删除标志(0代表存在 1代表删除)" json:"delFlag"`
|
||||
CreateBy string `gorm:"column:create_by;type:varchar(64);comment:创建者" json:"createBy"`
|
||||
CreateTime *time.Time `gorm:"column:create_time;type:datetime;comment:创建时间" json:"createTime"`
|
||||
UpdateBy string `gorm:"column:update_by;type:varchar(64);comment:更新者" json:"updateBy"`
|
||||
UpdateTime *time.Time `gorm:"column:update_time;type:datetime;comment:更新时间" json:"updateTime"`
|
||||
DeployTime *time.Time `gorm:"column:deploy_time;type:datetime;comment:部署时间" json:"deployTime"`
|
||||
LastAccessTime *time.Time `gorm:"column:last_access_time;type:datetime;comment:最后访问时间" json:"lastAccessTime"`
|
||||
AccessCount int64 `gorm:"column:access_count;type:bigint;default:0;comment:访问次数" json:"accessCount"`
|
||||
DeployId string `gorm:"column:deploy_id;type:varchar(64);primary_key;comment:部署ID" json:"deployId"`
|
||||
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"`
|
||||
Version string `gorm:"column:version;type:varchar(50);comment:版本号" json:"version"`
|
||||
Description string `gorm:"column:description;type:varchar(500);comment:描述" json:"description"`
|
||||
DelFlag string `gorm:"column:del_flag;type:char(1);default:0;comment:删除标志(0代表存在 1代表删除)" json:"delFlag"`
|
||||
CreateBy string `gorm:"column:create_by;type:varchar(64);comment:创建者" json:"createBy"`
|
||||
CreateTime *time.Time `gorm:"column:create_time;type:datetime;comment:创建时间" json:"createTime"`
|
||||
UpdateBy string `gorm:"column:update_by;type:varchar(64);comment:更新者" json:"updateBy"`
|
||||
UpdateTime *time.Time `gorm:"column:update_time;type:datetime;comment:更新时间" json:"updateTime"`
|
||||
DeployTime *time.Time `gorm:"column:deploy_time;type:datetime;comment:部署时间" json:"deployTime"`
|
||||
LastAccessTime *time.Time `gorm:"column:last_access_time;type:datetime;comment:最后访问时间" json:"lastAccessTime"`
|
||||
AccessCount int64 `gorm:"column:access_count;type:bigint;default:0;comment:访问次数" json:"accessCount"`
|
||||
SysDeployFiles []*SysDeployFile `gorm:"-" json:"sysDeployFiles"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
|
|
|
@ -33,4 +33,6 @@ func SysDeployFileHandlerRouter(group *gin.RouterGroup, h *handler.SysDeployFile
|
|||
g.GET("", h.GetByCondition)
|
||||
g.GET("/parent/:parentId", h.GetByParentID)
|
||||
g.PUT("/:id/active", h.SetActiveFile)
|
||||
g.GET("/:id/download", h.DownloadByID)
|
||||
g.PUT("/:id/deploy", h.DeployByID)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/md5"
|
||||
"ego/internal/model"
|
||||
"ego/internal/serializer"
|
||||
|
@ -9,13 +10,12 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
@ -41,24 +41,21 @@ func (s *SysDeployFileService) Create(c *gin.Context) serializer.Response {
|
|||
}
|
||||
|
||||
// 校验文件类型
|
||||
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)
|
||||
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))
|
||||
var deployFile model.SysDeployFile
|
||||
|
||||
// 设置文件名
|
||||
deployFile.FileName = filename
|
||||
|
||||
// 获取项目id
|
||||
deployFile.ParentId = c.PostForm("parentId")
|
||||
|
||||
// 获取文件扩展名
|
||||
fileExt := filepath.Ext(file.Filename)
|
||||
|
||||
|
@ -75,10 +72,10 @@ func (s *SysDeployFileService) Create(c *gin.Context) serializer.Response {
|
|||
}
|
||||
|
||||
// 校验文件
|
||||
err = s.ValidateArchiveFile(deployFile.FilePath, fileExt, file.Filename)
|
||||
if err != nil {
|
||||
logger.Error(c, "校验文件失败!")
|
||||
return serializer.ParamErr("校验文件失败!", err)
|
||||
zipFile, err := s.validateZipFile(deployFile.FilePath)
|
||||
if !zipFile {
|
||||
logger.Error(c, "文件格式错误!")
|
||||
return serializer.ParamErr("文件格式错误!", nil)
|
||||
}
|
||||
|
||||
// 生成文件ID
|
||||
|
@ -381,41 +378,184 @@ 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系统跳过校验
|
||||
// DownloadByID 根据ID下载文件
|
||||
func (s *SysDeployFileService) DownloadByID(c *gin.Context) serializer.Response {
|
||||
// 获取当前用户ID
|
||||
currentUserId := c.GetString("id")
|
||||
if currentUserId == "" {
|
||||
return serializer.ParamErr("用户信息获取失败!", nil)
|
||||
}
|
||||
|
||||
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("不支持的文件格式")
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
logger.Error(c, "id 不可为空!")
|
||||
return serializer.ParamErr("id不可为空!", fmt.Errorf("id不可为空"))
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
// 获取文件信息
|
||||
var deployFile model.SysDeployFile
|
||||
if err := s.Db.Where("file_id = ? AND del_flag = ? AND create_by = ?", id, "0", currentUserId).First(&deployFile).Error; err != nil {
|
||||
logger.Error(c, "获取部署文件记录失败!")
|
||||
return serializer.DBErr("获取部署文件记录失败!", err)
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(deployFile.FilePath); os.IsNotExist(err) {
|
||||
logger.Error(c, "文件不存在!")
|
||||
return serializer.ParamErr("文件不存在!", err)
|
||||
}
|
||||
|
||||
// 设置响应头,提供文件下载
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Transfer-Encoding", "binary")
|
||||
c.Header("Content-Disposition", "attachment; filename="+deployFile.FileName+filepath.Ext(deployFile.FilePath))
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
|
||||
// 提供文件下载
|
||||
c.File(deployFile.FilePath)
|
||||
return serializer.Response{Code: 200}
|
||||
}
|
||||
|
||||
// DeployByID 部署文件
|
||||
func (s *SysDeployFileService) DeployByID(c *gin.Context) serializer.Response {
|
||||
logger.Info(c, "部署文件", zap.String("id", c.Param("id")))
|
||||
// 获取当前用户ID
|
||||
currentUserId := c.GetString("id")
|
||||
if currentUserId == "" {
|
||||
return serializer.ParamErr("用户信息获取失败!", nil)
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
logger.Error(c, "id 不可为空!")
|
||||
return serializer.ParamErr("id不可为空!", fmt.Errorf("id不可为空"))
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
var deployFile model.SysDeployFile
|
||||
if err := s.Db.Where("file_id = ? AND del_flag = ? AND create_by = ?", id, "0", currentUserId).First(&deployFile).Error; err != nil {
|
||||
logger.Error(c, "获取部署文件记录失败!")
|
||||
return serializer.DBErr("获取部署文件记录失败!", err)
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(deployFile.FilePath); os.IsNotExist(err) {
|
||||
logger.Error(c, "文件不存在!")
|
||||
return serializer.ParamErr("文件不存在!", err)
|
||||
}
|
||||
|
||||
var deployProject model.SysDeployProject
|
||||
if err := s.Db.Where("deploy_id = ? AND del_flag = ? AND create_by = ?", deployFile.ParentId, "0", currentUserId).First(&deployProject).Error; err != nil {
|
||||
logger.Error(c, "获取部署项目记录失败!")
|
||||
return serializer.DBErr("获取部署项目记录失败!", err)
|
||||
}
|
||||
|
||||
err := s.extractZip(deployFile.FilePath, deployProject.DeployPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查文件内容失败: %v", err)
|
||||
logger.Error(c, "解压文件失败!")
|
||||
return serializer.DBErr("解压文件失败!", err)
|
||||
}
|
||||
|
||||
// 检查输出中是否包含 index.html
|
||||
outputStr := string(output)
|
||||
if !strings.Contains(outputStr, "index.html") {
|
||||
return fmt.Errorf("压缩包中必须包含 index.html 文件")
|
||||
// 开始事务
|
||||
tx := s.Db.Begin()
|
||||
|
||||
// 更新状态
|
||||
if tx.Model(&model.SysDeployFile{}).Where("file_id = ?", id).Updates(map[string]any{
|
||||
"status": model.DeployStatusSuccess,
|
||||
"update_time": time.Now(),
|
||||
"update_by": currentUserId,
|
||||
}).Error != nil {
|
||||
tx.Rollback()
|
||||
logger.Error(c, "更新部署文件状态失败!")
|
||||
return serializer.DBErr("更新部署文件状态失败!", err)
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
if tx.Model(&model.SysDeployProject{}).Where("deploy_id = ?", deployFile.ParentId).Updates(map[string]any{
|
||||
"deploy_status": model.DeployStatusSuccess,
|
||||
"deploy_time": time.Now(),
|
||||
"update_time": time.Now(),
|
||||
"update_by": currentUserId,
|
||||
}).Error != nil {
|
||||
tx.Rollback()
|
||||
logger.Error(c, "更新部署项目状态失败!")
|
||||
return serializer.DBErr("更新部署项目状态失败!", err)
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return serializer.Succ("部署成功!", nil)
|
||||
}
|
||||
|
||||
// validateZipFile 校验压缩包是否包含index.html
|
||||
func (s *SysDeployFileService) 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 *SysDeployFileService) 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
|
||||
|
|
|
@ -276,6 +276,15 @@ func (s *SysDeployProjectService) GetByCondition(c *gin.Context) serializer.Resp
|
|||
return serializer.DBErr("获取部署项目记录失败!", err)
|
||||
}
|
||||
|
||||
// 查询每个项目的关联文件
|
||||
for i := range deployProjects {
|
||||
var deployFiles []*model.SysDeployFile
|
||||
s.Db.Where("parent_id = ? AND del_flag = ? AND create_by = ?",
|
||||
deployProjects[i].DeployId, "0", currentUserId).Find(&deployFiles)
|
||||
// 这里需要在 model.SysDeployProject 中添加 SysDeployFiles 字段
|
||||
deployProjects[i].SysDeployFiles = deployFiles
|
||||
}
|
||||
|
||||
// 执行总数查询
|
||||
if err := db.Where("del_flag = ?", "0").Count(&total).Error; err != nil {
|
||||
logger.Error(c, "获取部署项目记录总数失败!")
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"ego/internal/model"
|
||||
"ego/internal/serializer"
|
||||
"ego/pkg/logger"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
@ -114,7 +115,7 @@ func (s *SysUploadService) UploadZip(c *gin.Context) serializer.Response {
|
|||
// 检查是否已存在相同域名的项目
|
||||
err = s.Db.Where("domain = ? AND del_flag = ?", domain, "0").First(&deployProject).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 创建新项目
|
||||
deployProject = model.SysDeployProject{
|
||||
ProjectName: filename,
|
||||
|
|
Loading…
Reference in New Issue