Backup-Oberflaeche im Settings-Tab
Der BackupService bekommt eine Bedienoberflaeche im neuen Unterreiter "Backup". Aufbau als eigenes UserControl (UI/BackupPanel) statt direkt in frm_main.Designer.cs. Diese Datei ist ueber 1250 Zeilen gewachsene Handarbeit; ein Fehler beim Bearbeiten waere teuer. Eingehaengt wird das Panel mit vier Zeilen. Drei Bereiche: Sicherung erstellen (Zielordner, Umgang mit Zugangsdaten, Umfang), automatische Sicherung (Uhrzeit, Aufbewahrungszahl) und die Liste vorhandener Sicherungen mit Wiederherstellen, Anzeigen und Loeschen. Die Liste liest das Manifest jeder Datei und zeigt Instanz und ob Zugangsdaten enthalten sind; unlesbare oder fremde Archive werden ausgegraut mitgezeigt, statt sie zu verschweigen. Zwei Entscheidungen praegen das Verhalten. Die Passphrase wird nirgends gespeichert. Laege sie neben den Sicherungen, waere die Verschluesselung wirkungslos. Sie muss deshalb bei jeder geschuetzten Sicherung neu eingegeben und wiederholt werden, und vor dem Erstellen wird ausdruecklich bestaetigt, dass sie notiert ist — ohne sie sind die Zugangsdaten unwiederbringlich. Die Automatik sichert aus demselben Grund ohne Zugangsdaten. Wiederhergestellt wird standardmaessig in einen NEUEN Ordner neben der Instanz, nicht ueber die laufende. Diese haelt Chatverlaeufe im Speicher und die Datenbank geoeffnet — ein Ueberschreiben im Betrieb wuerde teils sofort wieder ueberschrieben und teils scheitern. Der Dialog benennt das. Vor dem Schreiben laeuft immer ein Probelauf: Er prueft die Pruefsummen und zaehlt die Konflikte, sodass ein beschaedigtes Archiv auffaellt und Ueberschreiben bestaetigt wird, bevor etwas passiert. BackupScheduler sichert taeglich zur eingestellten Uhrzeit und wendet die Rotation an. Der Zeitpunkt wird bei jedem Durchlauf neu aus den Einstellungen gelesen, damit eine Aenderung ohne Neustart greift. Die Oberflaeche ist noch nicht optisch geprueft — die Dev-Instanz haelt einen echten API-Key und wuerde beim Start Agenten ausfuehren. 385 Tests gruen (237 Core, 148 Tools). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b55512e734
commit
b51cc29667
@@ -0,0 +1,511 @@
|
||||
using ClawdDotNet.Core.Backup;
|
||||
using ClawdDotNet.Models;
|
||||
using ClawdDotNet.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Bedienoberfläche für Sicherung und Wiederherstellung einer Instanz.
|
||||
///
|
||||
/// Zwei Entscheidungen prägen das Verhalten:
|
||||
///
|
||||
/// Die Passphrase wird nirgends gespeichert. Läge sie neben den Sicherungen, wäre die
|
||||
/// Verschlüsselung wirkungslos. Deshalb muss sie bei jeder geschützten Sicherung neu
|
||||
/// eingegeben werden, und die Automatik sichert ohne Zugangsdaten.
|
||||
///
|
||||
/// Wiederhergestellt wird standardmäßig in einen NEUEN Ordner, nicht über die laufende
|
||||
/// Instanz. Diese hält Chatverläufe im Speicher und die Datenbank geöffnet — ein
|
||||
/// Überschreiben im Betrieb würde teils sofort wieder überschrieben und teils
|
||||
/// scheitern.
|
||||
/// </summary>
|
||||
public sealed partial class BackupPanel : UserControl
|
||||
{
|
||||
private readonly BackupService _service = new();
|
||||
|
||||
private SettingsManager? _settingsManager;
|
||||
private ILogger? _logger;
|
||||
private string _instanceDir = "";
|
||||
private string _instanceName = "";
|
||||
private bool _loading;
|
||||
|
||||
public BackupPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
WireEvents();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aktualisiert die Liste von außen — etwa nachdem die Automatik gesichert hat.
|
||||
/// </summary>
|
||||
public void RefreshFromOutside()
|
||||
{
|
||||
if (!IsHandleCreated || IsDisposed)
|
||||
return;
|
||||
|
||||
RefreshBackupList();
|
||||
}
|
||||
|
||||
/// <summary>Wird vom Hauptfenster nach dem Laden der Instanz aufgerufen.</summary>
|
||||
public void Initialize(string instanceDir, string instanceName,
|
||||
SettingsManager settingsManager, ILogger logger)
|
||||
{
|
||||
_instanceDir = instanceDir;
|
||||
_instanceName = instanceName;
|
||||
_settingsManager = settingsManager;
|
||||
_logger = logger;
|
||||
|
||||
LoadSettings();
|
||||
UpdateEnabledState();
|
||||
RefreshBackupList();
|
||||
}
|
||||
|
||||
private void WireEvents()
|
||||
{
|
||||
btn_browseFolder.Click += OnBrowseFolder;
|
||||
btn_createBackup.Click += async (_, _) => await CreateBackupAsync();
|
||||
btn_refresh.Click += (_, _) => RefreshBackupList();
|
||||
btn_restore.Click += async (_, _) => await RestoreSelectedAsync();
|
||||
btn_showInFolder.Click += (_, _) => ShowSelectedInFolder();
|
||||
btn_delete.Click += (_, _) => DeleteSelected();
|
||||
|
||||
rb_secretsExclude.CheckedChanged += (_, _) => UpdateEnabledState();
|
||||
rb_secretsPassphrase.CheckedChanged += (_, _) => UpdateEnabledState();
|
||||
|
||||
chk_autoEnabled.CheckedChanged += (_, _) => { UpdateEnabledState(); SaveSettings(); };
|
||||
dtp_autoTime.ValueChanged += (_, _) => SaveSettings();
|
||||
num_keepCount.ValueChanged += (_, _) => SaveSettings();
|
||||
txt_targetFolder.Leave += (_, _) => SaveSettings();
|
||||
|
||||
lv_backups.SelectedIndexChanged += (_, _) => UpdateEnabledState();
|
||||
lv_backups.DoubleClick += async (_, _) => await RestoreSelectedAsync();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Einstellungen
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
if (_settingsManager is null) return;
|
||||
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var settings = _settingsManager.AppSettings;
|
||||
|
||||
txt_targetFolder.Text = Path.GetFullPath(settings.BackupDirectory);
|
||||
chk_autoEnabled.Checked = settings.AutoBackupEnabled;
|
||||
num_keepCount.Value = Math.Clamp(settings.BackupKeepCount, 0, 999);
|
||||
|
||||
dtp_autoTime.Value = TimeSpan.TryParse(settings.AutoBackupTime, out var time)
|
||||
? DateTime.Today.Add(time)
|
||||
: DateTime.Today.AddHours(3);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
if (_loading || _settingsManager is null) return;
|
||||
|
||||
var settings = _settingsManager.AppSettings;
|
||||
settings.BackupDirectory = txt_targetFolder.Text.Trim();
|
||||
settings.AutoBackupEnabled = chk_autoEnabled.Checked;
|
||||
settings.AutoBackupTime = dtp_autoTime.Value.ToString("HH:mm");
|
||||
settings.BackupKeepCount = (int)num_keepCount.Value;
|
||||
|
||||
_settingsManager.Save();
|
||||
}
|
||||
|
||||
private void UpdateEnabledState()
|
||||
{
|
||||
var withPassphrase = rb_secretsPassphrase.Checked;
|
||||
label_passphrase.Enabled = withPassphrase;
|
||||
txt_passphrase.Enabled = withPassphrase;
|
||||
label_passphraseRepeat.Enabled = withPassphrase;
|
||||
txt_passphraseRepeat.Enabled = withPassphrase;
|
||||
|
||||
dtp_autoTime.Enabled = chk_autoEnabled.Checked;
|
||||
num_keepCount.Enabled = chk_autoEnabled.Checked;
|
||||
|
||||
var hasSelection = lv_backups.SelectedItems.Count > 0;
|
||||
btn_restore.Enabled = hasSelection;
|
||||
btn_showInFolder.Enabled = hasSelection;
|
||||
btn_delete.Enabled = hasSelection;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Sichern
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private void OnBrowseFolder(object? sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new FolderBrowserDialog
|
||||
{
|
||||
Description = "Ordner für Sicherungen wählen",
|
||||
UseDescriptionForTitle = true,
|
||||
SelectedPath = Directory.Exists(txt_targetFolder.Text)
|
||||
? txt_targetFolder.Text
|
||||
: Environment.CurrentDirectory
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
txt_targetFolder.Text = dialog.SelectedPath;
|
||||
SaveSettings();
|
||||
RefreshBackupList();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateBackupAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_instanceDir))
|
||||
return;
|
||||
|
||||
var options = BuildOptions();
|
||||
if (options is null)
|
||||
return; // Eingabefehler, bereits gemeldet
|
||||
|
||||
var folder = txt_targetFolder.Text.Trim();
|
||||
var file = Path.Combine(folder, BuildFileName());
|
||||
|
||||
btn_createBackup.Enabled = false;
|
||||
SetStatus("Sicherung läuft…", SystemColors.GrayText);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await Task.Run(() => _service.CreateAsync(_instanceDir, file, options));
|
||||
|
||||
SetStatus($"Fertig: {Path.GetFileName(result.ZipPath)} ({FormatSize(result.SizeBytes)})",
|
||||
Color.FromArgb(0, 128, 0));
|
||||
|
||||
_logger?.LogInformation("Sicherung erstellt: {Path} ({Size} Bytes)",
|
||||
result.ZipPath, result.SizeBytes);
|
||||
|
||||
txt_passphrase.Clear();
|
||||
txt_passphraseRepeat.Clear();
|
||||
|
||||
ApplyRotation(folder);
|
||||
RefreshBackupList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus("Fehlgeschlagen", Color.Firebrick);
|
||||
_logger?.LogError(ex, "Sicherung fehlgeschlagen");
|
||||
MessageBox.Show(this, $"Die Sicherung ist fehlgeschlagen:\n\n{ex.Message}",
|
||||
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
btn_createBackup.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Liest die Eingaben. Gibt null zurück, wenn etwas fehlt.</summary>
|
||||
private BackupOptions? BuildOptions()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(txt_targetFolder.Text))
|
||||
{
|
||||
MessageBox.Show(this, "Bitte einen Zielordner angeben.",
|
||||
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!rb_secretsPassphrase.Checked)
|
||||
{
|
||||
return new BackupOptions
|
||||
{
|
||||
Secrets = SecretMode.Exclude,
|
||||
IncludeLogs = chk_includeLogs.Checked,
|
||||
IncludeChatHistory = chk_includeChatHistory.Checked
|
||||
};
|
||||
}
|
||||
|
||||
var passphrase = txt_passphrase.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(passphrase))
|
||||
{
|
||||
MessageBox.Show(this, "Bitte eine Passphrase eingeben.",
|
||||
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
txt_passphrase.Focus();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (passphrase != txt_passphraseRepeat.Text)
|
||||
{
|
||||
MessageBox.Show(this, "Die beiden Passphrasen stimmen nicht überein.",
|
||||
"Sicherung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
txt_passphraseRepeat.Focus();
|
||||
txt_passphraseRepeat.SelectAll();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Eine vergessene Passphrase bedeutet unwiederbringlich verlorene Zugangsdaten.
|
||||
var answer = MessageBox.Show(this,
|
||||
"Ohne diese Passphrase lassen sich die Zugangsdaten in der Sicherung " +
|
||||
"nicht wiederherstellen. Es gibt keinen Ersatzweg.\n\n" +
|
||||
"Passphrase sicher notiert?",
|
||||
"Passphrase bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
|
||||
|
||||
if (answer != DialogResult.Yes)
|
||||
return null;
|
||||
|
||||
return new BackupOptions
|
||||
{
|
||||
Secrets = SecretMode.Passphrase,
|
||||
Passphrase = passphrase,
|
||||
IncludeLogs = chk_includeLogs.Checked,
|
||||
IncludeChatHistory = chk_includeChatHistory.Checked
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildFileName()
|
||||
{
|
||||
var safeName = string.Concat(
|
||||
(_instanceName.Length > 0 ? _instanceName : "Instanz")
|
||||
.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
|
||||
|
||||
return $"backup_{safeName}_{DateTime.Now:yyyy-MM-dd_HHmm}.zip";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Liste
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private void RefreshBackupList()
|
||||
{
|
||||
lv_backups.BeginUpdate();
|
||||
lv_backups.Items.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var folder = txt_targetFolder.Text.Trim();
|
||||
if (!Directory.Exists(folder))
|
||||
return;
|
||||
|
||||
var files = new DirectoryInfo(folder)
|
||||
.GetFiles("*.zip")
|
||||
.OrderByDescending(f => f.LastWriteTime);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var item = new ListViewItem(file.Name) { Tag = file.FullName };
|
||||
item.SubItems.Add(file.LastWriteTime.ToString("g"));
|
||||
|
||||
// Das Manifest verrät Instanz und ob Zugangsdaten enthalten sind.
|
||||
try
|
||||
{
|
||||
var manifest = _service.InspectAsync(file.FullName).GetAwaiter().GetResult();
|
||||
item.SubItems.Add(manifest.InstanceName);
|
||||
item.SubItems.Add(FormatSize(file.Length));
|
||||
item.SubItems.Add(manifest.HasSecrets
|
||||
? $"ja ({manifest.SecretCount}, Passphrase)"
|
||||
: "nein");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fremde oder beschädigte Archive trotzdem anzeigen — der Benutzer
|
||||
// soll sehen, was im Ordner liegt.
|
||||
item.SubItems.Add("—");
|
||||
item.SubItems.Add(FormatSize(file.Length));
|
||||
item.SubItems.Add("unlesbar");
|
||||
item.ForeColor = SystemColors.GrayText;
|
||||
}
|
||||
|
||||
lv_backups.Items.Add(item);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lv_backups.EndUpdate();
|
||||
UpdateEnabledState();
|
||||
}
|
||||
}
|
||||
|
||||
private string? SelectedBackupPath
|
||||
=> lv_backups.SelectedItems.Count > 0
|
||||
? lv_backups.SelectedItems[0].Tag as string
|
||||
: null;
|
||||
|
||||
private void ShowSelectedInFolder()
|
||||
{
|
||||
if (SelectedBackupPath is not { } path) return;
|
||||
|
||||
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
|
||||
}
|
||||
|
||||
private void DeleteSelected()
|
||||
{
|
||||
if (SelectedBackupPath is not { } path) return;
|
||||
|
||||
var answer = MessageBox.Show(this,
|
||||
$"Sicherung endgültig löschen?\n\n{Path.GetFileName(path)}",
|
||||
"Löschen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
|
||||
|
||||
if (answer != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
RefreshBackupList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, $"Konnte nicht gelöscht werden:\n\n{ex.Message}",
|
||||
"Löschen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Wiederherstellen
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private async Task RestoreSelectedAsync()
|
||||
{
|
||||
if (SelectedBackupPath is not { } zipPath) return;
|
||||
|
||||
BackupManifest manifest;
|
||||
try
|
||||
{
|
||||
manifest = await _service.InspectAsync(zipPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, $"Die Sicherung ist nicht lesbar:\n\n{ex.Message}",
|
||||
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
using var dialog = new frm_RestoreBackup(manifest, SuggestRestoreTarget(manifest));
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var options = new RestoreOptions
|
||||
{
|
||||
Passphrase = dialog.Passphrase,
|
||||
Overwrite = dialog.Overwrite
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Erst ohne zu schreiben prüfen — Prüfsummen und Konflikte.
|
||||
var preview = await Task.Run(() =>
|
||||
_service.RestoreAsync(zipPath, dialog.TargetDirectory,
|
||||
options with { DryRun = true, Overwrite = true }));
|
||||
|
||||
if (preview.WouldOverwrite.Count > 0 && !dialog.Overwrite)
|
||||
{
|
||||
var answer = MessageBox.Show(this,
|
||||
$"Im Zielordner liegen bereits {preview.WouldOverwrite.Count} der enthaltenen " +
|
||||
"Dateien.\n\nVorhandene Dateien überschreiben?",
|
||||
"Wiederherstellen", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
|
||||
|
||||
if (answer == DialogResult.Cancel)
|
||||
return;
|
||||
|
||||
options = options with { Overwrite = answer == DialogResult.Yes };
|
||||
}
|
||||
|
||||
var result = await Task.Run(() =>
|
||||
_service.RestoreAsync(zipPath, dialog.TargetDirectory, options));
|
||||
|
||||
_logger?.LogInformation("Wiederherstellung nach {Target}: {Written} Dateien, {Skipped} übersprungen",
|
||||
dialog.TargetDirectory, result.Written.Count, result.Skipped.Count);
|
||||
|
||||
var summary = $"{result.Written.Count} Datei(en) wiederhergestellt nach:\n{dialog.TargetDirectory}";
|
||||
if (result.Skipped.Count > 0)
|
||||
summary += $"\n\n{result.Skipped.Count} vorhandene Datei(en) blieben unangetastet.";
|
||||
|
||||
if (!manifest.HasSecrets && manifest.SecretCount > 0)
|
||||
summary += "\n\nHinweis: Diese Sicherung enthält keine Zugangsdaten. " +
|
||||
"API-Schlüssel und Passwörter müssen neu eingetragen werden.";
|
||||
|
||||
summary += "\n\nDie wiederhergestellte Instanz wird erst nach einem Neustart " +
|
||||
"der Anwendung sichtbar.";
|
||||
|
||||
MessageBox.Show(this, summary, "Wiederherstellen",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Wiederherstellung fehlgeschlagen");
|
||||
MessageBox.Show(this, $"Die Wiederherstellung ist fehlgeschlagen:\n\n{ex.Message}",
|
||||
"Wiederherstellen", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schlägt einen neuen Ordner neben der Instanz vor, nicht die laufende Instanz
|
||||
/// selbst — diese hält Dateien geöffnet und würde Teile sofort überschreiben.
|
||||
/// </summary>
|
||||
private string SuggestRestoreTarget(BackupManifest manifest)
|
||||
{
|
||||
var parent = Directory.GetParent(_instanceDir)?.FullName ?? _instanceDir;
|
||||
var name = string.IsNullOrWhiteSpace(manifest.InstanceName)
|
||||
? "Instanz"
|
||||
: manifest.InstanceName;
|
||||
|
||||
var safeName = string.Concat(
|
||||
name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
|
||||
|
||||
return Path.Combine(parent, $"Instance-{safeName}_wiederhergestellt_{DateTime.Now:yyyyMMdd_HHmm}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Rotation
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
/// <summary>Entfernt die ältesten Sicherungen dieser Instanz über der Aufbewahrungszahl.</summary>
|
||||
private void ApplyRotation(string folder)
|
||||
{
|
||||
var keep = (int)num_keepCount.Value;
|
||||
if (keep <= 0 || !Directory.Exists(folder))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var prefix = $"backup_{_instanceName}_";
|
||||
|
||||
var obsolete = new DirectoryInfo(folder)
|
||||
.GetFiles("*.zip")
|
||||
.Where(f => f.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(f => f.LastWriteTime)
|
||||
.Skip(keep)
|
||||
.ToList();
|
||||
|
||||
foreach (var file in obsolete)
|
||||
{
|
||||
file.Delete();
|
||||
_logger?.LogInformation("Alte Sicherung entfernt: {Name}", file.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Aufräumen darf eine erfolgreiche Sicherung nicht nachträglich entwerten.
|
||||
_logger?.LogWarning(ex, "Rotation der Sicherungen fehlgeschlagen");
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// Kleinkram
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
private void SetStatus(string text, Color color)
|
||||
{
|
||||
label_createStatus.Text = text;
|
||||
label_createStatus.ForeColor = color;
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes) => bytes switch
|
||||
{
|
||||
< 1024 => $"{bytes} B",
|
||||
< 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
|
||||
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:F1} MB",
|
||||
_ => $"{bytes / 1024.0 / 1024 / 1024:F2} GB"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user