repo creation with initial code after cloning public repo

This commit is contained in:
2026-02-19 07:36:25 -05:00
commit 460884fea3
2860 changed files with 650825 additions and 0 deletions
@@ -0,0 +1,215 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
{
public sealed class FileRefresher : IDisposable
{
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly List<string> _affectedPaths = new();
private readonly Lock _timerLock = new();
private Timer? _timer;
private bool _disposed;
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
{
logger.LogDebug("New file refresher created for {0}", path);
Path = path;
_configurationManager = configurationManager;
_libraryManager = libraryManager;
_logger = logger;
AddPath(path);
}
public event EventHandler<EventArgs>? Completed;
public string Path { get; private set; }
private void AddAffectedPath(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
if (!_affectedPaths.Contains(path, StringComparer.Ordinal))
{
_affectedPaths.Add(path);
}
}
public void AddPath(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
lock (_timerLock)
{
AddAffectedPath(path);
}
RestartTimer();
}
public void RestartTimer()
{
if (_disposed)
{
return;
}
lock (_timerLock)
{
if (_disposed)
{
return;
}
if (_timer is null)
{
_timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
}
else
{
_timer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
}
}
}
public void ResetPath(string path, string? affectedFile)
{
lock (_timerLock)
{
_logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
Path = path;
AddAffectedPath(path);
if (!string.IsNullOrEmpty(affectedFile))
{
AddAffectedPath(affectedFile);
}
}
RestartTimer();
}
private void OnTimerCallback(object? state)
{
List<string> paths;
lock (_timerLock)
{
paths = _affectedPaths.ToList();
}
_logger.LogDebug("Timer stopped.");
DisposeTimer();
Completed?.Invoke(this, EventArgs.Empty);
try
{
ProcessPathChanges(paths);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing directory changes");
}
}
private void ProcessPathChanges(List<string> paths)
{
IEnumerable<BaseItem> itemsToRefresh = paths
.Distinct()
.Select(GetAffectedBaseItem)
.Where(item => item is not null)
.DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where()
foreach (var item in itemsToRefresh)
{
if (item is AggregateFolder)
{
continue;
}
_logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path);
try
{
item.ChangedExternally();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
}
}
}
/// <summary>
/// Gets the affected base item.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>BaseItem.</returns>
private BaseItem? GetAffectedBaseItem(string path)
{
BaseItem? item = null;
while (item is null && !string.IsNullOrEmpty(path))
{
item = _libraryManager.FindByPath(path, null);
path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
}
if (item is not null)
{
// If the item has been deleted find the first valid parent that still exists
while (!Directory.Exists(item.Path) && !File.Exists(item.Path))
{
item = item.GetOwner() ?? item.GetParent();
if (item is null)
{
break;
}
}
}
return item;
}
private void DisposeTimer()
{
lock (_timerLock)
{
if (_timer is not null)
{
_timer.Dispose();
_timer = null;
}
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
DisposeTimer();
_disposed = true;
}
}
}
@@ -0,0 +1,491 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
{
/// <inheritdoc cref="ILibraryMonitor" />
public sealed class LibraryMonitor : ILibraryMonitor, IDisposable
{
private readonly ILogger<LibraryMonitor> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
/// <summary>
/// The file system watchers.
/// </summary>
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// The affected paths.
/// </summary>
private readonly List<FileRefresher> _activeRefreshers = [];
/// <summary>
/// A dynamic list of paths that should be ignored. Added to during our own file system modifications.
/// </summary>
private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new(StringComparer.OrdinalIgnoreCase);
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
public LibraryMonitor(
ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
IHostApplicationLifetime appLifetime)
{
_libraryManager = libraryManager;
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
appLifetime.ApplicationStarted.Register(Start);
}
/// <inheritdoc />
public void ReportFileSystemChangeBeginning(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
_tempIgnoredPaths[path] = path;
}
/// <inheritdoc />
public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
{
ArgumentException.ThrowIfNullOrEmpty(path);
// This is an arbitrary amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
// Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
// But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
await Task.Delay(45000).ConfigureAwait(false);
_tempIgnoredPaths.TryRemove(path, out _);
if (refreshPath)
{
try
{
ReportFileSystemChanged(path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
}
}
}
private bool IsLibraryMonitorEnabled(BaseItem item)
{
if (item is BasePluginFolder)
{
return false;
}
var options = _libraryManager.GetLibraryOptions(item);
return options is not null && options.EnableRealtimeMonitor;
}
/// <inheritdoc />
public void Start()
{
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_libraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
var pathsToWatch = new List<string>();
var paths = _libraryManager
.RootFolder
.Children
.Where(IsLibraryMonitorEnabled)
.OfType<Folder>()
.SelectMany(f => f.PhysicalLocations)
.Distinct()
.Order();
foreach (var path in paths)
{
if (!ContainsParentFolder(pathsToWatch, path))
{
pathsToWatch.Add(path);
}
}
foreach (var path in pathsToWatch)
{
StartWatchingPath(path);
}
}
private void StartWatching(BaseItem item)
{
if (IsLibraryMonitorEnabled(item))
{
StartWatchingPath(item.Path);
}
}
/// <summary>
/// Handles the ItemRemoved event of the LibraryManager control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
StopWatchingPath(e.Item.Path);
}
}
/// <summary>
/// Handles the ItemAdded event of the LibraryManager control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
StartWatching(e.Item);
}
}
/// <summary>
/// Examine a list of strings assumed to be file paths to see if it contains a parent of
/// the provided path.
/// </summary>
/// <param name="lst">The LST.</param>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
{
if (path.IsEmpty)
{
throw new ArgumentException("Path can't be empty", nameof(path));
}
path = path.TrimEnd(Path.DirectorySeparatorChar);
foreach (var str in lst)
{
// this should be a little quicker than examining each actual parent folder...
var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
|| (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar))
{
return true;
}
}
return false;
}
/// <summary>
/// Starts the watching path.
/// </summary>
/// <param name="path">The path.</param>
private void StartWatchingPath(string path)
{
if (!Directory.Exists(path))
{
// Seeing a crash in the mono runtime due to an exception being thrown on a different thread
_logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
return;
}
// Already being watched
if (_fileSystemWatchers.ContainsKey(path))
{
return;
}
// Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel
Task.Run(() =>
{
try
{
var newWatcher = new FileSystemWatcher(path, "*")
{
IncludeSubdirectories = true,
InternalBufferSize = 65536,
NotifyFilter = NotifyFilters.CreationTime |
NotifyFilters.DirectoryName |
NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size |
NotifyFilters.Attributes
};
newWatcher.Created += OnWatcherChanged;
newWatcher.Deleted += OnWatcherChanged;
newWatcher.Renamed += OnWatcherChanged;
newWatcher.Changed += OnWatcherChanged;
newWatcher.Error += OnWatcherError;
if (_fileSystemWatchers.TryAdd(path, newWatcher))
{
newWatcher.EnableRaisingEvents = true;
_logger.LogInformation("Watching directory {Path}", path);
}
else
{
DisposeWatcher(newWatcher, false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error watching path: {Path}", path);
}
});
}
/// <summary>
/// Stops the watching path.
/// </summary>
/// <param name="path">The path.</param>
private void StopWatchingPath(string path)
{
if (_fileSystemWatchers.TryGetValue(path, out var watcher))
{
DisposeWatcher(watcher, true);
}
}
/// <summary>
/// Disposes the watcher.
/// </summary>
private void DisposeWatcher(FileSystemWatcher watcher, bool removeFromList)
{
try
{
using (watcher)
{
_logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
watcher.Created -= OnWatcherChanged;
watcher.Deleted -= OnWatcherChanged;
watcher.Renamed -= OnWatcherChanged;
watcher.Changed -= OnWatcherChanged;
watcher.Error -= OnWatcherError;
watcher.EnableRaisingEvents = false;
}
}
finally
{
if (removeFromList)
{
_fileSystemWatchers.TryRemove(watcher.Path, out _);
}
}
}
/// <summary>
/// Handles the Error event of the watcher control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
private void OnWatcherError(object sender, ErrorEventArgs e)
{
var ex = e.GetException();
var dw = (FileSystemWatcher)sender;
if (ex is UnauthorizedAccessException unauthorizedAccessException)
{
_logger.LogError(unauthorizedAccessException, "Permission error for Directory watcher: {Path}", dw.Path);
return;
}
_logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
DisposeWatcher(dw, true);
}
/// <summary>
/// Handles the Changed event of the watcher control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
private void OnWatcherChanged(object sender, FileSystemEventArgs e)
{
try
{
ReportFileSystemChanged(e.FullPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath);
}
}
/// <inheritdoc />
public void ReportFileSystemChanged(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
if (IgnorePatterns.ShouldIgnore(path))
{
return;
}
var fileInfo = _fileSystem.GetFileSystemInfo(path);
if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
{
return;
}
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
foreach (var i in _tempIgnoredPaths.Keys)
{
if (_fileSystem.AreEqual(i, path)
|| _fileSystem.ContainsSubPath(i, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
return;
}
// Go up a level
var parent = Path.GetDirectoryName(i);
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
return;
}
}
CreateRefresher(path);
}
private void CreateRefresher(string path)
{
var parentPath = Path.GetDirectoryName(path);
lock (_activeRefreshers)
{
foreach (var refresher in _activeRefreshers)
{
// Path is already being refreshed
if (_fileSystem.AreEqual(path, refresher.Path))
{
refresher.RestartTimer();
return;
}
// Parent folder is already being refreshed
if (_fileSystem.ContainsSubPath(refresher.Path, path))
{
refresher.AddPath(path);
return;
}
// New path is a parent
if (_fileSystem.ContainsSubPath(path, refresher.Path))
{
refresher.ResetPath(path, null);
return;
}
// They are siblings. Rebase the refresher to the parent folder.
if (parentPath is not null
&& Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
{
refresher.ResetPath(parentPath, path);
return;
}
}
var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
newRefresher.Completed += OnNewRefresherCompleted;
_activeRefreshers.Add(newRefresher);
}
}
private void OnNewRefresherCompleted(object? sender, EventArgs e)
{
if (sender is null)
{
return;
}
var refresher = (FileRefresher)sender;
DisposeRefresher(refresher);
}
/// <summary>
/// Stops this instance.
/// </summary>
public void Stop()
{
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
_libraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
foreach (var watcher in _fileSystemWatchers.Values.ToList())
{
DisposeWatcher(watcher, false);
}
_fileSystemWatchers.Clear();
DisposeRefreshers();
}
private void DisposeRefresher(FileRefresher refresher)
{
lock (_activeRefreshers)
{
refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose();
_activeRefreshers.Remove(refresher);
}
}
private void DisposeRefreshers()
{
lock (_activeRefreshers)
{
foreach (var refresher in _activeRefreshers)
{
refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose();
}
_activeRefreshers.Clear();
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
Stop();
_disposed = true;
}
}
}
@@ -0,0 +1,716 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
{
/// <summary>
/// Class ManagedFileSystem.
/// </summary>
public class ManagedFileSystem : IFileSystem
{
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
private static readonly char[] _invalidPathCharacters =
{
'\"', '<', '>', '|', '\0',
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
(char)31, ':', '*', '?', '\\', '/'
};
private readonly ILogger<ManagedFileSystem> _logger;
private readonly List<IShortcutHandler> _shortcutHandlers;
private readonly string _tempPath;
/// <summary>
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
/// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths,
IEnumerable<IShortcutHandler> shortcutHandlers)
{
_logger = logger;
_tempPath = applicationPaths.TempDirectory;
_shortcutHandlers = shortcutHandlers.ToList();
}
/// <summary>
/// Determines whether the specified filename is shortcut.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual bool IsShortcut(string filename)
{
ArgumentException.ThrowIfNullOrEmpty(filename);
var extension = Path.GetExtension(filename);
return _shortcutHandlers.Any(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Resolves the shortcut.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual string? ResolveShortcut(string filename)
{
ArgumentException.ThrowIfNullOrEmpty(filename);
var extension = Path.GetExtension(filename);
var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
return handler?.Resolve(filename);
}
/// <inheritdoc />
public virtual string MakeAbsolutePath(string folderPath, string filePath)
{
// path is actually a stream
if (string.IsNullOrWhiteSpace(filePath))
{
return filePath;
}
var isAbsolutePath = Path.IsPathRooted(filePath) && (!OperatingSystem.IsWindows() || filePath[0] != '\\');
if (isAbsolutePath)
{
// absolute local path
return filePath;
}
// unc path
if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
{
return filePath;
}
var filePathSpan = filePath.AsSpan();
// relative path on windows
if (filePath[0] == '\\')
{
filePathSpan = filePathSpan.Slice(1);
}
try
{
return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
}
catch (ArgumentException)
{
return filePath;
}
catch (PathTooLongException)
{
return filePath;
}
catch (NotSupportedException)
{
return filePath;
}
}
/// <summary>
/// Creates the shortcut.
/// </summary>
/// <param name="shortcutPath">The shortcut path.</param>
/// <param name="target">The target.</param>
/// <exception cref="ArgumentNullException">The shortcutPath or target is null.</exception>
public virtual void CreateShortcut(string shortcutPath, string target)
{
ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
ArgumentException.ThrowIfNullOrEmpty(target);
var extension = Path.GetExtension(shortcutPath);
var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
if (handler is not null)
{
handler.Create(shortcutPath, target);
}
else
{
throw new NotImplementedException();
}
}
/// <inheritdoc />
public void MoveDirectory(string source, string destination)
{
// Make sure parent directory of target exists
var parent = Directory.GetParent(destination);
parent?.Create();
try
{
Directory.Move(source, destination);
}
catch (IOException)
{
// Cross device move requires a copy
Directory.CreateDirectory(destination);
var sourceDir = new DirectoryInfo(source);
foreach (var file in sourceDir.EnumerateFiles())
{
file.CopyTo(Path.Combine(destination, file.Name), true);
}
sourceDir.Delete(true);
}
}
/// <summary>
/// Returns a <see cref="FileSystemMetadata"/> object for the specified file or directory path.
/// </summary>
/// <param name="path">A path to a file or directory.</param>
/// <returns>A <see cref="FileSystemMetadata"/> object.</returns>
/// <remarks>If the specified path points to a directory, the returned <see cref="FileSystemMetadata"/> object's
/// <see cref="FileSystemMetadata.IsDirectory"/> property will be set to true and all other properties will reflect the properties of the directory.</remarks>
public virtual FileSystemMetadata GetFileSystemInfo(string path)
{
// Take a guess to try and avoid two file system hits, but we'll double-check by calling Exists
if (Path.HasExtension(path))
{
var fileInfo = new FileInfo(path);
if (fileInfo.Exists)
{
return GetFileSystemMetadata(fileInfo);
}
return GetFileSystemMetadata(new DirectoryInfo(path));
}
else
{
var fileInfo = new DirectoryInfo(path);
if (fileInfo.Exists)
{
return GetFileSystemMetadata(fileInfo);
}
return GetFileSystemMetadata(new FileInfo(path));
}
}
/// <summary>
/// Returns a <see cref="FileSystemMetadata"/> object for the specified file path.
/// </summary>
/// <param name="path">A path to a file.</param>
/// <returns>A <see cref="FileSystemMetadata"/> object.</returns>
/// <remarks><para>If the specified path points to a directory, the returned <see cref="FileSystemMetadata"/> object's
/// <see cref="FileSystemMetadata.IsDirectory"/> property and the <see cref="FileSystemMetadata.Exists"/> property will both be set to false.</para>
/// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks>
public virtual FileSystemMetadata GetFileInfo(string path)
{
var fileInfo = new FileInfo(path);
return GetFileSystemMetadata(fileInfo);
}
/// <summary>
/// Returns a <see cref="FileSystemMetadata"/> object for the specified directory path.
/// </summary>
/// <param name="path">A path to a directory.</param>
/// <returns>A <see cref="FileSystemMetadata"/> object.</returns>
/// <remarks><para>If the specified path points to a file, the returned <see cref="FileSystemMetadata"/> object's
/// <see cref="FileSystemMetadata.IsDirectory"/> property will be set to true and the <see cref="FileSystemMetadata.Exists"/> property will be set to false.</para>
/// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks>
public virtual FileSystemMetadata GetDirectoryInfo(string path)
{
var fileInfo = new DirectoryInfo(path);
return GetFileSystemMetadata(fileInfo);
}
private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info)
{
var result = new FileSystemMetadata
{
Exists = info.Exists,
FullName = info.FullName,
Extension = info.Extension,
Name = info.Name
};
if (result.Exists)
{
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
if (info is FileInfo fileInfo)
{
result.CreationTimeUtc = GetCreationTimeUtc(info);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
if (fileInfo.LinkTarget is not null)
{
try
{
var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true);
if (targetFileInfo is not null)
{
result.Exists = targetFileInfo.Exists;
if (result.Exists)
{
result.Length = targetFileInfo.Length;
result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
}
}
else
{
result.Exists = false;
}
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
}
}
else
{
result.Length = fileInfo.Length;
}
}
}
else
{
result.IsDirectory = info is DirectoryInfo;
}
return result;
}
/// <summary>
/// Takes a filename and removes invalid characters.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">The filename is null.</exception>
public string GetValidFilename(string filename)
{
var first = filename.IndexOfAny(_invalidPathCharacters);
if (first == -1)
{
// Fast path for clean strings
return filename;
}
return string.Create(
filename.Length,
(filename, _invalidPathCharacters, first),
(chars, state) =>
{
state.filename.AsSpan().CopyTo(chars);
chars[state.first++] = ' ';
var len = chars.Length;
foreach (var c in state._invalidPathCharacters)
{
for (int i = state.first; i < len; i++)
{
if (chars[i] == c)
{
chars[i] = ' ';
}
}
}
});
}
/// <summary>
/// Gets the creation time UTC.
/// </summary>
/// <param name="info">The info.</param>
/// <returns>DateTime.</returns>
public DateTime GetCreationTimeUtc(FileSystemInfo info)
{
// This could throw an error on some file systems that have dates out of range
try
{
return info.CreationTimeUtc;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
/// <inheritdoc />
public virtual DateTime GetCreationTimeUtc(string path)
{
return GetCreationTimeUtc(GetFileSystemInfo(path));
}
/// <inheritdoc />
public virtual DateTime GetCreationTimeUtc(FileSystemMetadata info)
{
return info.CreationTimeUtc;
}
/// <inheritdoc />
public virtual DateTime GetLastWriteTimeUtc(FileSystemMetadata info)
{
return info.LastWriteTimeUtc;
}
/// <summary>
/// Gets the creation time UTC.
/// </summary>
/// <param name="info">The info.</param>
/// <returns>DateTime.</returns>
public DateTime GetLastWriteTimeUtc(FileSystemInfo info)
{
// This could throw an error on some file systems that have dates out of range
try
{
return info.LastWriteTimeUtc;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
/// <inheritdoc />
public virtual DateTime GetLastWriteTimeUtc(string path)
{
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
}
/// <inheritdoc />
public virtual void SetHidden(string path, bool isHidden)
{
if (!OperatingSystem.IsWindows())
{
return;
}
var info = new FileInfo(path);
if (info.Exists &&
(info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden != isHidden)
{
if (isHidden)
{
File.SetAttributes(path, info.Attributes | FileAttributes.Hidden);
}
else
{
File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden);
}
}
}
/// <inheritdoc />
public virtual void SetAttributes(string path, bool isHidden, bool readOnly)
{
if (!OperatingSystem.IsWindows())
{
return;
}
var info = new FileInfo(path);
if (!info.Exists)
{
return;
}
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly == readOnly
&& (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden == isHidden)
{
return;
}
var attributes = info.Attributes;
if (readOnly)
{
attributes |= FileAttributes.ReadOnly;
}
else
{
attributes &= ~FileAttributes.ReadOnly;
}
if (isHidden)
{
attributes |= FileAttributes.Hidden;
}
else
{
attributes &= ~FileAttributes.Hidden;
}
File.SetAttributes(path, attributes);
}
/// <inheritdoc />
public virtual void SwapFiles(string file1, string file2)
{
ArgumentException.ThrowIfNullOrEmpty(file1);
ArgumentException.ThrowIfNullOrEmpty(file2);
var temp1 = Path.Combine(_tempPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
// Copying over will fail against hidden files
SetHidden(file1, false);
SetHidden(file2, false);
Directory.CreateDirectory(_tempPath);
File.Copy(file1, temp1, true);
File.Copy(file2, file1, true);
File.Move(temp1, file2, true);
}
/// <inheritdoc />
public virtual bool ContainsSubPath(string parentPath, string path)
{
ArgumentException.ThrowIfNullOrEmpty(parentPath);
ArgumentException.ThrowIfNullOrEmpty(path);
return path.Contains(
Path.TrimEndingDirectorySeparator(parentPath) + Path.DirectorySeparatorChar,
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
/// <inheritdoc />
public virtual bool AreEqual(string path1, string path2)
{
if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2))
{
return false;
}
var normalized1 = Path.TrimEndingDirectorySeparator(path1);
var normalized2 = Path.TrimEndingDirectorySeparator(path2);
return string.Equals(
normalized1,
normalized2,
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
/// <inheritdoc />
public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
{
if (info.IsDirectory)
{
return info.Name;
}
return Path.GetFileNameWithoutExtension(info.FullName);
}
/// <inheritdoc />
public virtual bool IsPathFile(string path)
{
if (path.Contains("://", StringComparison.OrdinalIgnoreCase)
&& !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
/// <inheritdoc />
public virtual void DeleteFile(string path)
{
SetAttributes(path, false, false);
File.Delete(path);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetDrives()
{
// check for ready state to avoid waiting for drives to timeout
// some drives on linux have no actual size or are used for other purposes
return DriveInfo.GetDrives()
.Where(
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
&& d.IsReady
&& d.TotalSize != 0)
.Select(d => new FileSystemMetadata
{
Name = d.Name,
FullName = d.RootDirectory.FullName,
IsDirectory = true
});
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
{
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
{
return GetFiles(path, "*", recursive);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, bool recursive = false)
{
return GetFiles(path, searchPattern, null, false, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive)
{
return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and macOS the search pattern is case-sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
{
searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0];
return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions));
}
var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions);
if (extensions is not null && extensions.Count > 0)
{
files = files.Where(i =>
{
var ext = i.Extension.AsSpan();
if (ext.IsEmpty)
{
return false;
}
return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}
return ToMetadata(files);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
// Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
// But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
// For example, the scanner will remove everything in that path due to unhandled errors.
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);
return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions));
}
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
{
return infos.Select(GetFileSystemMetadata);
}
/// <inheritdoc />
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
{
return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
}
/// <inheritdoc />
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
{
return GetFilePaths(path, null, false, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and macOS the search pattern is case-sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1)
{
return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions);
}
var files = Directory.EnumerateFiles(path, "*", enumerationOptions);
if (extensions is not null && extensions.Length > 0)
{
files = files.Where(i =>
{
var ext = Path.GetExtension(i.AsSpan());
if (ext.IsEmpty)
{
return false;
}
return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}
return files;
}
/// <inheritdoc />
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{
try
{
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
}
catch (Exception ex) when (ex is UnauthorizedAccessException or DirectoryNotFoundException or SecurityException)
{
_logger.LogError(ex, "Failed to enumerate path {Path}", path);
return Enumerable.Empty<string>();
}
}
/// <inheritdoc />
public virtual bool DirectoryExists(string path)
{
return Directory.Exists(path);
}
/// <inheritdoc />
public virtual bool FileExists(string path)
{
return File.Exists(path);
}
private EnumerationOptions GetEnumerationOptions(bool recursive)
{
return new EnumerationOptions
{
RecurseSubdirectories = recursive,
IgnoreInaccessible = true,
// Don't skip any files.
AttributesToSkip = 0
};
}
}
}
@@ -0,0 +1,35 @@
#pragma warning disable CS1591
using System;
using System.IO;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.IO
{
public class MbLinkShortcutHandler : IShortcutHandler
{
public string Extension => ".mblink";
public string? Resolve(string shortcutPath)
{
ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase))
{
var path = File.ReadAllText(shortcutPath);
return Path.TrimEndingDirectorySeparator(path);
}
return null;
}
public void Create(string shortcutPath, string targetPath)
{
ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
ArgumentException.ThrowIfNullOrEmpty(targetPath);
File.WriteAllText(shortcutPath, targetPath);
}
}
}