feat: auto-generate full package when uploading patch
- Add FullPackagePath, FullPackageSize, FullPackageHash to Version model - When uploading patch, merge with base full package to create new full package - Check update API returns full package info for download - Add package_util.go with merge functions Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -220,24 +220,27 @@ type Application struct {
|
|||||||
|
|
||||||
// Version 版本模型
|
// Version 版本模型
|
||||||
type Version struct {
|
type Version struct {
|
||||||
ID uint `gorm:"primaryKey" json:"id"`
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
ApplicationID uint `json:"application_id"`
|
ApplicationID uint `json:"application_id"`
|
||||||
Version string `gorm:"size:50" json:"version"`
|
Version string `gorm:"size:50" json:"version"`
|
||||||
FilePath string `gorm:"size:255" json:"file_path"`
|
FilePath string `gorm:"size:255" json:"file_path"`
|
||||||
FileSize int64 `json:"file_size"`
|
FileSize int64 `json:"file_size"`
|
||||||
FileHash string `gorm:"size:64" json:"file_hash"`
|
FileHash string `gorm:"size:64" json:"file_hash"`
|
||||||
EntryFile string `gorm:"size:255" json:"entry_file"`
|
EntryFile string `gorm:"size:255" json:"entry_file"`
|
||||||
UpdateStrategy string `gorm:"size:20;default:optional" json:"update_strategy"`
|
UpdateStrategy string `gorm:"size:20;default:optional" json:"update_strategy"`
|
||||||
UpdateType string `gorm:"size:20;default:full" json:"update_type"`
|
UpdateType string `gorm:"size:20;default:full" json:"update_type"`
|
||||||
UpdateMethod string `gorm:"size:20;default:auto" json:"update_method"`
|
UpdateMethod string `gorm:"size:20;default:auto" json:"update_method"`
|
||||||
CustomDownloadURL string `gorm:"size:500" json:"custom_download_url"`
|
CustomDownloadURL string `gorm:"size:500" json:"custom_download_url"`
|
||||||
Description string `gorm:"type:text" json:"description"`
|
FullPackagePath string `gorm:"size:255" json:"full_package_path"`
|
||||||
Changelog string `gorm:"type:text" json:"changelog"`
|
FullPackageSize int64 `json:"full_package_size"`
|
||||||
Status string `gorm:"size:20;default:active" json:"status"`
|
FullPackageHash string `gorm:"size:64" json:"full_package_hash"`
|
||||||
BaseVersionID *uint `json:"base_version_id"`
|
Description string `gorm:"type:text" json:"description"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Changelog string `gorm:"type:text" json:"changelog"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Status string `gorm:"size:20;default:active" json:"status"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
BaseVersionID *uint `json:"base_version_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||||
Files []VersionFile `gorm:"foreignKey:VersionID" json:"files,omitempty"`
|
Files []VersionFile `gorm:"foreignKey:VersionID" json:"files,omitempty"`
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MergePatchWithFullPackage 合并补丁包和全量包,生成新的全量包
|
||||||
|
func MergePatchWithFullPackage(fullPackagePath, patchPath string, appID, userID uint) (string, int64, string, error) {
|
||||||
|
// 读取全量包
|
||||||
|
fullPackageFile := filepath.Join(".", fullPackagePath)
|
||||||
|
patchFile := filepath.Join(".", patchPath)
|
||||||
|
|
||||||
|
// 创建临时目录
|
||||||
|
tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("merge_%d_%d", appID, time.Now().Unix()))
|
||||||
|
if err := os.MkdirAll(tempDir, 0755); err != nil {
|
||||||
|
return "", 0, "", fmt.Errorf("create temp dir failed: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// 解压全量包
|
||||||
|
extractDir := filepath.Join(tempDir, "extracted")
|
||||||
|
if err := os.MkdirAll(extractDir, 0755); err != nil {
|
||||||
|
return "", 0, "", fmt.Errorf("create extract dir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解压全量包
|
||||||
|
if err := extractZip(fullPackageFile, extractDir); err != nil {
|
||||||
|
return "", 0, "", fmt.Errorf("extract full package failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解压补丁包(覆盖同名文件)
|
||||||
|
if err := extractZip(patchFile, extractDir); err != nil {
|
||||||
|
return "", 0, "", fmt.Errorf("extract patch failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的全量包
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
newFileName := fmt.Sprintf("full_%d_%d_%d.zip", userID, appID, timestamp)
|
||||||
|
newFilePath := filepath.Join("uploads", "versions", newFileName)
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if err := os.MkdirAll(filepath.Dir(newFilePath), 0755); err != nil {
|
||||||
|
return "", 0, "", fmt.Errorf("create dir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打包新的全量包
|
||||||
|
if err := createZipFromDir(extractDir, newFilePath); err != nil {
|
||||||
|
return "", 0, "", fmt.Errorf("create new full package failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件大小和哈希
|
||||||
|
fileInfo, err := os.Stat(newFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, "", fmt.Errorf("stat new file failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := calculateFileHash(newFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, "", fmt.Errorf("calculate hash failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Created merged full package: %s, size: %d\n", newFilePath, fileInfo.Size())
|
||||||
|
|
||||||
|
return "/" + filepath.ToSlash(newFilePath), fileInfo.Size(), hash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractZip 解压zip文件到指定目录
|
||||||
|
func extractZip(zipPath, destDir string) error {
|
||||||
|
r, err := zip.OpenReader(zipPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
destPath := filepath.Join(destDir, f.Name)
|
||||||
|
|
||||||
|
if f.FileInfo().IsDir() {
|
||||||
|
os.MkdirAll(destPath, 0755)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保父目录存在
|
||||||
|
os.MkdirAll(filepath.Dir(destPath), 0755)
|
||||||
|
|
||||||
|
destFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srcFile, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
destFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(destFile, srcFile)
|
||||||
|
srcFile.Close()
|
||||||
|
destFile.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createZipFromDir 将目录打包成zip
|
||||||
|
func createZipFromDir(srcDir, zipPath string) error {
|
||||||
|
zipFile, err := os.Create(zipPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer zipFile.Close()
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(zipFile)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(srcDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
_, err = zipWriter.Create(relPath + "/")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := zipWriter.Create(relPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(writer, file)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -336,11 +336,38 @@ func handleCreateVersionGlobal(c *gin.Context) {
|
|||||||
version.UpdateMethod = "auto"
|
version.UpdateMethod = "auto"
|
||||||
}
|
}
|
||||||
|
|
||||||
if version.UpdateType == "patch" {
|
// 处理全量包和补丁包
|
||||||
|
if version.UpdateType == "full" {
|
||||||
|
// 全量包:full_package 就是自己
|
||||||
|
version.FullPackagePath = version.FilePath
|
||||||
|
version.FullPackageSize = version.FileSize
|
||||||
|
version.FullPackageHash = version.FileHash
|
||||||
|
} else if version.UpdateType == "patch" {
|
||||||
|
// 补丁包:需要找到基础全量包并合并
|
||||||
var lastFullVersion model.Version
|
var lastFullVersion model.Version
|
||||||
if err := database.DB.Where("application_id = ? AND update_type = ?", req.ApplicationID, "full").
|
if err := database.DB.Where("application_id = ? AND update_type = ?", req.ApplicationID, "full").
|
||||||
Order("id DESC").First(&lastFullVersion).Error; err == nil {
|
Order("id DESC").First(&lastFullVersion).Error; err == nil {
|
||||||
version.BaseVersionID = &lastFullVersion.ID
|
version.BaseVersionID = &lastFullVersion.ID
|
||||||
|
|
||||||
|
// 合并生成新的全量包
|
||||||
|
fullPkgPath, fullPkgSize, fullPkgHash, err := MergePatchWithFullPackage(
|
||||||
|
lastFullVersion.FullPackagePath,
|
||||||
|
req.FilePath,
|
||||||
|
req.ApplicationID,
|
||||||
|
userID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to merge patch with full package: %v\n", err)
|
||||||
|
response.Error(c, 500, "合并补丁包失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
version.FullPackagePath = fullPkgPath
|
||||||
|
version.FullPackageSize = fullPkgSize
|
||||||
|
version.FullPackageHash = fullPkgHash
|
||||||
|
} else {
|
||||||
|
response.Error(c, 400, "找不到基础全量包,无法创建补丁版本")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,8 +216,18 @@ func handleAppCheckUpdate(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定下载地址:优先使用自定义地址
|
// 确定下载地址:优先使用完整包地址,其次自定义地址
|
||||||
downloadURL := latestVersion.FilePath
|
downloadURL := latestVersion.FilePath
|
||||||
|
downloadSize := latestVersion.FileSize
|
||||||
|
downloadHash := latestVersion.FileHash
|
||||||
|
|
||||||
|
// 使用完整包地址(如果有)
|
||||||
|
if latestVersion.FullPackagePath != "" {
|
||||||
|
downloadURL = latestVersion.FullPackagePath
|
||||||
|
downloadSize = latestVersion.FullPackageSize
|
||||||
|
downloadHash = latestVersion.FullPackageHash
|
||||||
|
}
|
||||||
|
// 自定义地址优先级最高
|
||||||
if latestVersion.CustomDownloadURL != "" {
|
if latestVersion.CustomDownloadURL != "" {
|
||||||
downloadURL = latestVersion.CustomDownloadURL
|
downloadURL = latestVersion.CustomDownloadURL
|
||||||
}
|
}
|
||||||
@@ -228,8 +238,8 @@ func handleAppCheckUpdate(c *gin.Context) {
|
|||||||
"current_version": clientVersion,
|
"current_version": clientVersion,
|
||||||
"latest_version": latestVersion.Version,
|
"latest_version": latestVersion.Version,
|
||||||
"download_url": downloadURL,
|
"download_url": downloadURL,
|
||||||
"file_size": latestVersion.FileSize,
|
"file_size": downloadSize,
|
||||||
"file_hash": latestVersion.FileHash,
|
"file_hash": downloadHash,
|
||||||
"entry_file": latestVersion.EntryFile,
|
"entry_file": latestVersion.EntryFile,
|
||||||
"update_notes": latestVersion.Description,
|
"update_notes": latestVersion.Description,
|
||||||
"update_strategy": updateStrategy,
|
"update_strategy": updateStrategy,
|
||||||
|
|||||||
Reference in New Issue
Block a user