Initial commit: R0Installer source, patch generator, API backend and build scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user