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);
}