247 lines
6.2 KiB
Go
247 lines
6.2 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)
|
|
|
|
// 创建部署文件记录
|
|
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
|
|
}
|