c755587a4d
后端改进: - 新增版本发布状态管理 (draft/published/deprecated/superseded) - 新增发布/废弃/回滚 API 接口 - 新增增量包生成功能 (仅包含差异文件) - 新增删除版本时的依赖检查 - 数据库字段: publish_status, superseded_by, delta_package_* 前端改进: - 版本列表新增发布状态列和增量包大小列 - 新增发布/废弃/回滚操作按钮 - 新增增量包下载功能 - 更新国际化翻译 修复: - 修复 announcements 页面 typeOptions 未定义问题 - 修复 echarts 模块导入类型问题 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
286 lines
6.7 KiB
Go
286 lines
6.7 KiB
Go
package admin
|
|
|
|
import (
|
|
"archive/zip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// MergePatchWithFullPackage merges a patch with a full package to create a new full package
|
|
func MergePatchWithFullPackage(fullPackagePath, patchPath string, appID 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.zip", 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", newFilePath, fileInfo.Size())
|
|
|
|
return "/" + filepath.ToSlash(newFilePath), fileInfo.Size(), hash, nil
|
|
}
|
|
|
|
// GenerateDeltaPackage generates a delta package containing only changed/new files
|
|
func GenerateDeltaPackage(basePackagePath, newPackagePath string, appID uint) (string, int64, string, error) {
|
|
basePackageFile := filepath.Join(".", basePackagePath)
|
|
newPackageFile := filepath.Join(".", newPackagePath)
|
|
|
|
// 获取基础包的文件哈希映射
|
|
baseFiles, err := getZipFileHashes(basePackageFile)
|
|
if err != nil {
|
|
return "", 0, "", fmt.Errorf("read base package failed: %v", err)
|
|
}
|
|
|
|
// 获取新包的文件哈希映射
|
|
newFiles, err := getZipFileHashes(newPackageFile)
|
|
if err != nil {
|
|
return "", 0, "", fmt.Errorf("read new package failed: %v", err)
|
|
}
|
|
|
|
// 找出差异文件(新增或修改)
|
|
var deltaFiles []string
|
|
for filePath, hash := range newFiles {
|
|
if baseHash, exists := baseFiles[filePath]; !exists || baseHash != hash {
|
|
deltaFiles = append(deltaFiles, filePath)
|
|
}
|
|
}
|
|
|
|
if len(deltaFiles) == 0 {
|
|
log.Printf("[INFO] No delta files found, packages are identical")
|
|
return "", 0, "", nil
|
|
}
|
|
|
|
log.Printf("[INFO] Found %d delta files", len(deltaFiles))
|
|
|
|
// 创建增量包
|
|
timestamp := time.Now().Unix()
|
|
deltaFileName := fmt.Sprintf("delta_%d_%d.zip", appID, timestamp)
|
|
deltaFilePath := filepath.Join("uploads", "versions", deltaFileName)
|
|
|
|
if err := os.MkdirAll(filepath.Dir(deltaFilePath), 0755); err != nil {
|
|
return "", 0, "", fmt.Errorf("create dir failed: %v", err)
|
|
}
|
|
|
|
// 从新包中提取差异文件并打包
|
|
if err := createDeltaZip(newPackageFile, deltaFilePath, deltaFiles); err != nil {
|
|
return "", 0, "", fmt.Errorf("create delta package failed: %v", err)
|
|
}
|
|
|
|
fileInfo, err := os.Stat(deltaFilePath)
|
|
if err != nil {
|
|
return "", 0, "", fmt.Errorf("stat delta file failed: %v", err)
|
|
}
|
|
|
|
hash, err := calculateFileHash(deltaFilePath)
|
|
if err != nil {
|
|
return "", 0, "", fmt.Errorf("calculate hash failed: %v", err)
|
|
}
|
|
|
|
log.Printf("[INFO] Created delta package: %s, size: %d", deltaFilePath, fileInfo.Size())
|
|
|
|
return "/" + filepath.ToSlash(deltaFilePath), fileInfo.Size(), hash, nil
|
|
}
|
|
|
|
// getZipFileHashes returns a map of file paths to their SHA256 hashes
|
|
func getZipFileHashes(zipPath string) (map[string]string, error) {
|
|
r, err := zip.OpenReader(zipPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer r.Close()
|
|
|
|
fileHashes := make(map[string]string)
|
|
for _, f := range r.File {
|
|
if f.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
hash := sha256.New()
|
|
io.Copy(hash, rc)
|
|
rc.Close()
|
|
|
|
fileHashes[f.Name] = hex.EncodeToString(hash.Sum(nil))
|
|
}
|
|
|
|
return fileHashes, nil
|
|
}
|
|
|
|
// createDeltaZip creates a zip file containing only the specified files from the source zip
|
|
func createDeltaZip(sourceZipPath, deltaZipPath string, deltaFiles []string) error {
|
|
r, err := zip.OpenReader(sourceZipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
|
|
deltaFileSet := make(map[string]bool)
|
|
for _, f := range deltaFiles {
|
|
deltaFileSet[f] = true
|
|
}
|
|
|
|
zipFile, err := os.Create(deltaZipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer zipFile.Close()
|
|
|
|
zipWriter := zip.NewWriter(zipFile)
|
|
defer zipWriter.Close()
|
|
|
|
for _, f := range r.File {
|
|
if !deltaFileSet[f.Name] {
|
|
continue
|
|
}
|
|
|
|
if f.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
writer, err := zipWriter.Create(f.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = io.Copy(writer, rc)
|
|
rc.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
})
|
|
} |