diff --git a/JournalApp.Tests/ImportExportTests.cs b/JournalApp.Tests/ImportExportTests.cs index 5a94617..30806fd 100644 --- a/JournalApp.Tests/ImportExportTests.cs +++ b/JournalApp.Tests/ImportExportTests.cs @@ -119,12 +119,12 @@ public async Task ReadArchive_ThrowsOnMissingInternalFile() try { - using (var stream = File.Create(zipPath)) - using (var archive = new System.IO.Compression.ZipArchive(stream, System.IO.Compression.ZipArchiveMode.Create)) + await using (var stream = File.Create(zipPath)) + await using (var archive = await System.IO.Compression.ZipArchive.CreateAsync(stream, System.IO.Compression.ZipArchiveMode.Create, leaveOpen: false, entryNameEncoding: null)) { // Create an entry with wrong name var entry = archive.CreateEntry("wrong-name.json"); - using var entryStream = entry.Open(); + await using var entryStream = await entry.OpenAsync(); await entryStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes("{}")); } @@ -148,11 +148,11 @@ public async Task ReadArchive_ThrowsOnCorruptedJSON() try { - using (var stream = File.Create(zipPath)) - using (var archive = new System.IO.Compression.ZipArchive(stream, System.IO.Compression.ZipArchiveMode.Create)) + await using (var stream = File.Create(zipPath)) + await using (var archive = await System.IO.Compression.ZipArchive.CreateAsync(stream, System.IO.Compression.ZipArchiveMode.Create, leaveOpen: false, entryNameEncoding: null)) { var entry = archive.CreateEntry("journalapp-data.json"); - using var entryStream = entry.Open(); + await using var entryStream = await entry.OpenAsync(); await entryStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes("{invalid json content")); } diff --git a/JournalApp/Data/AppDataService.cs b/JournalApp/Data/AppDataService.cs index 3e28d02..d8c5b0c 100644 --- a/JournalApp/Data/AppDataService.cs +++ b/JournalApp/Data/AppDataService.cs @@ -5,24 +5,24 @@ /// public sealed class AppDataService(ILogger logger, IDbContextFactory dbFactory, PreferenceService preferences) { - public async Task DeleteDbSets() + public async Task DeleteDbSets(CancellationToken cancellationToken = default) { var sw = Stopwatch.StartNew(); - await using var db = await dbFactory.CreateDbContextAsync(); - await DeleteDbSetsInternal(db); - await db.SaveChangesAsync(); + await using var db = await dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await DeleteDbSetsInternal(db, cancellationToken).ConfigureAwait(false); + await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); sw.Stop(); logger.LogInformation("Cleared data sets in {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds); } - public async Task RestoreDbSets(BackupFile backup) + public async Task RestoreDbSets(BackupFile backup, CancellationToken cancellationToken = default) { var sw = Stopwatch.StartNew(); - await using var db = await dbFactory.CreateDbContextAsync(); - await RestoreDbSetsInternal(db, backup); + await using var db = await dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await RestoreDbSetsInternal(db, backup, cancellationToken).ConfigureAwait(false); sw.Stop(); logger.LogInformation( @@ -37,23 +37,23 @@ public async Task RestoreDbSets(BackupFile backup) /// Atomically deletes all existing data and restores from backup in a single transaction. /// This prevents database corruption if the operation is interrupted. /// - public async Task ReplaceDbSets(BackupFile backup) + public async Task ReplaceDbSets(BackupFile backup, CancellationToken cancellationToken = default) { var sw = Stopwatch.StartNew(); - await using var db = await dbFactory.CreateDbContextAsync(); - await using var transaction = await db.Database.BeginTransactionAsync(); + await using var db = await dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); try { // Delete all existing data - await DeleteDbSetsInternal(db); + await DeleteDbSetsInternal(db, cancellationToken).ConfigureAwait(false); // Restore from backup - await RestoreDbSetsInternal(db, backup); + await RestoreDbSetsInternal(db, backup, cancellationToken).ConfigureAwait(false); // Commit the transaction - both delete and restore succeed or fail together - await transaction.CommitAsync(); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); sw.Stop(); logger.LogInformation( @@ -66,16 +66,16 @@ public async Task ReplaceDbSets(BackupFile backup) catch { // Rollback on any error - database remains in original state - await transaction.RollbackAsync(); + await transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); throw; } } - private async Task DeleteDbSetsInternal(AppDbContext db) + private async Task DeleteDbSetsInternal(AppDbContext db, CancellationToken cancellationToken = default) { - var pointsDeleted = await db.Points.ExecuteDeleteAsync(); - var daysDeleted = await db.Days.ExecuteDeleteAsync(); - var categoriesDeleted = await db.Categories.ExecuteDeleteAsync(); + var pointsDeleted = await db.Points.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + var daysDeleted = await db.Days.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + var categoriesDeleted = await db.Categories.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); logger.LogDebug( "Cleared data sets (points: {PointsDeleted}, days: {DaysDeleted}, categories: {CategoriesDeleted})", @@ -84,12 +84,12 @@ private async Task DeleteDbSetsInternal(AppDbContext db) categoriesDeleted); } - private async Task RestoreDbSetsInternal(AppDbContext db, BackupFile backup) + private async Task RestoreDbSetsInternal(AppDbContext db, BackupFile backup, CancellationToken cancellationToken = default) { - await db.Days.AddRangeAsync(backup.Days); - await db.Categories.AddRangeAsync(backup.Categories); - await db.Points.AddRangeAsync(backup.Points); - await db.SaveChangesAsync(); + await db.Days.AddRangeAsync(backup.Days, cancellationToken).ConfigureAwait(false); + await db.Categories.AddRangeAsync(backup.Categories, cancellationToken).ConfigureAwait(false); + await db.Points.AddRangeAsync(backup.Points, cancellationToken).ConfigureAwait(false); + await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } public void SetPreferences(BackupFile backup) @@ -110,14 +110,14 @@ public IEnumerable GetPreferenceBackups() } } - public async Task CreateBackup() + public async Task CreateBackup(CancellationToken cancellationToken = default) { var sw = Stopwatch.StartNew(); - await using var db = await dbFactory.CreateDbContextAsync(); + await using var db = await dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - var days = await db.Days.Include(d => d.Points).ToListAsync(); - var categories = await db.Categories.Include(c => c.Points).ToListAsync(); - var points = await db.Points.ToListAsync(); + var days = await db.Days.Include(d => d.Points).ToListAsync(cancellationToken).ConfigureAwait(false); + var categories = await db.Categories.Include(c => c.Points).ToListAsync(cancellationToken).ConfigureAwait(false); + var points = await db.Points.ToListAsync(cancellationToken).ConfigureAwait(false); var preferencesBackup = GetPreferenceBackups().ToList(); sw.Stop(); diff --git a/JournalApp/Data/AppDataUIService.cs b/JournalApp/Data/AppDataUIService.cs index 85ce530..fb7b65a 100644 --- a/JournalApp/Data/AppDataUIService.cs +++ b/JournalApp/Data/AppDataUIService.cs @@ -29,7 +29,7 @@ await dialogService.ShowJaMessageBox("Recommended: Back up your current data fir BackupFile backup; try { - backup = await BackupFile.ReadArchive(path); + backup = await BackupFile.ReadArchive(path).ConfigureAwait(false); readStopwatch.Stop(); logger.LogInformation("Backup file read successfully in {ElapsedMilliseconds}ms - contains {DayCount} days, {CategoryCount} categories, {PointCount} points", readStopwatch.ElapsedMilliseconds, backup.Days.Count, backup.Categories.Count, backup.Points.Count); @@ -65,7 +65,7 @@ await dialogService.ShowJaMessageBox("Recommended: Back up your current data fir { // Import data from backup atomically - delete and restore in a single transaction. // If this fails, the database will be rolled back to its original state. - await appDataService.ReplaceDbSets(backup); + await appDataService.ReplaceDbSets(backup).ConfigureAwait(false); // Only restore preferences after database operations succeed. appDataService.SetPreferences(backup); @@ -101,7 +101,7 @@ public async Task StartExportWizard(IDialogService dialogService) { var sw = Stopwatch.StartNew(); - backupFile = await appDataService.CreateBackup(); + backupFile = await appDataService.CreateBackup().ConfigureAwait(false); logger.LogInformation("Backup created in {ElapsedMilliseconds}ms - {DayCount} days, {CategoryCount} categories, {PointCount} points", sw.ElapsedMilliseconds, backupFile.Days.Count, backupFile.Categories.Count, backupFile.Points.Count); @@ -109,7 +109,7 @@ public async Task StartExportWizard(IDialogService dialogService) using (var memoryStream = new MemoryStream()) { - await backupFile.WriteArchive(memoryStream); + await backupFile.WriteArchive(memoryStream).ConfigureAwait(false); archiveBytes = memoryStream.ToArray(); } diff --git a/JournalApp/Data/BackupFile.cs b/JournalApp/Data/BackupFile.cs index 0fddb16..b3dec42 100644 --- a/JournalApp/Data/BackupFile.cs +++ b/JournalApp/Data/BackupFile.cs @@ -29,17 +29,17 @@ public class BackupFile /// /// Reads a backup file from the specified stream. /// - public static async Task ReadArchive(Stream stream) + public static async Task ReadArchive(Stream stream, CancellationToken cancellationToken = default) { - using var archive = new ZipArchive(stream, ZipArchiveMode.Read); + await using var archive = await ZipArchive.CreateAsync(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null, cancellationToken).ConfigureAwait(false); foreach (var entry in archive.Entries) { if (entry.FullName == InternalBackupFileName) { - await using var entryStream = entry.Open(); + await using var entryStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync(entryStream, SerializerOptions); + return await JsonSerializer.DeserializeAsync(entryStream, SerializerOptions, cancellationToken).ConfigureAwait(false); } } @@ -49,32 +49,32 @@ public static async Task ReadArchive(Stream stream) /// /// Reads a backup file from the specified path. /// - public static async Task ReadArchive(string path) + public static async Task ReadArchive(string path, CancellationToken cancellationToken = default) { await using var fs = File.Open(path, FileMode.Open); - return await ReadArchive(fs); + return await ReadArchive(fs, cancellationToken).ConfigureAwait(false); } /// /// Writes the backup file to the specified stream. /// - public async Task WriteArchive(Stream stream) + public async Task WriteArchive(Stream stream, CancellationToken cancellationToken = default) { - using var archive = new ZipArchive(stream, ZipArchiveMode.Create); + await using var archive = await ZipArchive.CreateAsync(stream, ZipArchiveMode.Create, leaveOpen: false, entryNameEncoding: null, cancellationToken).ConfigureAwait(false); var entry = archive.CreateEntry(InternalBackupFileName); - await using var entryStream = entry.Open(); + await using var entryStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); - await JsonSerializer.SerializeAsync(entryStream, this, SerializerOptions); + await JsonSerializer.SerializeAsync(entryStream, this, SerializerOptions, cancellationToken).ConfigureAwait(false); } /// /// Writes the backup file to the specified path. /// - public async Task WriteArchive(string path) + public async Task WriteArchive(string path, CancellationToken cancellationToken = default) { await using var stream = File.Create(path); - await WriteArchive(stream); + await WriteArchive(stream, cancellationToken).ConfigureAwait(false); } } diff --git a/JournalApp/Pages/SettingsPage.razor b/JournalApp/Pages/SettingsPage.razor index 1f86355..b0c07c9 100644 --- a/JournalApp/Pages/SettingsPage.razor +++ b/JournalApp/Pages/SettingsPage.razor @@ -11,6 +11,8 @@ @* TODO: Replace with separate page so we can change state of inline fields and have it updated properly in settings *@
+ @_busyMessage + ⚠️ Do not close the app
} else @@ -106,6 +108,7 @@ else @code { bool _busy; + string _busyMessage; bool _creditsExpanded; public string CreditsText { get; private set; } @@ -164,6 +167,7 @@ else try { _busy = true; + _busyMessage = "Importing data..."; StateHasChanged(); var path = App.ActivatedFilePath; @@ -174,6 +178,7 @@ else { App.ActivatedFilePath = null; _busy = false; + _busyMessage = null; } } @@ -182,6 +187,7 @@ else try { _busy = true; + _busyMessage = "Exporting data..."; StateHasChanged(); await AppDataUIService.StartExportWizard(DialogService); @@ -189,6 +195,7 @@ else finally { _busy = false; + _busyMessage = null; } }