using System.IO; using System.IO.Compression; using System.Text.Json; namespace DeskCore; /// Export/Import aller Kategorien samt Einstellungen als ZIP. public static class Backup { private const string CategoriesPrefix = "Kategorien/"; private const string SettingsEntry = "einstellungen.json"; public static long StorageSize() { try { return Directory.EnumerateFiles(Storage.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", }; /// Gibt die Anzahl gesicherter Dateien zurück. public static int Export(string zipPath) { if (File.Exists(zipPath)) File.Delete(zipPath); int count = 0; using var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create); foreach (var cat in Storage.Categories()) { var dir = Storage.CategoryPath(cat); zip.CreateEntry(CategoriesPrefix + cat + "/"); // auch leere Kategorien sichern foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) { var rel = Path.GetRelativePath(Storage.Root, f).Replace('\\', '/'); zip.CreateEntryFromFile(f, CategoriesPrefix + rel, CompressionLevel.Optimal); count++; } foreach (var d in Directory.EnumerateDirectories(dir, "*", SearchOption.AllDirectories)) if (!Directory.EnumerateFileSystemEntries(d).Any()) zip.CreateEntry(CategoriesPrefix + Path.GetRelativePath(Storage.Root, d).Replace('\\', '/') + "/"); } var entry = zip.CreateEntry(SettingsEntry); using (var w = new StreamWriter(entry.Open())) w.Write(JsonSerializer.Serialize(AppSettings.Current, AppSettings.Json)); return count; } /// Führt eine Sicherung mit dem Bestand zusammen. Vorhandenes wird nicht überschrieben. public static (int Added, int Skipped) Import(string zipPath) { int added = 0, skipped = 0; using var zip = ZipFile.OpenRead(zipPath); var root = Path.GetFullPath(Storage.Root); foreach (var e in zip.Entries) { 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++; continue; } Directory.CreateDirectory(Path.GetDirectoryName(target)!); e.ExtractToFile(target); added++; } if (zip.GetEntry(SettingsEntry) is { } se) { try { using var r = new StreamReader(se.Open()); var imported = JsonSerializer.Deserialize(r.ReadToEnd(), AppSettings.Json); if (imported is not null) Merge(imported); } catch { } } return (added, skipped); } 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); s.Save(); } }