Files
verify/backend/internal/router/admin/package_util.go
T
admin c755587a4d feat: 完善版本管理功能
后端改进:
- 新增版本发布状态管理 (draft/published/deprecated/superseded)
- 新增发布/废弃/回滚 API 接口
- 新增增量包生成功能 (仅包含差异文件)
- 新增删除版本时的依赖检查
- 数据库字段: publish_status, superseded_by, delta_package_*

前端改进:
- 版本列表新增发布状态列和增量包大小列
- 新增发布/废弃/回滚操作按钮
- 新增增量包下载功能
- 更新国际化翻译

修复:
- 修复 announcements 页面 typeOptions 未定义问题
- 修复 echarts 模块导入类型问题

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 20:31:52 +08:00

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
})
}