Files
2026-10-03 19:51:56 +00:00

182 lines
8.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.IO;
using System.IO.Compression;
using System.Text.Json;
namespace DeskCore;
/// <summary>Export/Import aller Kategorien samt Einstellungen als ZIP – mit Fortschritt und Abbrechen.</summary>
public static class Backup
{
private const string CategoriesPrefix = "Kategorien/";
private const string SettingsEntry = "einstellungen.json";
private const int Chunk = 1 << 20;
public static long StorageSize() => StorageSize(Storage.Root);
public static long StorageSize(string root)
{
try
{
return Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)
.Sum(f => { try { return new FileInfo(f).Length; } catch { return 0L; } });
}
catch { return 0; }
}
public static string FormatSize(long bytes) => bytes switch
{
< 1024 * 1024 => $"{Math.Max(1, bytes / 1024)} KB",
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:0.#} MB",
_ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB",
};
/// <summary>Sichert alle Kategorien. Gibt die Anzahl gesicherter Dateien zurück.</summary>
public static int Export(string zipPath, Action<long>? onBytes = null, CancellationToken ct = default) =>
Export(zipPath, Storage.Root, Storage.Categories(), JsonSerializer.Serialize(AppSettings.Current, AppSettings.Json), onBytes, ct);
public static int Export(string zipPath, string root, IEnumerable<string> categories, string settingsJson,
Action<long>? onBytes = null, CancellationToken ct = default)
{
var tmp = zipPath + ".teil";
int count = 0;
try
{
if (File.Exists(tmp)) File.Delete(tmp);
// große Sicherungen bestehen meist aus Videos/Bildern → stärkere Kompression bringt kaum etwas
var level = StorageSize(root) > 1L << 30 ? CompressionLevel.Fastest : CompressionLevel.Optimal;
using (var zip = ZipFile.Open(tmp, ZipArchiveMode.Create))
{
foreach (var cat in categories)
{
var dir = Path.Combine(root, cat);
zip.CreateEntry(CategoriesPrefix + cat + "/"); // auch leere Kategorien sichern
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
{
ct.ThrowIfCancellationRequested();
var rel = Path.GetRelativePath(root, f).Replace('\\', '/');
var entry = zip.CreateEntry(CategoriesPrefix + rel, level);
entry.LastWriteTime = File.GetLastWriteTime(f);
using (var input = new FileStream(f, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, Chunk, FileOptions.SequentialScan))
using (var output = entry.Open())
CopyWithProgress(input, output, onBytes, ct);
count++;
}
foreach (var d in Directory.EnumerateDirectories(dir, "*", SearchOption.AllDirectories))
if (!Directory.EnumerateFileSystemEntries(d).Any())
zip.CreateEntry(CategoriesPrefix + Path.GetRelativePath(root, d).Replace('\\', '/') + "/");
}
var se = zip.CreateEntry(SettingsEntry);
using (var w = new StreamWriter(se.Open()))
w.Write(settingsJson);
}
// erst am Ende an den richtigen Namen – so bleibt nie eine halbe Sicherung mit gültigem Namen liegen
if (File.Exists(zipPath)) File.Delete(zipPath);
File.Move(tmp, zipPath);
return count;
}
catch
{
try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
throw;
}
}
/// <summary>Summe der unkomprimierten Größen (für die Fortschrittsanzeige beim Wiederherstellen).</summary>
public static long ArchiveSize(string zipPath)
{
using var zip = ZipFile.OpenRead(zipPath);
return zip.Entries.Sum(e => e.Length);
}
/// <summary>Führt eine Sicherung mit dem Bestand zusammen. Vorhandenes wird nicht überschrieben.</summary>
public static (int Added, int Skipped) Import(string zipPath, Action<long>? onBytes = null, CancellationToken ct = default)
{
var (added, skipped, settings) = Import(zipPath, Storage.Root, onBytes, ct);
if (settings is not null) Merge(settings);
return (added, skipped);
}
public static (int Added, int Skipped, AppSettings? Settings) Import(string zipPath, string root,
Action<long>? onBytes = null, CancellationToken ct = default)
{
int added = 0, skipped = 0;
AppSettings? settings = null;
using var zip = ZipFile.OpenRead(zipPath);
root = Path.GetFullPath(root);
foreach (var e in zip.Entries)
{
ct.ThrowIfCancellationRequested();
if (e.FullName.Equals(SettingsEntry, StringComparison.OrdinalIgnoreCase))
{
try
{
using var r = new StreamReader(e.Open());
settings = JsonSerializer.Deserialize<AppSettings>(r.ReadToEnd(), AppSettings.Json);
}
catch { }
continue;
}
if (!e.FullName.StartsWith(CategoriesPrefix, StringComparison.OrdinalIgnoreCase)) continue;
var rel = e.FullName[CategoriesPrefix.Length..];
if (rel.Length == 0) continue;
var target = Path.GetFullPath(Path.Combine(root, rel));
if (!Storage.IsUnder(target, root)) continue; // Schutz vor manipulierten ZIPs
if (e.FullName.EndsWith('/'))
{
Directory.CreateDirectory(target);
continue;
}
if (File.Exists(target)) { skipped++; onBytes?.Invoke(e.Length); continue; }
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
try
{
using (var input = e.Open())
using (var output = new FileStream(target, FileMode.CreateNew, FileAccess.Write, FileShare.None, Chunk))
CopyWithProgress(input, output, onBytes, ct);
File.SetLastWriteTime(target, e.LastWriteTime.DateTime);
}
catch (OperationCanceledException)
{
try { File.Delete(target); } catch { } // halbe Datei nicht liegen lassen
throw;
}
added++;
}
return (added, skipped, settings);
}
private static void CopyWithProgress(Stream input, Stream output, Action<long>? onBytes, CancellationToken ct)
{
var buffer = new byte[Chunk];
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ct.ThrowIfCancellationRequested();
output.Write(buffer, 0, read);
onBytes?.Invoke(read);
}
}
private static void Merge(AppSettings from)
{
var s = AppSettings.Current;
foreach (var (name, meta) in from.Categories ?? [])
if (!s.Categories.ContainsKey(name)) s.Categories[name] = meta;
foreach (var n in from.CategoryOrder ?? [])
if (!s.CategoryOrder.Contains(n, StringComparer.OrdinalIgnoreCase)) s.CategoryOrder.Add(n);
foreach (var r in from.Rules ?? [])
if (!s.Rules.Any(x => string.Equals(x.Contains, r.Contains, StringComparison.OrdinalIgnoreCase) &&
string.Equals(x.Category, r.Category, StringComparison.OrdinalIgnoreCase)))
s.Rules.Add(r);
foreach (var g in from.LaunchGroups ?? [])
if (!s.LaunchGroups.Any(x => string.Equals(x.Name, g.Name, StringComparison.OrdinalIgnoreCase)))
s.LaunchGroups.Add(g);
foreach (var snap in from.Snapshots ?? [])
if (!s.Snapshots.Any(x => string.Equals(x.Name, snap.Name, StringComparison.OrdinalIgnoreCase)))
s.Snapshots.Add(snap);
s.Save();
}
}