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 disk-full and common quota-exceeded text from logs and StatusHandler errors. /// 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) || message.Contains("no space left on device", StringComparison.OrdinalIgnoreCase) || message.Contains("disk quota", StringComparison.OrdinalIgnoreCase) || message.Contains("quota exceeded", StringComparison.OrdinalIgnoreCase) || message.Contains("storage quota", 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); }