6124d6060c
Co-authored-by: Cursor <cursoragent@cursor.com>
369 lines
15 KiB
C#
369 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace R0PatchGenerator
|
|
{
|
|
public class PatchGenerator
|
|
{
|
|
private string hdiffzPath;
|
|
|
|
public PatchGenerator()
|
|
{
|
|
// 查找 hdiffz.exe
|
|
string exeDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
|
hdiffzPath = Path.Combine(exeDir, "hdiffz.exe");
|
|
|
|
if (!File.Exists(hdiffzPath))
|
|
{
|
|
// 尝试在当前目录查找
|
|
hdiffzPath = "hdiffz.exe";
|
|
}
|
|
}
|
|
|
|
public bool GeneratePatch(string oldPath, string newPath, string outputZip, string fromVersion, string toVersion)
|
|
{
|
|
// 创建临时目录
|
|
string tempDir = Path.Combine(Path.GetTempPath(), "r0patch_" + Guid.NewGuid().ToString("N"));
|
|
string patchesDir = Path.Combine(tempDir, "patches");
|
|
string newFilesDir = Path.Combine(tempDir, "new");
|
|
|
|
Directory.CreateDirectory(tempDir);
|
|
Directory.CreateDirectory(patchesDir);
|
|
Directory.CreateDirectory(newFilesDir);
|
|
|
|
try
|
|
{
|
|
// 获取所有文件
|
|
Dictionary<string, FileInfo> oldFiles = GetAllFiles(oldPath);
|
|
Dictionary<string, FileInfo> newFiles = GetAllFiles(newPath);
|
|
|
|
Console.WriteLine("旧版本文件数: " + oldFiles.Count);
|
|
Console.WriteLine("新版本文件数: " + newFiles.Count);
|
|
Console.WriteLine();
|
|
|
|
// 创建清单
|
|
PatchManifest manifest = new PatchManifest();
|
|
manifest.from_version = fromVersion;
|
|
manifest.to_version = toVersion;
|
|
manifest.created_at = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
|
|
|
int processedCount = 0;
|
|
int totalFiles = newFiles.Count;
|
|
|
|
foreach (var kvp in newFiles)
|
|
{
|
|
string relativePath = kvp.Key;
|
|
FileInfo newFile = kvp.Value;
|
|
processedCount++;
|
|
|
|
Console.Write("[" + processedCount + "/" + totalFiles + "] " + relativePath + " ... ");
|
|
|
|
if (oldFiles.ContainsKey(relativePath))
|
|
{
|
|
// 文件存在于两个版本
|
|
FileInfo oldFile = oldFiles[relativePath];
|
|
string oldHash = CalculateMD5(oldFile.FullName);
|
|
string newHash = CalculateMD5(newFile.FullName);
|
|
|
|
if (oldHash == newHash)
|
|
{
|
|
// 文件未变化
|
|
Console.WriteLine("未变化");
|
|
manifest.unchanged.Add(relativePath);
|
|
}
|
|
else
|
|
{
|
|
// 文件已修改,生成差异
|
|
string patchFileName = relativePath.Replace("\\", "_").Replace("/", "_") + ".hdiff";
|
|
string patchFilePath = Path.Combine(patchesDir, patchFileName);
|
|
|
|
// 确保目录存在
|
|
Directory.CreateDirectory(Path.GetDirectoryName(patchFilePath));
|
|
|
|
bool patchSuccess = CreateHDiff(oldFile.FullName, newFile.FullName, patchFilePath);
|
|
|
|
if (patchSuccess && File.Exists(patchFilePath))
|
|
{
|
|
FileInfo patchInfo = new FileInfo(patchFilePath);
|
|
FileInfo originalInfo = new FileInfo(newFile.FullName);
|
|
|
|
// 如果差异文件比原文件还大,直接用新文件
|
|
if (patchInfo.Length >= originalInfo.Length * 0.9)
|
|
{
|
|
Console.WriteLine("替换 (差异过大)");
|
|
File.Delete(patchFilePath);
|
|
|
|
// 作为新文件处理
|
|
string newFilePath = Path.Combine(newFilesDir, relativePath);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
|
File.Copy(newFile.FullName, newFilePath, true);
|
|
|
|
PatchFileInfo pfi = new PatchFileInfo();
|
|
pfi.path = relativePath;
|
|
pfi.hash = newHash;
|
|
pfi.size = newFile.Length;
|
|
manifest.new_files.Add(pfi);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("差异 (" + FormatFileSize(patchInfo.Length) + ")");
|
|
|
|
PatchDiffInfo pdi = new PatchDiffInfo();
|
|
pdi.path = relativePath;
|
|
pdi.patch_file = "patches/" + patchFileName;
|
|
pdi.old_hash = oldHash;
|
|
pdi.new_hash = newHash;
|
|
pdi.new_size = newFile.Length;
|
|
manifest.modified.Add(pdi);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 差异生成失败,使用完整文件
|
|
Console.WriteLine("替换 (差异失败)");
|
|
|
|
string newFilePath = Path.Combine(newFilesDir, relativePath);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
|
File.Copy(newFile.FullName, newFilePath, true);
|
|
|
|
PatchFileInfo pfi = new PatchFileInfo();
|
|
pfi.path = relativePath;
|
|
pfi.hash = newHash;
|
|
pfi.size = newFile.Length;
|
|
manifest.new_files.Add(pfi);
|
|
}
|
|
}
|
|
|
|
// 从旧文件列表移除
|
|
oldFiles.Remove(relativePath);
|
|
}
|
|
else
|
|
{
|
|
// 新增文件
|
|
Console.WriteLine("新增");
|
|
|
|
string newFilePath = Path.Combine(newFilesDir, relativePath);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
|
File.Copy(newFile.FullName, newFilePath, true);
|
|
|
|
PatchFileInfo pfi = new PatchFileInfo();
|
|
pfi.path = relativePath;
|
|
pfi.hash = CalculateMD5(newFile.FullName);
|
|
pfi.size = newFile.Length;
|
|
manifest.new_files.Add(pfi);
|
|
}
|
|
}
|
|
|
|
// 剩余的旧文件需要删除
|
|
foreach (var kvp in oldFiles)
|
|
{
|
|
Console.WriteLine("删除: " + kvp.Key);
|
|
manifest.deleted.Add(kvp.Key);
|
|
}
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("统计:");
|
|
Console.WriteLine(" - 未变化: " + manifest.unchanged.Count);
|
|
Console.WriteLine(" - 已修改: " + manifest.modified.Count);
|
|
Console.WriteLine(" - 新增: " + manifest.new_files.Count);
|
|
Console.WriteLine(" - 删除: " + manifest.deleted.Count);
|
|
|
|
// 生成清单JSON
|
|
string manifestJson = GenerateManifestJson(manifest);
|
|
File.WriteAllText(Path.Combine(tempDir, "patch.json"), manifestJson, Encoding.UTF8);
|
|
|
|
// 打包成ZIP
|
|
if (File.Exists(outputZip))
|
|
File.Delete(outputZip);
|
|
|
|
ZipFile.CreateFromDirectory(tempDir, outputZip, CompressionLevel.Optimal, false);
|
|
|
|
return true;
|
|
}
|
|
finally
|
|
{
|
|
// 清理临时目录
|
|
try
|
|
{
|
|
if (Directory.Exists(tempDir))
|
|
Directory.Delete(tempDir, true);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
private Dictionary<string, FileInfo> GetAllFiles(string rootPath)
|
|
{
|
|
Dictionary<string, FileInfo> files = new Dictionary<string, FileInfo>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (string filePath in Directory.GetFiles(rootPath, "*", SearchOption.AllDirectories))
|
|
{
|
|
string relativePath = filePath.Substring(rootPath.Length).TrimStart('\\', '/');
|
|
files[relativePath] = new FileInfo(filePath);
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
private string CalculateMD5(string filePath)
|
|
{
|
|
using (var md5 = MD5.Create())
|
|
using (var stream = File.OpenRead(filePath))
|
|
{
|
|
byte[] hash = md5.ComputeHash(stream);
|
|
StringBuilder sb = new StringBuilder();
|
|
foreach (byte b in hash)
|
|
sb.Append(b.ToString("x2"));
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
|
|
private bool CreateHDiff(string oldFile, string newFile, string patchFile)
|
|
{
|
|
if (!File.Exists(hdiffzPath))
|
|
{
|
|
Console.WriteLine("警告: hdiffz.exe 未找到,将使用完整文件替换");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
ProcessStartInfo psi = new ProcessStartInfo();
|
|
psi.FileName = hdiffzPath;
|
|
// -m: 使用内存匹配(更快),-c-zlib: 使用zlib压缩
|
|
psi.Arguments = String.Format("-m -c-zlib \"{0}\" \"{1}\" \"{2}\"", oldFile, newFile, patchFile);
|
|
psi.UseShellExecute = false;
|
|
psi.RedirectStandardOutput = true;
|
|
psi.RedirectStandardError = true;
|
|
psi.CreateNoWindow = true;
|
|
|
|
using (Process process = Process.Start(psi))
|
|
{
|
|
process.WaitForExit(60000); // 最多等待60秒
|
|
return process.ExitCode == 0;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("HDiff错误: " + ex.Message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private string GenerateManifestJson(PatchManifest manifest)
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.AppendLine("{");
|
|
sb.AppendLine(" \"from_version\": \"" + EscapeJson(manifest.from_version) + "\",");
|
|
sb.AppendLine(" \"to_version\": \"" + EscapeJson(manifest.to_version) + "\",");
|
|
sb.AppendLine(" \"created_at\": \"" + EscapeJson(manifest.created_at) + "\",");
|
|
|
|
// unchanged
|
|
sb.AppendLine(" \"unchanged\": [");
|
|
for (int i = 0; i < manifest.unchanged.Count; i++)
|
|
{
|
|
sb.Append(" \"" + EscapeJson(manifest.unchanged[i]) + "\"");
|
|
if (i < manifest.unchanged.Count - 1) sb.Append(",");
|
|
sb.AppendLine();
|
|
}
|
|
sb.AppendLine(" ],");
|
|
|
|
// modified
|
|
sb.AppendLine(" \"modified\": [");
|
|
for (int i = 0; i < manifest.modified.Count; i++)
|
|
{
|
|
PatchDiffInfo pdi = manifest.modified[i];
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" \"path\": \"" + EscapeJson(pdi.path) + "\",");
|
|
sb.AppendLine(" \"patch_file\": \"" + EscapeJson(pdi.patch_file) + "\",");
|
|
sb.AppendLine(" \"old_hash\": \"" + pdi.old_hash + "\",");
|
|
sb.AppendLine(" \"new_hash\": \"" + pdi.new_hash + "\",");
|
|
sb.AppendLine(" \"new_size\": " + pdi.new_size);
|
|
sb.Append(" }");
|
|
if (i < manifest.modified.Count - 1) sb.Append(",");
|
|
sb.AppendLine();
|
|
}
|
|
sb.AppendLine(" ],");
|
|
|
|
// new_files
|
|
sb.AppendLine(" \"new_files\": [");
|
|
for (int i = 0; i < manifest.new_files.Count; i++)
|
|
{
|
|
PatchFileInfo pfi = manifest.new_files[i];
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" \"path\": \"" + EscapeJson(pfi.path) + "\",");
|
|
sb.AppendLine(" \"hash\": \"" + pfi.hash + "\",");
|
|
sb.AppendLine(" \"size\": " + pfi.size);
|
|
sb.Append(" }");
|
|
if (i < manifest.new_files.Count - 1) sb.Append(",");
|
|
sb.AppendLine();
|
|
}
|
|
sb.AppendLine(" ],");
|
|
|
|
// deleted
|
|
sb.AppendLine(" \"deleted\": [");
|
|
for (int i = 0; i < manifest.deleted.Count; i++)
|
|
{
|
|
sb.Append(" \"" + EscapeJson(manifest.deleted[i]) + "\"");
|
|
if (i < manifest.deleted.Count - 1) sb.Append(",");
|
|
sb.AppendLine();
|
|
}
|
|
sb.AppendLine(" ]");
|
|
|
|
sb.AppendLine("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
private string EscapeJson(string s)
|
|
{
|
|
if (s == null) return "";
|
|
return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
|
|
}
|
|
|
|
private string FormatFileSize(long bytes)
|
|
{
|
|
if (bytes >= 1073741824)
|
|
return String.Format("{0:F2} GB", bytes / 1073741824.0);
|
|
if (bytes >= 1048576)
|
|
return String.Format("{0:F2} MB", bytes / 1048576.0);
|
|
if (bytes >= 1024)
|
|
return String.Format("{0:F2} KB", bytes / 1024.0);
|
|
return String.Format("{0} B", bytes);
|
|
}
|
|
}
|
|
|
|
// 清单数据结构
|
|
public class PatchManifest
|
|
{
|
|
public string from_version = "";
|
|
public string to_version = "";
|
|
public string created_at = "";
|
|
public List<string> unchanged = new List<string>();
|
|
public List<PatchDiffInfo> modified = new List<PatchDiffInfo>();
|
|
public List<PatchFileInfo> new_files = new List<PatchFileInfo>();
|
|
public List<string> deleted = new List<string>();
|
|
}
|
|
|
|
public class PatchDiffInfo
|
|
{
|
|
public string path = "";
|
|
public string patch_file = "";
|
|
public string old_hash = "";
|
|
public string new_hash = "";
|
|
public long new_size = 0;
|
|
}
|
|
|
|
public class PatchFileInfo
|
|
{
|
|
public string path = "";
|
|
public string hash = "";
|
|
public long size = 0;
|
|
}
|
|
}
|
|
|