From 77f518f8d3e850f91c74ce8f9442df839ec2e9e5 Mon Sep 17 00:00:00 2001 From: rmcrackan Date: Mon, 18 May 2026 09:20:51 -0400 Subject: [PATCH] #1823 - Address full disk better. Fail clearly and early when possible, fail safely when space cannot be queried, and do not take down the UI or spam the user when the disk is full. --- .../ViewModels/MainVM.Liberate.cs | 11 +- .../ViewModels/MainVM.ProcessQueue.cs | 6 +- .../ViewModels/MainVM.VisibleBooks.cs | 4 +- Source/LibationFileManager/DiskSpaceHelper.cs | 145 ++++++++++++++++++ Source/LibationFileManager/FilePathCache.cs | 4 +- Source/LibationUiBase/DiskFullUserMessage.cs | 67 ++++++++ .../DiskSpaceBackupPreflight.cs | 48 ++++++ .../ProcessQueue/ProcessBookViewModel.cs | 23 ++- .../ProcessQueue/ProcessQueueViewModel.cs | 30 +++- Source/LibationWinForms/Form1.Liberate.cs | 9 +- Source/LibationWinForms/Form1.ProcessQueue.cs | 8 +- Source/LibationWinForms/Form1.VisibleBooks.cs | 4 +- .../DiskSpaceHelperTests.cs | 32 ++++ 13 files changed, 362 insertions(+), 29 deletions(-) create mode 100644 Source/LibationFileManager/DiskSpaceHelper.cs create mode 100644 Source/LibationUiBase/DiskFullUserMessage.cs create mode 100644 Source/LibationUiBase/DiskSpaceBackupPreflight.cs create mode 100644 Source/_Tests/LibationFileManager.Tests/DiskSpaceHelperTests.cs diff --git a/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs index 359de0f8..615b0b43 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.Liberate.cs @@ -18,20 +18,17 @@ partial class MainVM public async Task BackupAllBooks() { var books = await Task.Run(DbContexts.GetUnliberated_Flat_NoTracking); - BackupAllBooks(books); + await BackupAllBooksAsync(books); } - private void BackupAllBooks(IEnumerable books) + private async Task BackupAllBooksAsync(IEnumerable books) { try { var unliberated = books.UnLiberated().ToArray(); - Dispatcher.UIThread.Invoke(() => - { - if (ProcessQueue.QueueDownloadDecrypt(unliberated)) - setQueueCollapseState(false); - }); + if (await ProcessQueue.QueueDownloadDecryptAsync(unliberated)) + setQueueCollapseState(false); } catch (Exception ex) { diff --git a/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs index 625ec94f..767cfd5d 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.ProcessQueue.cs @@ -34,7 +34,7 @@ partial class MainVM { try { - if (ProcessQueue.QueueDownloadDecrypt(libraryBooks, config)) + if (await ProcessQueue.QueueDownloadDecryptAsync(libraryBooks, config)) setQueueCollapseState(false); else if (libraryBooks.Count == 1 && libraryBooks[0].Book.AudioExists) { @@ -53,13 +53,13 @@ partial class MainVM } } - public void LiberateSeriesClicked(SeriesEntry series) + public async void LiberateSeriesClicked(SeriesEntry series) { try { Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook); - if (ProcessQueue.QueueDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray())) + if (await ProcessQueue.QueueDownloadDecryptAsync(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray())) setQueueCollapseState(false); } catch (Exception ex) diff --git a/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs index 326a1479..c611e749 100644 --- a/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs +++ b/Source/LibationAvalonia/ViewModels/MainVM.VisibleBooks.cs @@ -68,11 +68,11 @@ partial class MainVM => await setLiberatedVisibleMenuItemAsync(); - public void LiberateVisible() + public async void LiberateVisible() { try { - if (ProcessQueue.QueueDownloadDecrypt(ProductsDisplay.GetVisibleBookEntries().UnLiberated().ToArray())) + if (await ProcessQueue.QueueDownloadDecryptAsync(ProductsDisplay.GetVisibleBookEntries().UnLiberated().ToArray())) setQueueCollapseState(false); } catch (Exception ex) diff --git a/Source/LibationFileManager/DiskSpaceHelper.cs b/Source/LibationFileManager/DiskSpaceHelper.cs new file mode 100644 index 00000000..f75bc0e2 --- /dev/null +++ b/Source/LibationFileManager/DiskSpaceHelper.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace LibationFileManager; + +/// +/// Detects disk-full I/O failures and reports free space for Libation backup paths. +/// Preflight uses when available; runtime detection uses actual write failures +/// (works even when free space cannot be queried, e.g. some UNC/SMB shares). +/// +public static class DiskSpaceHelper +{ + /// Conservative per-title estimate (download + decrypt temp + final file) for bulk preflight. + public const long EstimatedBytesPerAudiobookBackup = 400_000_000L; + + /// Below this free space on a relevant drive, bulk backup is blocked (no Continue). + public const long CriticalFreeBytes = 100_000_000L; + + private const int HResultDiskFull = unchecked((int)0x80070070); + + public static bool IsDiskFullException(Exception? ex) + { + for (var current = ex; current is not null; current = current.InnerException) + { + if (current is IOException && current.HResult == HResultDiskFull) + return true; + + if (ErrorMessageIndicatesDiskFull(current.Message)) + return true; + } + + return false; + } + + /// + /// Matches Windows-style disk-full text from logs and StatusHandler errors. + /// Does not cover quota-specific wording from some NAS/cloud providers; those fall through to normal retry UI. + /// + public static bool ErrorMessageIndicatesDiskFull(string? message) + { + if (string.IsNullOrWhiteSpace(message)) + return false; + + return message.Contains("not enough space on the disk", StringComparison.OrdinalIgnoreCase) + || message.Contains("disk was full", StringComparison.OrdinalIgnoreCase) + || message.Contains("there is not enough space on the disk", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Returns free bytes for the volume containing , or null if unknown. + /// Null means preflight cannot warn/block on that root (writable shares with no capacity API, offline drive, bad path). + /// On Windows, yields drive letters (C:\) or UNC roots (\\server\share\). + /// + public static long? TryGetAvailableFreeBytes(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + return null; + + try + { + var fullPath = Path.GetFullPath(path); + var root = Path.GetPathRoot(fullPath); + if (string.IsNullOrWhiteSpace(root)) + return null; + + var drive = new DriveInfo(root); + // IsReady is false for disconnected network drives; AvailableFreeSpace may be wrong on some NAS reporting. + return drive.IsReady ? drive.AvailableFreeSpace : null; + } + catch + { + // DriveInfo can throw for invalid roots; treat as unknown rather than failing backup setup. + return null; + } + } + + public static IReadOnlyList GetBackupDriveSpaces(Configuration config, int bookCount) + { + var requiredBytes = Math.Max(0, bookCount) * EstimatedBytesPerAudiobookBackup; + var pathsByRoot = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + void addPath(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + return; + + string fullPath; + try + { + fullPath = Path.GetFullPath(path); + } + catch + { + return; + } + + var root = Path.GetPathRoot(fullPath); + if (string.IsNullOrWhiteSpace(root)) + return; + + // Same physical share via Z: vs \\server\share appears as two roots; each gets the full estimate (conservative). + if (!pathsByRoot.TryGetValue(root, out var list)) + { + list = []; + pathsByRoot[root] = list; + } + + if (!list.Contains(fullPath, StringComparer.OrdinalIgnoreCase)) + list.Add(fullPath); + } + + addPath(config.Books?.Path); + addPath(config.InProgress); + + return pathsByRoot + .Select(kvp => new BackupDriveSpace( + kvp.Key, + kvp.Value, + TryGetAvailableFreeBytes(kvp.Key), + requiredBytes)) + .ToList(); + } + + /// + /// True when every root is unknown or has enough reported space. All-unknown => no preflight dialog. + /// + public static bool HasSufficientSpaceForBulkBackup(IReadOnlyList drives) + => drives.All(d => d.AvailableBytes is null || d.AvailableBytes >= d.RequiredBytes); + + /// + /// Only applies when free space was read successfully; unknown (null) never hard-blocks. + /// + public static bool AnyDriveCriticallyLow(IReadOnlyList drives) + => drives.Any(d => d.AvailableBytes is not null && d.AvailableBytes < CriticalFreeBytes); + + public readonly record struct BackupDriveSpace( + /// Path root from (e.g. C:\ or \\nas\library\). + string DriveRoot, + IReadOnlyList Paths, + /// Null when could not query this root. + long? AvailableBytes, + long RequiredBytes); +} diff --git a/Source/LibationFileManager/FilePathCache.cs b/Source/LibationFileManager/FilePathCache.cs index 511a261d..a751c1df 100644 --- a/Source/LibationFileManager/FilePathCache.cs +++ b/Source/LibationFileManager/FilePathCache.cs @@ -172,7 +172,9 @@ public static class FilePathCache catch (IOException ex) { Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME_V2}"); - throw; + // Keep in-memory cache; rethrow other I/O errors (permissions, network drop). Disk full must not take down the UI. + if (!DiskSpaceHelper.IsDiskFullException(ex)) + throw; } } } diff --git a/Source/LibationUiBase/DiskFullUserMessage.cs b/Source/LibationUiBase/DiskFullUserMessage.cs new file mode 100644 index 00000000..3fbe4ebf --- /dev/null +++ b/Source/LibationUiBase/DiskFullUserMessage.cs @@ -0,0 +1,67 @@ +using LibationFileManager; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LibationUiBase; + +/// +/// User-facing copy when backups fail or are blocked due to insufficient disk space. +/// +public static class DiskFullUserMessage +{ + public const string DialogCaption = "Not enough disk space"; + + public static string BuildQueueStoppedBody() + => """ + Libation stopped the backup queue because the disk ran out of free space. + + Download and decrypt use temporary files under your "In progress" folder, then write finished audiobooks to your Books location. Both need enough free space on their drives. + + Free disk space (or move Books / In progress to a larger drive in Settings), delete partial files under your In progress folder if needed, then retry your backups in smaller batches. + """; + + public static string BuildPreflightBlockedBody(IReadOnlyList drives, int bookCount) + { + var sb = new StringBuilder(); + sb.AppendLine($"You are about to back up {bookCount} books, but a drive Libation uses does not have enough free space to continue safely."); + sb.AppendLine(); + AppendDriveLines(sb, drives); + sb.AppendLine(); + sb.Append("Free space or change the Books and In progress locations in Settings, then try again."); + return sb.ToString(); + } + + public static string BuildPreflightWarningBody(IReadOnlyList drives, int bookCount) + { + var sb = new StringBuilder(); + sb.AppendLine($"You are about to back up {bookCount} books. Libation estimates you may need on the order of {FormatBytes(bookCount * DiskSpaceHelper.EstimatedBytesPerAudiobookBackup)} total, plus extra room for temporary files during each download."); + sb.AppendLine(); + AppendDriveLines(sb, drives); + sb.AppendLine(); + sb.Append("Continue anyway?"); + return sb.ToString(); + } + + private static void AppendDriveLines(StringBuilder sb, IReadOnlyList drives) + { + foreach (var drive in drives) + { + // "unknown" when DriveInfo could not report space (typical for some network paths); user may still have chosen Continue. + var free = drive.AvailableBytes is null ? "unknown" : FormatBytes(drive.AvailableBytes.Value); + var needed = FormatBytes(drive.RequiredBytes); + sb.AppendLine($"{drive.DriveRoot} Free: {free} Estimated needed: {needed}"); + foreach (var path in drive.Paths) + sb.AppendLine($" {path}"); + } + } + + private static string FormatBytes(long bytes) + { + const long gb = 1024L * 1024 * 1024; + if (bytes >= gb) + return $"{bytes / (double)gb:F1} GB"; + const long mb = 1024 * 1024; + return $"{bytes / (double)mb:F0} MB"; + } +} diff --git a/Source/LibationUiBase/DiskSpaceBackupPreflight.cs b/Source/LibationUiBase/DiskSpaceBackupPreflight.cs new file mode 100644 index 00000000..bc5df877 --- /dev/null +++ b/Source/LibationUiBase/DiskSpaceBackupPreflight.cs @@ -0,0 +1,48 @@ +using LibationFileManager; +using LibationUiBase.Forms; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LibationUiBase; + +/// +/// Optional bulk-backup gate before titles are queued. Skipped for single-book backups. +/// Network paths that do not report free space pass through with no dialog; failures are handled at download time. +/// +public static class DiskSpaceBackupPreflight +{ + private const int BulkBackupBookThreshold = 2; + + public static async Task ConfirmBulkBackupAsync(int bookCount, Configuration config) + { + if (bookCount < BulkBackupBookThreshold) + return true; + + var drives = DiskSpaceHelper.GetBackupDriveSpaces(config, bookCount); + + // All roots unknown, or all have enough reported space: queue without prompting. + if (DiskSpaceHelper.HasSufficientSpaceForBulkBackup(drives)) + return true; + + // Known space below CriticalFreeBytes: do not offer Continue (avoids starting a huge queue on a full local disk). + if (DiskSpaceHelper.AnyDriveCriticallyLow(drives)) + { + await MessageBoxBase.Show( + DiskFullUserMessage.BuildPreflightBlockedBody(drives, bookCount), + DiskFullUserMessage.DialogCaption, + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return false; + } + + // At least one root reported space below estimate but above critical (or mixed known/unknown with a shortfall). + var result = await MessageBoxBase.Show( + DiskFullUserMessage.BuildPreflightWarningBody(drives, bookCount), + DiskFullUserMessage.DialogCaption, + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2); + + return result == DialogResult.Yes; + } +} diff --git a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs index d602babb..7579fdc1 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessBookViewModel.cs @@ -24,7 +24,9 @@ public enum ProcessBookResult FailedSkip, FailedAbort, LicenseDenied, - LicenseDeniedPossibleOutage + LicenseDeniedPossibleOutage, + /// Volume full on write; queue should stop (see ProcessQueueViewModel queue loop). + DiskFull } public enum ProcessBookStatus @@ -69,6 +71,7 @@ public class ProcessBookViewModel : ReactiveObject (ProcessBookResult.LicenseDenied, true) => "License denied (Plus; often temporary)", (ProcessBookResult.LicenseDenied, false) => "License Denied", (ProcessBookResult.LicenseDeniedPossibleOutage, _) => "Possible Service Interruption", + (ProcessBookResult.DiskFull, _) => "Disk full, queue stopped", _ => Status.ToString(), }; @@ -152,6 +155,13 @@ public class ProcessBookViewModel : ReactiveObject { foreach (var errorMessage in statusHandler.Errors) LogError($"{procName}: {errorMessage}"); + + // Prefer disk-full detection over generic retry; avoids treating truncated .aaxc as a normal failure. + if (statusHandler.Errors.Any(DiskSpaceHelper.ErrorMessageIndicatesDiskFull)) + { + LogInfo($"{procName}: Disk is full. Free space or change Books / In progress in Settings. - {LibraryBook.Book}"); + result = ProcessBookResult.DiskFull; + } } } catch (ContentLicenseDeniedException ldex) @@ -173,6 +183,13 @@ public class ProcessBookViewModel : ReactiveObject result = ProcessBookResult.LicenseDenied; } } + // HRESULT 0x80070070 / known messages from the OS when a volume is full (including many SMB shares). + catch (Exception ex) when (DiskSpaceHelper.IsDiskFullException(ex)) + { + Serilog.Log.Logger.Error(ex, "Disk full during {ProcName} for {{@Book}}", procName, LibraryBook.LogFriendly()); + LogInfo($"{procName}: Disk is full. Free space or change Books / In progress in Settings. - {LibraryBook.Book}"); + result = ProcessBookResult.DiskFull; + } catch (Exception ex) { Serilog.Log.Logger.Error(ex, $"Unhandled exception in {procName} for {{@Book}}", LibraryBook.LogFriendly()); @@ -180,6 +197,7 @@ public class ProcessBookViewModel : ReactiveObject } finally { + // DiskFull skips the per-book Abort/Retry/Ignore dialog; the queue shows one disk-full message instead. if (result == ProcessBookResult.None) result = await GetFailureActionAsync(LibraryBook); @@ -345,6 +363,9 @@ public class ProcessBookViewModel : ReactiveObject { foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed")) LogError(errorMessage); + + if (result.Errors.Any(DiskSpaceHelper.ErrorMessageIndicatesDiskFull)) + LogInfo("Disk is full. Free space or change Books / In progress in Settings."); } } diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index a317db4b..b82e9130 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -73,8 +73,13 @@ public class ProcessQueueViewModel : ReactiveObject private void Queue_CompletedCountChanged(object? sender, int e) { - int errCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.FailedAbort or ProcessBookResult.FailedSkip or ProcessBookResult.FailedRetry or ProcessBookResult.ValidationFail); - int completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success); + var errCount = Queue.Completed.Count(p => p.Result + is ProcessBookResult.FailedAbort + or ProcessBookResult.FailedSkip + or ProcessBookResult.FailedRetry + or ProcessBookResult.ValidationFail + or ProcessBookResult.DiskFull); + var completeCount = Queue.Completed.Count(p => p.Result is ProcessBookResult.Success); ErrorCount = errCount; CompletedCount = completeCount; @@ -127,7 +132,7 @@ public class ProcessQueueViewModel : ReactiveObject return false; } - public bool QueueDownloadDecrypt(IList libraryBooks, Configuration? config = null) + public async Task QueueDownloadDecryptAsync(IList libraryBooks, Configuration? config = null) { config ??= Configuration.Instance; if (!IsBooksDirectoryValid(config)) @@ -182,6 +187,10 @@ public class ProcessQueueViewModel : ReactiveObject if (toLiberate.Length > 0) { + // May no-op when free space is unknown (common on UNC); see DiskSpaceBackupPreflight. + if (!await DiskSpaceBackupPreflight.ConfirmBulkBackupAsync(toLiberate.Length, config)) + return false; + Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length); AddDownloadDecrypt(toLiberate, config); return true; @@ -299,6 +308,7 @@ public class ProcessQueueViewModel : ReactiveObject ProgressBarVisible = true; var startingTime = DateTime.Now; bool shownLicenseGuidanceMessage = false; + bool shownDiskFullMessage = false; using var counterTimer = new System.Threading.Timer(_ => RunningTime = timeToStr(DateTime.Now - startingTime), null, 0, 500); @@ -321,6 +331,20 @@ public class ProcessQueueViewModel : ReactiveObject Queue.ClearCurrent(); else if (result == ProcessBookResult.FailedAbort) Queue.ClearQueue(); + // Stop the whole queue on first real disk-full write (local or network); do not retry hundreds of titles. + else if (result == ProcessBookResult.DiskFull) + { + if (!shownDiskFullMessage) + { + await MessageBoxBase.Show( + DiskFullUserMessage.BuildQueueStoppedBody(), + DiskFullUserMessage.DialogCaption, + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + shownDiskFullMessage = true; + } + Queue.ClearQueue(); + } else if (result == ProcessBookResult.FailedSkip) await nextBook.LibraryBook.UpdateBookStatusAsync(LiberatedStatus.Error); else if (!shownLicenseGuidanceMessage diff --git a/Source/LibationWinForms/Form1.Liberate.cs b/Source/LibationWinForms/Form1.Liberate.cs index 6eea9ad5..4300bee4 100644 --- a/Source/LibationWinForms/Form1.Liberate.cs +++ b/Source/LibationWinForms/Form1.Liberate.cs @@ -19,16 +19,13 @@ public partial class Form1 BackupAllBooks(library); } - private void BackupAllBooks(IEnumerable books) + private async void BackupAllBooks(IEnumerable books) { try { var unliberated = books.UnLiberated().ToArray(); - Invoke(() => - { - if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated)) - SetQueueCollapseState(false); - }); + if (await processBookQueue1.ViewModel.QueueDownloadDecryptAsync(unliberated)) + SetQueueCollapseState(false); } catch (Exception ex) { diff --git a/Source/LibationWinForms/Form1.ProcessQueue.cs b/Source/LibationWinForms/Form1.ProcessQueue.cs index aa0ddf9a..8824e102 100644 --- a/Source/LibationWinForms/Form1.ProcessQueue.cs +++ b/Source/LibationWinForms/Form1.ProcessQueue.cs @@ -23,11 +23,11 @@ public partial class Form1 this.Width = width; } - private void ProductsDisplay_LiberateClicked(object sender, System.Collections.Generic.IList libraryBooks, Configuration config) + private async void ProductsDisplay_LiberateClicked(object sender, System.Collections.Generic.IList libraryBooks, Configuration config) { try { - if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks, config)) + if (await processBookQueue1.ViewModel.QueueDownloadDecryptAsync(libraryBooks, config)) SetQueueCollapseState(false); else if (libraryBooks.Count == 1 && libraryBooks[0].Book.AudioExists) { @@ -46,13 +46,13 @@ public partial class Form1 } } - private void ProductsDisplay_LiberateSeriesClicked(object sender, SeriesEntry series) + private async void ProductsDisplay_LiberateSeriesClicked(object sender, SeriesEntry series) { try { Serilog.Log.Logger.Information("Begin backing up all {series} episodes", series.LibraryBook); - if (processBookQueue1.ViewModel.QueueDownloadDecrypt(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray())) + if (await processBookQueue1.ViewModel.QueueDownloadDecryptAsync(series.Children.Select(c => c.LibraryBook).UnLiberated().ToArray())) SetQueueCollapseState(false); } catch (Exception ex) diff --git a/Source/LibationWinForms/Form1.VisibleBooks.cs b/Source/LibationWinForms/Form1.VisibleBooks.cs index 1a27019d..c81be915 100644 --- a/Source/LibationWinForms/Form1.VisibleBooks.cs +++ b/Source/LibationWinForms/Form1.VisibleBooks.cs @@ -54,11 +54,11 @@ public partial class Form1 }); } - private void liberateVisible(object sender, EventArgs e) + private async void liberateVisible(object sender, EventArgs e) { try { - if (processBookQueue1.ViewModel.QueueDownloadDecrypt(productsDisplay.GetVisible().UnLiberated().ToArray())) + if (await processBookQueue1.ViewModel.QueueDownloadDecryptAsync(productsDisplay.GetVisible().UnLiberated().ToArray())) SetQueueCollapseState(false); } catch (Exception ex) diff --git a/Source/_Tests/LibationFileManager.Tests/DiskSpaceHelperTests.cs b/Source/_Tests/LibationFileManager.Tests/DiskSpaceHelperTests.cs new file mode 100644 index 00000000..4290bc3c --- /dev/null +++ b/Source/_Tests/LibationFileManager.Tests/DiskSpaceHelperTests.cs @@ -0,0 +1,32 @@ +using LibationFileManager; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.IO; + +namespace LibationFileManager.Tests; + +[TestClass] +public class DiskSpaceHelperTests +{ + [TestMethod] + public void IsDiskFullException_detects_message() + { + var ex = new IOException("There is not enough space on the disk. : 'C:\\temp\\x.aaxc'."); + Assert.IsTrue(DiskSpaceHelper.IsDiskFullException(ex)); + } + + [TestMethod] + public void IsDiskFullException_detects_message_in_aggregate() + { + var inner = new IOException("Failed to create file because the disk was full."); + var ex = new AggregateException(inner); + Assert.IsTrue(DiskSpaceHelper.IsDiskFullException(ex)); + } + + [TestMethod] + public void ErrorMessageIndicatesDiskFull_matches_common_phrases() + { + Assert.IsTrue(DiskSpaceHelper.ErrorMessageIndicatesDiskFull("There is not enough space on the disk. : 'C:\\temp\\x.aaxc'.")); + Assert.IsFalse(DiskSpaceHelper.ErrorMessageIndicatesDiskFull("Unable to read beyond the end of the stream.")); + } +}