< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Library.DotIgnoreIgnoreRule
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
Line coverage
89%
Covered lines: 120
Uncovered lines: 14
Coverable lines: 134
Total lines: 339
Line coverage: 89.5%
Branch coverage
92%
Covered branches: 59
Total branches: 64
Branch coverage: 92.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 65.8% (27/41) Branch coverage: 50% (15/30) Total lines: 1405/5/2026 - 12:15:44 AM Line coverage: 89.5% (120/134) Branch coverage: 92.1% (59/64) Total lines: 339 1/23/2026 - 12:11:06 AM Line coverage: 65.8% (27/41) Branch coverage: 50% (15/30) Total lines: 1405/5/2026 - 12:15:44 AM Line coverage: 89.5% (120/134) Branch coverage: 92.1% (59/64) Total lines: 339

Coverage delta

Coverage delta 43 -43

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor()100%11100%
ShouldIgnore(...)100%11100%
ClearDirectoryCache()100%11100%
IsIgnoredInternal(...)90%101093.33%
CheckIgnoreRules(...)100%11100%
CheckIgnoreRules(...)87.5%8892.85%
FindIgnoreFileCached(...)95.45%222290.32%
GetParsedRules(...)100%88100%
ParseIgnoreFile(...)91.66%131278.57%
GetPathToCheck(...)75%44100%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Text.RegularExpressions;
 5using BitFaster.Caching.Lru;
 6using MediaBrowser.Controller.Entities;
 7using MediaBrowser.Controller.IO;
 8using MediaBrowser.Controller.Resolvers;
 9using MediaBrowser.Model.IO;
 10
 11namespace Emby.Server.Implementations.Library;
 12
 13/// <summary>
 14/// Resolver rule class for ignoring files via .ignore.
 15/// </summary>
 16public class DotIgnoreIgnoreRule : IResolverIgnoreRule
 17{
 118    private static readonly bool IsWindows = OperatingSystem.IsWindows();
 19
 20    private readonly FastConcurrentLru<string, IgnoreFileCacheEntry> _directoryCache;
 21    private readonly FastConcurrentLru<string, ParsedIgnoreCacheEntry> _rulesCache;
 22
 23    /// <summary>
 24    /// Initializes a new instance of the <see cref="DotIgnoreIgnoreRule"/> class.
 25    /// </summary>
 26    public DotIgnoreIgnoreRule()
 27    {
 5928        var cacheSize = Math.Max(100, Environment.ProcessorCount * 100);
 5929        _directoryCache = new FastConcurrentLru<string, IgnoreFileCacheEntry>(
 5930            Environment.ProcessorCount,
 5931            cacheSize,
 5932            StringComparer.Ordinal);
 5933        _rulesCache = new FastConcurrentLru<string, ParsedIgnoreCacheEntry>(
 5934            Environment.ProcessorCount,
 5935            Math.Max(32, cacheSize / 4),
 5936            StringComparer.Ordinal);
 5937    }
 38
 39    /// <inheritdoc />
 33540    public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnoredInternal(fileInfo, parent);
 41
 42    /// <summary>
 43    /// Clears the directory lookup cache. The parsed rules cache is not cleared
 44    /// as it validates file modification time on each access.
 45    /// </summary>
 46    public void ClearDirectoryCache()
 47    {
 8148        _directoryCache.Clear();
 8149    }
 50
 51    /// <summary>
 52    /// Checks whether or not the file is ignored.
 53    /// </summary>
 54    /// <param name="fileInfo">The file information.</param>
 55    /// <param name="parent">The parent BaseItem.</param>
 56    /// <returns>True if the file should be ignored.</returns>
 57    public bool IsIgnoredInternal(FileSystemMetadata fileInfo, BaseItem? parent)
 58    {
 33559        var searchDirectory = fileInfo.IsDirectory
 33560            ? fileInfo.FullName
 33561            : Path.GetDirectoryName(fileInfo.FullName);
 62
 33563        if (string.IsNullOrEmpty(searchDirectory))
 64        {
 065            return false;
 66        }
 67
 33568        var ignoreFile = FindIgnoreFileCached(searchDirectory);
 33569        if (ignoreFile is null)
 70        {
 11871            return false;
 72        }
 73
 21774        var parsedEntry = GetParsedRules(ignoreFile);
 21775        if (parsedEntry is null)
 76        {
 77            // File was deleted after we cached the path - clear the directory cache entry and return false
 178            _directoryCache.TryRemove(searchDirectory, out _);
 179            return false;
 80        }
 81
 82        // Empty file means ignore everything
 21683        if (parsedEntry.IsEmpty)
 84        {
 285            return true;
 86        }
 87
 21488        return parsedEntry.Rules.IsIgnored(GetPathToCheck(fileInfo.FullName, fileInfo.IsDirectory));
 89    }
 90
 91    /// <summary>
 92    /// Checks whether a path should be ignored based on an array of ignore rules.
 93    /// </summary>
 94    /// <param name="path">The path to check.</param>
 95    /// <param name="rules">The array of ignore rules.</param>
 96    /// <param name="isDirectory">Whether the path is a directory.</param>
 97    /// <returns>True if the path should be ignored.</returns>
 98    internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory)
 1599        => CheckIgnoreRules(path, rules, isDirectory, IsWindows);
 100
 101    /// <summary>
 102    /// Checks whether a path should be ignored based on an array of ignore rules.
 103    /// </summary>
 104    /// <param name="path">The path to check.</param>
 105    /// <param name="rules">The array of ignore rules.</param>
 106    /// <param name="isDirectory">Whether the path is a directory.</param>
 107    /// <param name="normalizePath">Whether to normalize backslashes to forward slashes (for Windows paths).</param>
 108    /// <returns>True if the path should be ignored.</returns>
 109    internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath)
 110    {
 25111        var ignore = new Ignore.Ignore();
 112
 113        // Add each rule individually to catch and skip invalid patterns
 25114        var validRulesAdded = 0;
 176115        foreach (var rule in rules)
 116        {
 117            try
 118            {
 63119                ignore.Add(rule);
 45120                validRulesAdded++;
 45121            }
 18122            catch (RegexParseException)
 123            {
 124                // Ignore invalid patterns
 18125            }
 126        }
 127
 128        // If no valid rules were added, fall back to ignoring everything (like an empty .ignore file)
 25129        if (validRulesAdded == 0)
 130        {
 2131            return true;
 132        }
 133
 134        // Mitigate the problem of the Ignore library not handling Windows paths correctly.
 135        // See https://github.com/jellyfin/jellyfin/issues/15484
 23136        var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
 137
 138        // Add trailing slash for directories to match "folder/"
 23139        if (isDirectory)
 140        {
 0141            pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
 142        }
 143
 23144        return ignore.IsIgnored(pathToCheck);
 145    }
 146
 147    private FileInfo? FindIgnoreFileCached(string directory)
 148    {
 149        // Check if we have a cached result for this directory
 335150        if (_directoryCache.TryGet(directory, out var cached))
 151        {
 284152            return cached.IgnoreFileDirectory is null
 284153                ? null
 284154                : new FileInfo(Path.Join(cached.IgnoreFileDirectory, ".ignore"));
 155        }
 156
 157        DirectoryInfo startDir;
 158        try
 159        {
 51160            startDir = new DirectoryInfo(directory);
 51161        }
 0162        catch (ArgumentException)
 163        {
 0164            return null;
 165        }
 166
 167        // Walk up the directory tree to find .ignore file using DirectoryInfo.Parent
 51168        var checkedDirs = new List<string> { directory };
 169
 522170        for (var current = startDir; current is not null; current = current.Parent)
 171        {
 232172            var currentPath = current.FullName;
 173
 174            // Check if this intermediate directory is cached
 232175            if (current != startDir && _directoryCache.TryGet(currentPath, out var parentCached))
 176            {
 177                // Cache the result for all directories we checked
 11178                var entry = new IgnoreFileCacheEntry(parentCached.IgnoreFileDirectory);
 44179                foreach (var dir in checkedDirs)
 180                {
 11181                    _directoryCache.AddOrUpdate(dir, entry);
 182                }
 183
 11184                return parentCached.IgnoreFileDirectory is null
 11185                    ? null
 11186                    : new FileInfo(Path.Join(parentCached.IgnoreFileDirectory, ".ignore"));
 187            }
 188
 221189            var ignoreFile = new FileInfo(Path.Join(currentPath, ".ignore"));
 221190            if (ignoreFile.Exists)
 191            {
 192                // Cache for all directories we checked
 11193                var entry = new IgnoreFileCacheEntry(currentPath);
 46194                foreach (var dir in checkedDirs)
 195                {
 12196                    _directoryCache.AddOrUpdate(dir, entry);
 197                }
 198
 11199                return ignoreFile;
 200            }
 201
 210202            if (current != startDir)
 203            {
 167204                checkedDirs.Add(currentPath);
 205            }
 206        }
 207
 208        // No .ignore file found - cache null result for all directories
 29209        var nullEntry = new IgnoreFileCacheEntry((string?)null);
 448210        foreach (var dir in checkedDirs)
 211        {
 195212            _directoryCache.AddOrUpdate(dir, nullEntry);
 213        }
 214
 29215        return null;
 0216    }
 217
 218    private ParsedIgnoreCacheEntry? GetParsedRules(FileInfo ignoreFile)
 219    {
 217220        if (!ignoreFile.Exists)
 221        {
 1222            _rulesCache.TryRemove(ignoreFile.FullName, out _);
 1223            return null;
 224        }
 225
 216226        var lastModified = ignoreFile.LastWriteTimeUtc;
 216227        var fileLength = ignoreFile.Length;
 216228        var key = ignoreFile.FullName;
 229
 230        // Check cache
 216231        if (_rulesCache.TryGet(key, out var cached))
 232        {
 205233            if (cached.FileLastModified == lastModified && cached.FileLength == fileLength)
 234            {
 204235                return cached;
 236            }
 237
 238            // Stale - need to reparse
 1239            _rulesCache.TryRemove(key, out _);
 240        }
 241
 242        // Parse the file
 12243        var parsedEntry = ParseIgnoreFile(ignoreFile, lastModified, fileLength);
 12244        _rulesCache.AddOrUpdate(key, parsedEntry);
 12245        return parsedEntry;
 246    }
 247
 248    private static ParsedIgnoreCacheEntry ParseIgnoreFile(FileInfo ignoreFile, DateTime lastModified, long fileLength)
 249    {
 12250        if (ignoreFile.LinkTarget is null && fileLength == 0)
 251        {
 1252            return new ParsedIgnoreCacheEntry
 1253            {
 1254                Rules = new Ignore.Ignore(),
 1255                FileLastModified = lastModified,
 1256                FileLength = fileLength,
 1257                IsEmpty = true
 1258            };
 259        }
 260
 261        // Resolve symlinks
 11262        var resolvedFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
 11263        if (!resolvedFile.Exists)
 264        {
 0265            return new ParsedIgnoreCacheEntry
 0266            {
 0267                Rules = new Ignore.Ignore(),
 0268                FileLastModified = lastModified,
 0269                FileLength = fileLength,
 0270                IsEmpty = true
 0271            };
 272        }
 273
 11274        var content = File.ReadAllText(resolvedFile.FullName);
 11275        if (string.IsNullOrWhiteSpace(content))
 276        {
 1277            return new ParsedIgnoreCacheEntry
 1278            {
 1279                Rules = new Ignore.Ignore(),
 1280                FileLastModified = lastModified,
 1281                FileLength = fileLength,
 1282                IsEmpty = true
 1283            };
 284        }
 285
 10286        var rules = content.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 10287        var ignore = new Ignore.Ignore();
 10288        var validRulesAdded = 0;
 289
 40290        foreach (var rule in rules)
 291        {
 292            try
 293            {
 10294                ignore.Add(rule);
 10295                validRulesAdded++;
 10296            }
 0297            catch (RegexParseException)
 298            {
 299                // Ignore invalid patterns
 0300            }
 301        }
 302
 303        // No valid rules means treat as empty (ignore all)
 10304        return new ParsedIgnoreCacheEntry
 10305        {
 10306            Rules = ignore,
 10307            FileLastModified = lastModified,
 10308            FileLength = fileLength,
 10309            IsEmpty = validRulesAdded == 0
 10310        };
 311    }
 312
 313    private static string GetPathToCheck(string path, bool isDirectory)
 314    {
 315        // Normalize Windows paths
 214316        var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
 317
 318        // Add trailing slash for directories to match "folder/"
 214319        if (isDirectory)
 320        {
 1321            pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
 322        }
 323
 214324        return pathToCheck;
 325    }
 326
 327    private readonly record struct IgnoreFileCacheEntry(string? IgnoreFileDirectory);
 328
 329    private sealed class ParsedIgnoreCacheEntry
 330    {
 331        public required Ignore.Ignore Rules { get; init; }
 332
 333        public required DateTime FileLastModified { get; init; }
 334
 335        public required long FileLength { get; init; }
 336
 337        public required bool IsEmpty { get; init; }
 338    }
 339}