< Summary - Jellyfin

Information
Class: MediaBrowser.Controller.IO.FileSystemHelper
Assembly: MediaBrowser.Controller
File(s): /srv/git/jellyfin/MediaBrowser.Controller/IO/FileSystemHelper.cs
Line coverage
37%
Covered lines: 20
Uncovered lines: 34
Coverable lines: 54
Total lines: 205
Line coverage: 37%
Branch coverage
57%
Covered branches: 16
Total branches: 28
Branch coverage: 57.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/12/2026 - 12:15:57 AM Line coverage: 24.4% (11/45) Branch coverage: 38.8% (7/18) Total lines: 16910/29/2026 - 12:17:11 AM Line coverage: 37% (20/54) Branch coverage: 57.1% (16/28) Total lines: 205 5/12/2026 - 12:15:57 AM Line coverage: 24.4% (11/45) Branch coverage: 38.8% (7/18) Total lines: 16910/29/2026 - 12:17:11 AM Line coverage: 37% (20/54) Branch coverage: 57.1% (16/28) Total lines: 205

Coverage delta

Coverage delta 19 -19

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
DeleteFile(...)100%210%
DeleteEmptyFolders(...)50%13416.66%
Resolve(...)100%1150%
ResolveLinkTarget(...)35.71%881427.77%
ResolveLinkTarget(...)100%11100%
GetChildPath(...)90%1010100%

File(s)

/srv/git/jellyfin/MediaBrowser.Controller/IO/FileSystemHelper.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using MediaBrowser.Model.IO;
 6using Microsoft.Extensions.Logging;
 7
 8namespace MediaBrowser.Controller.IO;
 9
 10/// <summary>
 11/// Helper methods for file system management.
 12/// </summary>
 13public static class FileSystemHelper
 14{
 15    /// <summary>
 16    /// Deletes the file.
 17    /// </summary>
 18    /// <param name="fileSystem">The fileSystem.</param>
 19    /// <param name="path">The path.</param>
 20    /// <param name="logger">The logger.</param>
 21    public static void DeleteFile(IFileSystem fileSystem, string path, ILogger logger)
 22    {
 23        try
 24        {
 025            fileSystem.DeleteFile(path);
 026        }
 027        catch (UnauthorizedAccessException ex)
 28        {
 029            logger.LogError(ex, "Error deleting file {Path}", path);
 030        }
 031        catch (IOException ex)
 32        {
 033            logger.LogError(ex, "Error deleting file {Path}", path);
 034        }
 035    }
 36
 37    /// <summary>
 38    /// Recursively delete empty folders.
 39    /// </summary>
 40    /// <param name="fileSystem">The fileSystem.</param>
 41    /// <param name="path">The path.</param>
 42    /// <param name="logger">The logger.</param>
 43    public static void DeleteEmptyFolders(IFileSystem fileSystem, string path, ILogger logger)
 44    {
 645        foreach (var directory in fileSystem.GetDirectoryPaths(path))
 46        {
 047            DeleteEmptyFolders(fileSystem, directory, logger);
 048            if (!fileSystem.GetFileSystemEntryPaths(directory).Any())
 49            {
 50                try
 51                {
 052                    Directory.Delete(directory, false);
 053                }
 054                catch (UnauthorizedAccessException ex)
 55                {
 056                    logger.LogError(ex, "Error deleting directory {Path}", directory);
 057                }
 058                catch (IOException ex)
 59                {
 060                    logger.LogError(ex, "Error deleting directory {Path}", directory);
 061                }
 62            }
 63        }
 364    }
 65
 66    /// <summary>
 67    /// Resolves a single link hop for the specified path.
 68    /// </summary>
 69    /// <remarks>
 70    /// Returns <c>null</c> if the path is not a symbolic link or the filesystem does not support link resolution (e.g.,
 71    /// </remarks>
 72    /// <param name="path">The file path to resolve.</param>
 73    /// <returns>
 74    /// A <see cref="FileInfo"/> representing the next link target if the path is a link; otherwise, <c>null</c>.
 75    /// </returns>
 76    private static FileInfo? Resolve(string path)
 77    {
 78        try
 79        {
 1180            return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo;
 81        }
 082        catch (IOException)
 83        {
 84            // Filesystem doesn't support links (e.g., exFAT).
 085            return null;
 86        }
 1187    }
 88
 89    /// <summary>
 90    /// Gets the target of the specified file link.
 91    /// </summary>
 92    /// <remarks>
 93    /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
 94    /// </remarks>
 95    /// <param name="linkPath">The path of the file link.</param>
 96    /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next lin
 97    /// <returns>
 98    /// A <see cref="FileInfo"/> if the <paramref name="linkPath"/> is a link, regardless of if the target exists; other
 99    /// </returns>
 100    public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
 101    {
 102        // Check if the file exists so the native resolve handler won't throw at us.
 11103        if (!File.Exists(linkPath))
 104        {
 0105            return null;
 106        }
 107
 11108        if (!returnFinalTarget)
 109        {
 0110            return Resolve(linkPath);
 111        }
 112
 11113        var targetInfo = Resolve(linkPath);
 11114        if (targetInfo is null || !targetInfo.Exists)
 115        {
 11116            return targetInfo;
 117        }
 118
 0119        var currentPath = targetInfo.FullName;
 0120        var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath };
 121
 122        while (true)
 123        {
 0124            var linkInfo = Resolve(currentPath);
 0125            if (linkInfo is null)
 126            {
 127                break;
 128            }
 129
 0130            var targetPath = linkInfo.FullName;
 131
 132            // If an infinite loop is detected, return the file info for the
 133            // first link in the loop we encountered.
 0134            if (!visited.Add(targetPath))
 135            {
 0136                return new FileInfo(targetPath);
 137            }
 138
 0139            targetInfo = linkInfo;
 0140            currentPath = targetPath;
 141
 142            // Exit if the target doesn't exist, so the native resolve handler won't throw at us.
 0143            if (!targetInfo.Exists)
 144            {
 145                break;
 146            }
 147        }
 148
 0149        return targetInfo;
 150    }
 151
 152    /// <summary>
 153    /// Gets the target of the specified file link.
 154    /// </summary>
 155    /// <remarks>
 156    /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128.
 157    /// </remarks>
 158    /// <param name="fileInfo">The file info of the file link.</param>
 159    /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next lin
 160    /// <returns>
 161    /// A <see cref="FileInfo"/> if the <paramref name="fileInfo"/> is a link, regardless of if the target exists; other
 162    /// </returns>
 163    public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false)
 164    {
 11165        ArgumentNullException.ThrowIfNull(fileInfo);
 166
 11167        return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget);
 168    }
 169
 170    /// <summary>
 171    /// Combines a caller supplied name with a parent directory, making sure the name cannot escape that directory.
 172    /// </summary>
 173    /// <param name="parentPath">The directory the name has to resolve inside of.</param>
 174    /// <param name="name">The name of the child.</param>
 175    /// <returns>
 176    /// The full path of the child, or <c>null</c> if <paramref name="name"/> is not the name of a direct child
 177    /// of <paramref name="parentPath"/>.
 178    /// </returns>
 179    public static string? GetChildPath(string parentPath, string name)
 180    {
 49181        if (string.IsNullOrWhiteSpace(name) || name.Contains('\0', StringComparison.Ordinal))
 182        {
 3183            return null;
 184        }
 185
 186        // Rejects directory separators, and on Windows also volume separators, as those make the name more than a singl
 46187        if (!string.Equals(Path.GetFileName(name), name, StringComparison.Ordinal))
 188        {
 13189            return null;
 190        }
 191
 33192        var fullPath = Path.GetFullPath(Path.Combine(parentPath, name));
 33193        var fullParentPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(parentPath));
 194
 195        // Catches the remaining relative names, "." and "..", which are valid single segments.
 33196        if (!string.Equals(Path.GetDirectoryName(fullPath), fullParentPath, StringComparison.Ordinal))
 197        {
 7198            return null;
 199        }
 200
 201        // Windows strips trailing dots and spaces, so a name like "..." resolves to the parent directory itself
 202        // and a name like "Movies." to a different child. Reject anything normalization did not leave intact.
 26203        return string.Equals(Path.GetFileName(fullPath), name, StringComparison.Ordinal) ? fullPath : null;
 204    }
 205}