fix(files): report success cancellation and errors with safe next actions (U02)
This commit is contained in:
@@ -20,6 +20,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IEntryService, EntryService>();
|
||||
services.AddScoped<IBalanceQueryService, BalanceQueryService>();
|
||||
services.AddScoped<IPdfStatementService, PdfStatementService>();
|
||||
services.AddSingleton<IFileDialogService, FileDialogService>();
|
||||
services.AddScoped<IFileSaveService, FileSaveService>();
|
||||
services.AddSingleton<IBackupService, BackupService>();
|
||||
services.AddSingleton<ISettingsService, SettingsService>();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Duempelkas.App.Services.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Duempelkas.App.Services;
|
||||
@@ -9,30 +10,35 @@ public class BackupService : IBackupService
|
||||
{
|
||||
private readonly DatabaseLocation location;
|
||||
|
||||
public BackupService(DatabaseLocation location) => this.location = location;
|
||||
|
||||
public async Task<string> CreateBackupAsync()
|
||||
private readonly IFileDialogService dialogs;
|
||||
public BackupService(DatabaseLocation location, IFileDialogService? dialogs = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = await ShowSaveDialogAsync($"duempelkas-backup-{DateTime.Now:yyyyMMdd-HHmmss}.bak");
|
||||
if (string.IsNullOrWhiteSpace(path)) return "Sicherung wurde abgebrochen.";
|
||||
await CreateBackupToAsync(path);
|
||||
return $"Sicherung gespeichert: {path}";
|
||||
}
|
||||
catch (Exception ex) { return $"Sicherung fehlgeschlagen: {ex.Message}"; }
|
||||
this.location = location;
|
||||
this.dialogs = dialogs ?? new FileDialogService();
|
||||
}
|
||||
|
||||
public async Task<string> RestoreBackupAsync()
|
||||
public async Task<FileOperationResult> CreateBackupAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = await ShowOpenDialogAsync();
|
||||
if (string.IsNullOrWhiteSpace(path)) return "Wiederherstellung wurde abgebrochen.";
|
||||
var fallback = await RestoreFromAsync(path);
|
||||
return $"Wiederherstellung erfolgreich. Rückfallsicherung: {fallback}";
|
||||
var path = await dialogs.SaveAsync($"duempelkas-backup-{DateTime.Now:yyyyMMdd-HHmmss}.bak");
|
||||
if (string.IsNullOrWhiteSpace(path)) return FileOperationResult.Cancelled();
|
||||
await CreateBackupToAsync(path);
|
||||
return new(FileOperationStatus.Success, "Sicherung gespeichert.", path);
|
||||
}
|
||||
catch (Exception ex) { return $"Wiederherstellung fehlgeschlagen: {ex.Message}"; }
|
||||
catch (Exception ex) { return FileOperationResult.Error(ex); }
|
||||
}
|
||||
|
||||
public async Task<FileOperationResult> RestoreBackupAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = await dialogs.OpenBackupAsync();
|
||||
if (string.IsNullOrWhiteSpace(path)) return FileOperationResult.Cancelled();
|
||||
var fallback = await RestoreFromAsync(path);
|
||||
return new(FileOperationStatus.Success, "Sicherung zurückgespielt. Die bisherigen Daten liegen in der Rückfallsicherung:", fallback);
|
||||
}
|
||||
catch (Exception ex) { return FileOperationResult.Error(ex); }
|
||||
}
|
||||
|
||||
public async Task CreateBackupToAsync(string path)
|
||||
@@ -124,74 +130,4 @@ public class BackupService : IBackupService
|
||||
_ = db.TransferLinks.ToArray();
|
||||
}
|
||||
|
||||
private static Task<string?> ShowSaveDialogAsync(string suggestedFileName)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
return Path.Combine(documentsPath, suggestedFileName);
|
||||
}
|
||||
|
||||
var script = $@"
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$dialog = New-Object System.Windows.Forms.SaveFileDialog
|
||||
$dialog.Filter = 'Backup-Dateien (*.bak)|*.bak'
|
||||
$dialog.DefaultExt = 'bak'
|
||||
$dialog.AddExtension = $true
|
||||
$dialog.OverwritePrompt = $true
|
||||
$dialog.FileName = '{suggestedFileName.Replace("'", "''")}'
|
||||
$dialog.Title = 'Backup speichern'
|
||||
if ($dialog.ShowDialog() -eq 'OK') {{ $dialog.FileName }} else {{ '' }}
|
||||
";
|
||||
|
||||
return RunPowerShellDialog(script);
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<string?> ShowOpenDialogAsync()
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var script = @"
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$dialog = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$dialog.Filter = 'Backup-Dateien (*.bak)|*.bak'
|
||||
$dialog.Multiselect = $false
|
||||
$dialog.Title = 'Backup auswählen'
|
||||
if ($dialog.ShowDialog() -eq 'OK') { $dialog.FileName } else { '' }
|
||||
";
|
||||
|
||||
return RunPowerShellDialog(script);
|
||||
});
|
||||
}
|
||||
|
||||
private static string? RunPowerShellDialog(string script)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell",
|
||||
Arguments = $"-NoProfile -Command \"{script.Replace("\"", "\\\"") }\"",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit();
|
||||
|
||||
return string.IsNullOrWhiteSpace(result) ? null : result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Duempelkas.App.Services;
|
||||
|
||||
namespace Duempelkas.Infrastructure.Services;
|
||||
|
||||
public sealed class FileDialogService : IFileDialogService
|
||||
{
|
||||
public Task<string?> SaveAsync(string suggestedFileName) => ShowAsync(suggestedFileName);
|
||||
public Task<string?> OpenBackupAsync() => ShowAsync(null);
|
||||
|
||||
private static Task<string?> ShowAsync(string? suggestedFileName) => Task.Run(() =>
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
throw new PlatformNotSupportedException("Der Dateidialog ist auf diesem System nicht verfügbar.");
|
||||
var save = suggestedFileName != null;
|
||||
var extension = save ? (Path.GetExtension(suggestedFileName) ?? "") : ".bak";
|
||||
var script = $$"""
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$dialog = New-Object System.Windows.Forms.{{(save ? "SaveFileDialog" : "OpenFileDialog")}}
|
||||
$dialog.Filter = 'Dateien (*{{extension.Replace("'", "''")}})|*{{extension.Replace("'", "''")}}'
|
||||
$dialog.Title = '{{(save ? "Datei speichern" : "Sicherung auswählen")}}'
|
||||
{{(save ? "$dialog.OverwritePrompt = $true\n$dialog.AddExtension = $true\n$dialog.DefaultExt = '" + extension.TrimStart('.').Replace("'", "''") + "'\n$dialog.FileName = '" + suggestedFileName!.Replace("'", "''") + "'" : "$dialog.Multiselect = $false")}}
|
||||
try { if ($dialog.ShowDialog() -eq 'OK') { [Console]::Write($dialog.FileName) } } finally { $dialog.Dispose() }
|
||||
""";
|
||||
var start = new ProcessStartInfo("powershell.exe")
|
||||
{
|
||||
RedirectStandardOutput = true, RedirectStandardError = true,
|
||||
StandardOutputEncoding = Encoding.UTF8, UseShellExecute = false, CreateNoWindow = true
|
||||
};
|
||||
start.ArgumentList.Add("-NoProfile");
|
||||
start.ArgumentList.Add("-STA");
|
||||
start.ArgumentList.Add("-EncodedCommand");
|
||||
start.ArgumentList.Add(Convert.ToBase64String(Encoding.Unicode.GetBytes(script)));
|
||||
using var process = Process.Start(start) ?? throw new IOException("Der Dateidialog konnte nicht gestartet werden.");
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
if (process.ExitCode != 0) throw new IOException("Der Dateidialog konnte nicht geöffnet werden. " + error);
|
||||
return string.IsNullOrWhiteSpace(output) ? null : output;
|
||||
});
|
||||
}
|
||||
@@ -1,90 +1,49 @@
|
||||
using Duempelkas.App.Services.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Duempelkas.App.Services;
|
||||
|
||||
namespace Duempelkas.Infrastructure.Services;
|
||||
|
||||
public class FileSaveService : IFileSaveService
|
||||
public class FileSaveService(IFileDialogService dialogs) : IFileSaveService
|
||||
{
|
||||
public async Task<string?> SaveFileAsync(byte[] content, string suggestedFileName)
|
||||
public async Task<FileOperationResult> SaveFileAsync(byte[] content, string suggestedFileName)
|
||||
{
|
||||
var filePath = await ShowSaveDialogAsync(suggestedFileName);
|
||||
if (filePath == null) return null;
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, content);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public Task OpenFileAsync(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||
string? staged = null;
|
||||
try
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
var path = await dialogs.SaveAsync(suggestedFileName);
|
||||
if (string.IsNullOrWhiteSpace(path)) return FileOperationResult.Cancelled();
|
||||
staged = path + $".{Guid.NewGuid():N}.tmp";
|
||||
await File.WriteAllBytesAsync(staged, content);
|
||||
File.Move(staged, path, overwrite: true);
|
||||
return new(FileOperationStatus.Success, "Datei gespeichert.", path);
|
||||
}
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = filePath,
|
||||
UseShellExecute = true
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
Process.Start("open", filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start("xdg-open", filePath);
|
||||
});
|
||||
catch (Exception ex) { return FileOperationResult.Error(ex); }
|
||||
finally { try { if (staged != null && File.Exists(staged)) File.Delete(staged); } catch { } }
|
||||
}
|
||||
|
||||
private static Task<string?> ShowSaveDialogAsync(string suggestedFileName)
|
||||
public Task<FileOperationResult> OpenFileAsync(string filePath) => OpenAsync(filePath, false);
|
||||
public Task<FileOperationResult> OpenFolderAsync(string filePath) => OpenAsync(filePath, true);
|
||||
|
||||
private static Task<FileOperationResult> OpenAsync(string filePath, bool folder) => Task.Run(() =>
|
||||
{
|
||||
return Task.Run(() =>
|
||||
try
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Fallback: save to Documents folder
|
||||
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
return Path.Combine(documentsPath, suggestedFileName);
|
||||
}
|
||||
var path = folder ? Path.GetDirectoryName(Path.GetFullPath(filePath))! : filePath;
|
||||
if (folder ? !Directory.Exists(path) : !File.Exists(path))
|
||||
throw new FileNotFoundException("Der gespeicherte Pfad ist nicht mehr vorhanden.");
|
||||
var start = new ProcessStartInfo { UseShellExecute = true };
|
||||
if (OperatingSystem.IsWindows()) start.FileName = path;
|
||||
else { start.FileName = OperatingSystem.IsMacOS() ? "open" : "xdg-open"; start.ArgumentList.Add(path); }
|
||||
using var process = Process.Start(start) ?? throw new IOException("Kein Programm zum Öffnen gefunden.");
|
||||
return new FileOperationResult(FileOperationStatus.Success, folder ? "Ordner geöffnet." : "Datei geöffnet.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new FileOperationResult(FileOperationStatus.Error,
|
||||
"Der Pfad konnte nicht geöffnet werden. Die gespeicherte Datei bleibt erhalten; bitte den Speicherort im Datei-Explorer öffnen.", filePath, ex.Message);
|
||||
}
|
||||
});
|
||||
|
||||
var extension = Path.GetExtension(suggestedFileName);
|
||||
var filterName = extension == ".pdf" ? "PDF-Dateien" : "Dateien";
|
||||
var filter = $"{filterName} (*{extension})|*{extension}|Alle Dateien (*.*)|*.*";
|
||||
|
||||
var script = $@"
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$dialog = New-Object System.Windows.Forms.SaveFileDialog
|
||||
$dialog.Filter = '{filter}'
|
||||
$dialog.FileName = '{suggestedFileName.Replace("'", "''")}'
|
||||
$dialog.Title = 'Datei speichern'
|
||||
if ($dialog.ShowDialog() -eq 'OK') {{ $dialog.FileName }} else {{ '' }}
|
||||
";
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell",
|
||||
Arguments = $"-NoProfile -Command \"{script.Replace("\"", "\\\"\"")}\"",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return null;
|
||||
|
||||
var result = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit();
|
||||
|
||||
return string.IsNullOrWhiteSpace(result) ? null : result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user