namespace Updater.Core; /// /// Безопасный установщик: создает резервную копию замененных файлов, выполняет атомарную замену, /// и откатываемся в случае неудачи. /// public sealed class SafeFileInstaller : IInstaller { private readonly ILogger _log; public SafeFileInstaller(ILogger log) => _log = log; public void Install(string sourceDir, string installPath) { var backupDir = Path.Combine(Path.GetTempPath(), $"upd_backup_{Guid.NewGuid():N}"); Directory.CreateDirectory(backupDir); var completed = false; try { // Копирование файлов с резервной копией целей foreach (var src in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories)) { var rel = Path.GetRelativePath(sourceDir, src); var dst = Path.Combine(installPath, rel); var dstDir = Path.GetDirectoryName(dst)!; if (!Directory.Exists(dstDir)) Directory.CreateDirectory(dstDir); if (File.Exists(dst)) { var bkp = Path.Combine(backupDir, rel); var bkpDir = Path.GetDirectoryName(bkp); if (!Directory.Exists(bkpDir)) Directory.CreateDirectory(bkpDir!); File.Copy(dst, bkp, overwrite: true); } File.Copy(src, dst, overwrite: true); } completed = true; _log.Info("Install completed successfully."); } catch (Exception ex) { _log.Error($"Install failed: {ex.Message}"); _log.Warn("Rolling back..."); Rollback(backupDir, installPath); throw; } finally { // Очистка резервной копии только в случае успеха if (completed) { TryDeleteDirectory(backupDir); } } } private static void Rollback(string backupDir, string installPath) { if (!Directory.Exists(backupDir)) return; foreach (var bkp in Directory.EnumerateFiles(backupDir, "*", SearchOption.AllDirectories)) { var rel = Path.GetRelativePath(backupDir, bkp); var dst = Path.Combine(installPath, rel); var dstDir = Path.GetDirectoryName(dst); if (!Directory.Exists(dstDir)) Directory.CreateDirectory(dstDir!); File.Copy(bkp, dst, overwrite: true); } } private static void TryDeleteDirectory(string dir) { try { Directory.Delete(dir, true); } catch { /* ignore */ } } }