using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using EmbedIO.Utilities; namespace EmbedIO.Files { /// /// Provides access to the local file system to a . /// /// public class FileSystemProvider : IDisposable, IFileProvider { private readonly FileSystemWatcher? _watcher; /// /// Initializes a new instance of the class. /// /// /// OSX doesn't support , the parameter will be always . /// /// The file system path. /// if files and directories in /// are not expected to change during a web server's /// lifetime; otherwise. /// is . /// is not a valid local path. /// public FileSystemProvider(string fileSystemPath, bool isImmutable) { FileSystemPath = Validate.LocalPath(nameof(fileSystemPath), fileSystemPath, true); IsImmutable = isImmutable || RuntimeInformation.IsOSPlatform(OSPlatform.OSX); try { if (!IsImmutable) _watcher = new FileSystemWatcher(FileSystemPath); } catch (PlatformNotSupportedException) { IsImmutable = true; } } /// /// Finalizes an instance of the class. /// ~FileSystemProvider() { Dispose(false); } /// public event Action? ResourceChanged; /// /// Gets the file system path from which files are retrieved. /// public string FileSystemPath { get; } /// public bool IsImmutable { get; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// public void Start(CancellationToken cancellationToken) { if (_watcher != null) { _watcher.Changed += Watcher_ChangedOrDeleted; _watcher.Deleted += Watcher_ChangedOrDeleted; _watcher.Renamed += Watcher_Renamed; _watcher.EnableRaisingEvents = true; } } /// public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider) { urlPath = urlPath.Substring(1); // Drop the initial slash string localPath; // Disable CA1031 as there's little we can do if IsPathRooted or GetFullPath fails. #pragma warning disable CA1031 try { // Unescape the url before continue urlPath = Uri.UnescapeDataString(urlPath); // Bail out early if the path is a rooted path, // as Path.Combine would ignore our base path. // See https://docs.microsoft.com/en-us/dotnet/api/system.io.path.combine // (particularly the Remarks section). // // Under Windows, a relative URL path may be a full filesystem path // (e.g. "D:\foo\bar" or "\\192.168.0.1\Shared\MyDocuments\BankAccounts.docx"). // Under Unix-like operating systems we have no such problems, as relativeUrlPath // can never start with a slash; however, loading one more class from Swan // just to check the OS type would probably outweigh calling IsPathRooted. if (Path.IsPathRooted(urlPath)) return null; // Convert the relative URL path to a relative filesystem path // (practically a no-op under Unix-like operating systems) // and combine it with our base local path to obtain a full path. localPath = Path.Combine(FileSystemPath, urlPath.Replace('/', Path.DirectorySeparatorChar)); // Use GetFullPath as an additional safety check // for relative paths that contain a rooted path // (e.g. "valid/path/C:\Windows\System.ini") localPath = Path.GetFullPath(localPath); } catch { // Both IsPathRooted and GetFullPath throw exceptions // if a path contains invalid characters or is otherwise invalid; // bail out in this case too, as the path would not exist on disk anyway. return null; } #pragma warning restore CA1031 // As a final precaution, check that the resulting local path // is inside the folder intended to be served. if (!localPath.StartsWith(FileSystemPath, StringComparison.Ordinal)) return null; if (File.Exists(localPath)) return GetMappedFileInfo(mimeTypeProvider, localPath); if (Directory.Exists(localPath)) return GetMappedDirectoryInfo(localPath); return null; } /// public Stream OpenFile(string path) => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); /// public IEnumerable GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider) => new DirectoryInfo(path).EnumerateFileSystemInfos() .Select(fsi => GetMappedResourceInfo(mimeTypeProvider, fsi)); /// /// Releases unmanaged and - optionally - managed resources. /// /// to release both managed and unmanaged resources; /// to release only unmanaged resources. protected virtual void Dispose(bool disposing) { ResourceChanged = null; // Release references to listeners if (_watcher != null) { _watcher.EnableRaisingEvents = false; _watcher.Changed -= Watcher_ChangedOrDeleted; _watcher.Deleted -= Watcher_ChangedOrDeleted; _watcher.Renamed -= Watcher_Renamed; if (disposing) _watcher.Dispose(); } } private static MappedResourceInfo GetMappedFileInfo(IMimeTypeProvider mimeTypeProvider, string localPath) => GetMappedFileInfo(mimeTypeProvider, new FileInfo(localPath)); private static MappedResourceInfo GetMappedFileInfo(IMimeTypeProvider mimeTypeProvider, FileInfo info) => MappedResourceInfo.ForFile( info.FullName, info.Name, info.LastWriteTimeUtc, info.Length, mimeTypeProvider.GetMimeType(info.Extension)); private static MappedResourceInfo GetMappedDirectoryInfo(string localPath) => GetMappedDirectoryInfo(new DirectoryInfo(localPath)); private static MappedResourceInfo GetMappedDirectoryInfo(DirectoryInfo info) => MappedResourceInfo.ForDirectory(info.FullName, info.Name, info.LastWriteTimeUtc); private static MappedResourceInfo GetMappedResourceInfo(IMimeTypeProvider mimeTypeProvider, FileSystemInfo info) => info is DirectoryInfo directoryInfo ? GetMappedDirectoryInfo(directoryInfo) : GetMappedFileInfo(mimeTypeProvider, (FileInfo) info); private void Watcher_ChangedOrDeleted(object sender, FileSystemEventArgs e) => ResourceChanged?.Invoke(e.FullPath); private void Watcher_Renamed(object sender, RenamedEventArgs e) => ResourceChanged?.Invoke(e.OldFullPath); } }