diff --git a/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs b/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs index d0143d28..e04b4f06 100644 --- a/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs +++ b/src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs @@ -283,6 +283,62 @@ private IEnumerable ReadFileNode(string path, string rootPath = null) private void ResetId() => Interlocked.Exchange(ref _fileCount, 0); + /// Restore files from backup directory to install path. + public static void Restore(string backupDir, string installPath) + { + if (!Directory.Exists(backupDir)) + throw new DirectoryNotFoundException($"Backup directory not found: {backupDir}"); + + foreach (var file in Directory.GetFiles(backupDir, "*", SearchOption.AllDirectories)) + { + var relativePath = file.Substring(backupDir.Length).TrimStart(Path.DirectorySeparatorChar); + var dest = Path.Combine(installPath, relativePath); + Directory.CreateDirectory(Path.GetDirectoryName(dest)!); + File.Copy(file, dest, true); + } + } + + /// Clean old backups, keeping only the N most recent versions. + public static void CleanBackup(string installPath, int keepVersions = 3) + { + var backupRoot = Path.Combine(installPath, "__backups"); + if (!Directory.Exists(backupRoot)) return; + + var dirs = Directory.GetDirectories(backupRoot) + .Select(d => new DirectoryInfo(d)) + .OrderByDescending(d => d.CreationTime) + .Skip(keepVersions); + + foreach (var dir in dirs) + dir.Delete(true); + } + + /// List backup versions with metadata. + public static IReadOnlyList ListBackups(string installPath) + { + var backupRoot = Path.Combine(installPath, "__backups"); + if (!Directory.Exists(backupRoot)) return Array.Empty(); + + return Directory.GetDirectories(backupRoot) + .Select(d => new DirectoryInfo(d)) + .Select(d => new BackupInfo( + d.Name, d.FullName, d.CreationTime, + d.GetFiles("*", SearchOption.AllDirectories).Sum(f => f.Length))) + .ToList(); + } + #endregion } + + /// Backup configuration. + public sealed class BackupConfig + { + public int KeepVersions { get; set; } = 3; + public string? BackupRoot { get; set; } + public List SkipDirectories { get; set; } = new(); + public bool Enabled { get; set; } = true; + } + + /// Backup metadata. + public record BackupInfo(string Version, string Path, DateTime CreatedAt, long SizeBytes); } \ No newline at end of file diff --git a/src/c#/GeneralUpdate.Core/Hooks/IUpdateHooks.cs b/src/c#/GeneralUpdate.Core/Hooks/IUpdateHooks.cs new file mode 100644 index 00000000..55ee7e2c --- /dev/null +++ b/src/c#/GeneralUpdate.Core/Hooks/IUpdateHooks.cs @@ -0,0 +1,84 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using GeneralUpdate.Core.Configuration; + +namespace GeneralUpdate.Core.Hooks; + +/// Lifecycle hooks for update processes. +public interface IUpdateHooks +{ + Task OnBeforeUpdateAsync(UpdateContext ctx); + Task OnDownloadCompletedAsync(DownloadContext ctx); + Task OnAfterUpdateAsync(UpdateContext ctx); + Task OnUpdateErrorAsync(UpdateContext ctx, Exception ex); + Task OnBeforeStartAppAsync(UpdateContext ctx); +} + +public record UpdateContext( + string AppName, + string InstallPath, + string CurrentVersion, + string? TargetVersion, + int AppType +); + +public record DownloadContext( + string AssetName, + string Version, + long TotalBytes, + TimeSpan Duration, + string? LocalPath, + bool Success +); + +/// Default no-op hooks. +public class NoOpUpdateHooks : IUpdateHooks +{ + public Task OnBeforeUpdateAsync(UpdateContext ctx) => Task.FromResult(true); + public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask; + public Task OnAfterUpdateAsync(UpdateContext ctx) => Task.CompletedTask; + public Task OnUpdateErrorAsync(UpdateContext ctx, Exception ex) => Task.CompletedTask; + public Task OnBeforeStartAppAsync(UpdateContext ctx) => Task.CompletedTask; +} + +/// Unix permission hooks — chmod +x main app before start. +public class UnixPermissionHooks : IUpdateHooks +{ + public async Task OnBeforeStartAppAsync(UpdateContext ctx) + { + var mainApp = Path.Combine(ctx.InstallPath, ctx.AppName); + if (File.Exists(mainApp)) + await Process.Start("chmod", $"+x \"{mainApp}\"").WaitForExitAsync(); + } + public Task OnBeforeUpdateAsync(UpdateContext ctx) => Task.FromResult(true); + public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask; + public Task OnAfterUpdateAsync(UpdateContext ctx) => Task.CompletedTask; + public Task OnUpdateErrorAsync(UpdateContext ctx, Exception ex) => Task.CompletedTask; +} + +/// User-supplied permission script hook. +public class CustomPermissionHooks : IUpdateHooks +{ + private readonly string _scriptPath; + public CustomPermissionHooks(string scriptPath) + => _scriptPath = scriptPath ?? throw new ArgumentNullException(nameof(scriptPath)); + + public async Task OnBeforeStartAppAsync(UpdateContext ctx) + { + var psi = new ProcessStartInfo(_scriptPath, ctx.InstallPath) + { + RedirectStandardOutput = true, RedirectStandardError = true + }; + using var proc = Process.Start(psi)!; + await proc.WaitForExitAsync(); + if (proc.ExitCode != 0) + throw new InvalidOperationException( + $"Permission script '{_scriptPath}' failed (exit {proc.ExitCode})"); + } + public Task OnBeforeUpdateAsync(UpdateContext ctx) => Task.FromResult(true); + public Task OnDownloadCompletedAsync(DownloadContext ctx) => Task.CompletedTask; + public Task OnAfterUpdateAsync(UpdateContext ctx) => Task.CompletedTask; + public Task OnUpdateErrorAsync(UpdateContext ctx, Exception ex) => Task.CompletedTask; +}