diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index ce628a04d0..13c7895f83 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -28,22 +28,44 @@ public static class StorageHelper } /// - /// Gets the free space of a specific directory. + /// Gets the free space of the parent filesystem of a specific directory. /// /// Path to a folder. - /// The number of bytes available space. + /// Various details about the parent filesystem containing the directory. public static FolderStorageInfo GetFreeSpaceOf(string path) { try { - var driveInfo = new DriveInfo(path); + // Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar. + var resolvedPath = ResolvePath(path); + // We iterate all filesystems reported by GetDrives() here, and attempt to find the best + // match that contains, as deep as possible, the given path. + // This is required because simply calling `DriveInfo` on a path returns that path as + // the Name and RootDevice, which is not at all how this should work. + var allDrives = DriveInfo.GetDrives(); + DriveInfo? bestMatch = null; + foreach (DriveInfo d in allDrives) + { + if (resolvedPath.StartsWith(d.RootDirectory.FullName, StringComparison.InvariantCultureIgnoreCase) && + (bestMatch is null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length)) + { + bestMatch = d; + } + } + + if (bestMatch is null) + { + throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid."); + } + return new FolderStorageInfo() { Path = path, - FreeSpace = driveInfo.AvailableFreeSpace, - UsedSpace = driveInfo.TotalSize - driveInfo.AvailableFreeSpace, - StorageType = driveInfo.DriveType.ToString(), - DeviceId = driveInfo.Name, + ResolvedPath = resolvedPath, + FreeSpace = bestMatch.AvailableFreeSpace, + UsedSpace = bestMatch.TotalSize - bestMatch.AvailableFreeSpace, + StorageType = bestMatch.DriveType.ToString(), + DeviceId = bestMatch.Name, }; } catch @@ -51,6 +73,7 @@ public static class StorageHelper return new FolderStorageInfo() { Path = path, + ResolvedPath = path, FreeSpace = -1, UsedSpace = -1, StorageType = null, @@ -59,6 +82,26 @@ public static class StorageHelper } } + /// + /// Walk a path and fully resolve any symlinks within it. + /// + private static string ResolvePath(string path) + { + var parts = path.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); + var current = Path.DirectorySeparatorChar.ToString(); + foreach (var part in parts) + { + current = Path.Combine(current, part); + var resolved = new DirectoryInfo(current).ResolveLinkTarget(returnFinalTarget: true); + if (resolved is not null) + { + current = resolved.FullName; + } + } + + return current; + } + /// /// Gets the underlying drive data from a given path and checks if the available storage capacity matches the threshold. /// diff --git a/MediaBrowser.Model/System/FolderStorageInfo.cs b/MediaBrowser.Model/System/FolderStorageInfo.cs index 7b10e4ea58..ebca39228b 100644 --- a/MediaBrowser.Model/System/FolderStorageInfo.cs +++ b/MediaBrowser.Model/System/FolderStorageInfo.cs @@ -11,17 +11,22 @@ public record FolderStorageInfo public required string Path { get; init; } /// - /// Gets the free space of the underlying storage device of the . + /// Gets the fully resolved path of the folder in question (interpolating any symlinks if present). + /// + public required string ResolvedPath { get; init; } + + /// + /// Gets the free space of the underlying storage device of the . /// public long FreeSpace { get; init; } /// - /// Gets the used space of the underlying storage device of the . + /// Gets the used space of the underlying storage device of the . /// public long UsedSpace { get; init; } /// - /// Gets the kind of storage device of the . + /// Gets the kind of storage device of the . /// public string? StorageType { get; init; }