Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,62 @@ private IEnumerable<FileNode> ReadFileNode(string path, string rootPath = null)

private void ResetId() => Interlocked.Exchange(ref _fileCount, 0);

/// <summary>Restore files from backup directory to install path.</summary>
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);
}
}
Comment on lines +286 to +299

/// <summary>Clean old backups, keeping only the N most recent versions.</summary>
public static void CleanBackup(string installPath, int keepVersions = 3)
{
var backupRoot = Path.Combine(installPath, "__backups");
if (!Directory.Exists(backupRoot)) return;

Comment on lines +301 to +306
var dirs = Directory.GetDirectories(backupRoot)
.Select(d => new DirectoryInfo(d))
.OrderByDescending(d => d.CreationTime)
.Skip(keepVersions);

foreach (var dir in dirs)
dir.Delete(true);
}

/// <summary>List backup versions with metadata.</summary>
public static IReadOnlyList<BackupInfo> ListBackups(string installPath)
{
var backupRoot = Path.Combine(installPath, "__backups");
if (!Directory.Exists(backupRoot)) return Array.Empty<BackupInfo>();

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
}

/// <summary>Backup configuration.</summary>
public sealed class BackupConfig
{
public int KeepVersions { get; set; } = 3;
public string? BackupRoot { get; set; }
public List<string> SkipDirectories { get; set; } = new();
public bool Enabled { get; set; } = true;
}

/// <summary>Backup metadata.</summary>
public record BackupInfo(string Version, string Path, DateTime CreatedAt, long SizeBytes);
}
84 changes: 84 additions & 0 deletions src/c#/GeneralUpdate.Core/Hooks/IUpdateHooks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using GeneralUpdate.Core.Configuration;

namespace GeneralUpdate.Core.Hooks;

/// <summary>Lifecycle hooks for update processes.</summary>
public interface IUpdateHooks
{
Task<bool> 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
);

/// <summary>Default no-op hooks.</summary>
public class NoOpUpdateHooks : IUpdateHooks
{
public Task<bool> 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;
}

/// <summary>Unix permission hooks — chmod +x main app before start.</summary>
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();
}
Comment on lines +49 to +54
public Task<bool> 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;
}

/// <summary>User-supplied permission script hook.</summary>
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})");
}
Comment on lines +68 to +79
public Task<bool> 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;
}