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.")); + } +}