| | | 1 | | #pragma warning disable CS1591 |
| | | 2 | | |
| | | 3 | | using System; |
| | | 4 | | using System.Collections.Concurrent; |
| | | 5 | | using System.Collections.Generic; |
| | | 6 | | using System.IO; |
| | | 7 | | using System.Linq; |
| | | 8 | | using System.Threading; |
| | | 9 | | using MediaBrowser.Model.IO; |
| | | 10 | | |
| | | 11 | | namespace MediaBrowser.Controller.Providers |
| | | 12 | | { |
| | | 13 | | public class DirectoryService : IDirectoryService |
| | | 14 | | { |
| | | 15 | | // TODO replace with one shared bounded cache. |
| | | 16 | | private const int MaxCachedRecords = 100_000; |
| | | 17 | | private const int AccessIntervalMs = 1_000; |
| | | 18 | | // Timeout cache if no access for 5 minutes. |
| | | 19 | | private const int IdleTimeoutMs = 5 * 60 * 1_000; |
| | | 20 | | |
| | 351 | 21 | | private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new(StringComparer.Ordinal); |
| | | 22 | | |
| | 351 | 23 | | private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new(StringComparer.Ordinal); |
| | | 24 | | |
| | 351 | 25 | | private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new(StringComparer.Ordinal); |
| | | 26 | | |
| | | 27 | | private readonly IFileSystem _fileSystem; |
| | | 28 | | |
| | | 29 | | // ConcurrentDictionary.Count locks the dictionary, so keep an estimated counter. |
| | | 30 | | // Concurrent factory runs can overcount and a clear racing an add can undercount, |
| | | 31 | | // it only has to be roughly right. |
| | | 32 | | private int _recordCount; |
| | 351 | 33 | | private long _lastAccess = Environment.TickCount64; |
| | | 34 | | |
| | | 35 | | public DirectoryService(IFileSystem fileSystem) |
| | | 36 | | { |
| | 351 | 37 | | _fileSystem = fileSystem; |
| | 351 | 38 | | } |
| | | 39 | | |
| | | 40 | | public FileSystemMetadata[] GetFileSystemEntries(string path) |
| | | 41 | | { |
| | 679 | 42 | | DropCacheIfIdleOrFull(); |
| | | 43 | | |
| | 679 | 44 | | return _cache.GetOrAdd( |
| | 679 | 45 | | path, |
| | 679 | 46 | | static (p, state) => |
| | 679 | 47 | | { |
| | 679 | 48 | | FileSystemMetadata[] entries; |
| | 679 | 49 | | try |
| | 679 | 50 | | { |
| | 679 | 51 | | entries = state.FileSystem.GetFileSystemEntries(p).ToArray(); |
| | 679 | 52 | | } |
| | 679 | 53 | | catch (DirectoryNotFoundException) |
| | 679 | 54 | | { |
| | 679 | 55 | | entries = []; |
| | 679 | 56 | | } |
| | 679 | 57 | | |
| | 679 | 58 | | Interlocked.Add(ref state.Service._recordCount, entries.Length + 1); |
| | 679 | 59 | | return entries; |
| | 679 | 60 | | }, |
| | 679 | 61 | | (FileSystem: _fileSystem, Service: this)); |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | public List<FileSystemMetadata> GetDirectories(string path) |
| | | 65 | | { |
| | 2 | 66 | | var list = new List<FileSystemMetadata>(); |
| | 2 | 67 | | var items = GetFileSystemEntries(path); |
| | 16 | 68 | | for (var i = 0; i < items.Length; i++) |
| | | 69 | | { |
| | 6 | 70 | | var item = items[i]; |
| | 6 | 71 | | if (item.IsDirectory) |
| | | 72 | | { |
| | 3 | 73 | | list.Add(item); |
| | | 74 | | } |
| | | 75 | | } |
| | | 76 | | |
| | 2 | 77 | | return list; |
| | | 78 | | } |
| | | 79 | | |
| | | 80 | | public List<FileSystemMetadata> GetFiles(string path) |
| | | 81 | | { |
| | 2 | 82 | | var list = new List<FileSystemMetadata>(); |
| | 2 | 83 | | var items = GetFileSystemEntries(path); |
| | 16 | 84 | | for (var i = 0; i < items.Length; i++) |
| | | 85 | | { |
| | 6 | 86 | | var item = items[i]; |
| | 6 | 87 | | if (!item.IsDirectory) |
| | | 88 | | { |
| | 3 | 89 | | list.Add(item); |
| | | 90 | | } |
| | | 91 | | } |
| | | 92 | | |
| | 2 | 93 | | return list; |
| | | 94 | | } |
| | | 95 | | |
| | | 96 | | public FileSystemMetadata? GetFile(string path) |
| | | 97 | | { |
| | 6 | 98 | | var entry = GetFileSystemEntry(path); |
| | 6 | 99 | | return entry is not null && !entry.IsDirectory ? entry : null; |
| | | 100 | | } |
| | | 101 | | |
| | | 102 | | public FileSystemMetadata? GetDirectory(string path) |
| | | 103 | | { |
| | 4 | 104 | | var entry = GetFileSystemEntry(path); |
| | 4 | 105 | | return entry is not null && entry.IsDirectory ? entry : null; |
| | | 106 | | } |
| | | 107 | | |
| | | 108 | | public FileSystemMetadata? GetFileSystemEntry(string path) |
| | | 109 | | { |
| | 59 | 110 | | DropCacheIfIdleOrFull(); |
| | | 111 | | |
| | 59 | 112 | | if (!_fileCache.TryGetValue(path, out var result)) |
| | | 113 | | { |
| | 56 | 114 | | var file = _fileSystem.GetFileSystemInfo(path); |
| | 56 | 115 | | if (file?.Exists ?? false) |
| | | 116 | | { |
| | 50 | 117 | | result = file; |
| | 50 | 118 | | if (_fileCache.TryAdd(path, result)) |
| | | 119 | | { |
| | 50 | 120 | | Interlocked.Increment(ref _recordCount); |
| | | 121 | | } |
| | | 122 | | } |
| | | 123 | | } |
| | | 124 | | |
| | 59 | 125 | | return result; |
| | | 126 | | } |
| | | 127 | | |
| | | 128 | | public IReadOnlyList<string> GetFilePaths(string path) |
| | 5 | 129 | | => GetFilePaths(path, false); |
| | | 130 | | |
| | | 131 | | public IReadOnlyList<string> GetFilePaths(string path, bool clearCache) |
| | | 132 | | { |
| | 7 | 133 | | if (clearCache && _filePathCache.TryRemove(path, out var cached)) |
| | | 134 | | { |
| | 1 | 135 | | Interlocked.Add(ref _recordCount, -(cached.Count + 1)); |
| | | 136 | | } |
| | | 137 | | |
| | 7 | 138 | | DropCacheIfIdleOrFull(); |
| | | 139 | | |
| | 7 | 140 | | var filePaths = _filePathCache.GetOrAdd( |
| | 7 | 141 | | path, |
| | 7 | 142 | | static (p, state) => |
| | 7 | 143 | | { |
| | 7 | 144 | | List<string> filePaths; |
| | 7 | 145 | | try |
| | 7 | 146 | | { |
| | 7 | 147 | | filePaths = state.FileSystem.GetFilePaths(p).OrderBy(x => x).ToList(); |
| | 7 | 148 | | } |
| | 7 | 149 | | catch (DirectoryNotFoundException) |
| | 7 | 150 | | { |
| | 7 | 151 | | filePaths = []; |
| | 7 | 152 | | } |
| | 7 | 153 | | |
| | 7 | 154 | | Interlocked.Add(ref state.Service._recordCount, filePaths.Count + 1); |
| | 7 | 155 | | return filePaths; |
| | 7 | 156 | | }, |
| | 7 | 157 | | (FileSystem: _fileSystem, Service: this)); |
| | | 158 | | |
| | 7 | 159 | | return filePaths; |
| | | 160 | | } |
| | | 161 | | |
| | | 162 | | public void Invalidate(string path) |
| | | 163 | | { |
| | 67 | 164 | | Forget(path); |
| | | 165 | | |
| | 67 | 166 | | var parent = Path.GetDirectoryName(path); |
| | 67 | 167 | | if (!string.IsNullOrEmpty(parent)) |
| | | 168 | | { |
| | 67 | 169 | | Forget(parent); |
| | | 170 | | } |
| | 67 | 171 | | } |
| | | 172 | | |
| | | 173 | | public void Move(string source, string destination) |
| | | 174 | | { |
| | 3 | 175 | | Directory.Move(source, destination); |
| | | 176 | | |
| | 3 | 177 | | Invalidate(source); |
| | 3 | 178 | | Invalidate(destination); |
| | 3 | 179 | | } |
| | | 180 | | |
| | | 181 | | public bool IsAccessible(string path) |
| | | 182 | | { |
| | 49 | 183 | | return _fileSystem.GetFileSystemEntryPaths(path).Any(); |
| | | 184 | | } |
| | | 185 | | |
| | | 186 | | private void DropCacheIfIdleOrFull() |
| | | 187 | | { |
| | 745 | 188 | | var nowMs = Environment.TickCount64; |
| | 745 | 189 | | var idleMs = nowMs - _lastAccess; |
| | | 190 | | |
| | 745 | 191 | | if (idleMs >= IdleTimeoutMs || _recordCount >= MaxCachedRecords) |
| | | 192 | | { |
| | 3 | 193 | | _cache.Clear(); |
| | 3 | 194 | | _fileCache.Clear(); |
| | 3 | 195 | | _filePathCache.Clear(); |
| | 3 | 196 | | _recordCount = 0; |
| | 3 | 197 | | _lastAccess = nowMs; |
| | 3 | 198 | | return; |
| | | 199 | | } |
| | | 200 | | |
| | 742 | 201 | | if (idleMs >= AccessIntervalMs) |
| | | 202 | | { |
| | 2 | 203 | | _lastAccess = nowMs; |
| | | 204 | | } |
| | 742 | 205 | | } |
| | | 206 | | |
| | | 207 | | private void Forget(string path) |
| | | 208 | | { |
| | 134 | 209 | | if (_cache.TryRemove(path, out var entries)) |
| | | 210 | | { |
| | 54 | 211 | | Interlocked.Add(ref _recordCount, -(entries.Length + 1)); |
| | | 212 | | } |
| | | 213 | | |
| | 134 | 214 | | if (_fileCache.TryRemove(path, out _)) |
| | | 215 | | { |
| | 0 | 216 | | Interlocked.Decrement(ref _recordCount); |
| | | 217 | | } |
| | | 218 | | |
| | 134 | 219 | | if (_filePathCache.TryRemove(path, out var filePaths)) |
| | | 220 | | { |
| | 1 | 221 | | Interlocked.Add(ref _recordCount, -(filePaths.Count + 1)); |
| | | 222 | | } |
| | 134 | 223 | | } |
| | | 224 | | } |
| | | 225 | | } |