288 lines
7.6 KiB
Go
288 lines
7.6 KiB
Go
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)
|
|
|
|
// 先创建或获取项目记录
|
|
var deployProject model.SysDeployProject
|
|
currentUserId := c.GetString("id")
|
|
|
|
// 检查是否已存在相同域名的项目
|
|
err = s.Db.Where("domain = ? AND del_flag = ?", domain, "0").First(&deployProject).Error
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
// 创建新项目
|
|
deployProject = model.SysDeployProject{
|
|
ProjectName: filename,
|
|
Domain: domain,
|
|
DeployPath: targetDir,
|
|
Status: model.DeployProjectStatusNormal,
|
|
DeployStatus: model.DeployStatusSuccess,
|
|
Description: fmt.Sprintf("自动部署项目: %s", filename),
|
|
DelFlag: "0",
|
|
CreateBy: currentUserId,
|
|
}
|
|
|
|
// 生成项目ID
|
|
if id, err := SysSequenceServiceBuilder(deployProject.TableName()).GenerateId(); err == nil {
|
|
deployProject.DeployId = id
|
|
} else {
|
|
logger.Error(c, "生成项目ID失败!", zap.Error(err))
|
|
return serializer.DBErr("生成项目ID失败!", err)
|
|
}
|
|
|
|
// 设置时间
|
|
now := time.Now()
|
|
deployProject.CreateTime = &now
|
|
deployProject.DeployTime = &now
|
|
|
|
// 保存项目到数据库
|
|
if err := s.Db.Create(&deployProject).Error; err != nil {
|
|
logger.Error(c, "保存项目记录失败!", zap.Error(err))
|
|
return serializer.DBErr("保存项目记录失败!", err)
|
|
}
|
|
} else {
|
|
logger.Error(c, "查询项目记录失败!", zap.Error(err))
|
|
return serializer.DBErr("查询项目记录失败!", err)
|
|
}
|
|
}
|
|
|
|
// 创建文件记录
|
|
deployFile := model.SysDeployFile{
|
|
ParentId: deployProject.DeployId,
|
|
FileName: file.Filename,
|
|
FileSize: file.Size,
|
|
Status: model.FileStatusInUse, // 新上传的文件设为使用中
|
|
DelFlag: "0",
|
|
CreateBy: currentUserId,
|
|
}
|
|
|
|
// 生成文件ID
|
|
if id, err := SysSequenceServiceBuilder(deployFile.TableName()).GenerateId(); err == nil {
|
|
deployFile.FileId = 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
|
|
|
|
// 将同项目下的其他文件设为未使用状态
|
|
s.Db.Model(&model.SysDeployFile{}).
|
|
Where("parent_id = ? AND del_flag = ?", deployProject.DeployId, "0").
|
|
Update("status", model.FileStatusNotUsed)
|
|
|
|
// 保存文件记录到数据库
|
|
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,
|
|
"projectId": deployProject.DeployId,
|
|
"fileId": deployFile.FileId,
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|