Files

203 lines
8.3 KiB
C#

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
{
/// <summary>
/// Provides access to the local file system to a <see cref="FileModule"/>.
/// </summary>
/// <seealso cref="IFileProvider" />
public class FileSystemProvider : IDisposable, IFileProvider
{
private readonly FileSystemWatcher? _watcher;
/// <summary>
/// Initializes a new instance of the <see cref="FileSystemProvider"/> class.
/// </summary>
/// <remarks>
/// OSX doesn't support <see cref="FileSystemWatcher" />, the parameter <paramref name="isImmutable" /> will be always <see langword="true"/>.
/// </remarks>
/// <param name="fileSystemPath">The file system path.</param>
/// <param name="isImmutable"><see langword="true"/> if files and directories in
/// <paramref name="fileSystemPath"/> are not expected to change during a web server's
/// lifetime; <see langword="false"/> otherwise.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileSystemPath"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="fileSystemPath"/> is not a valid local path.</exception>
/// <seealso cref="Validate.LocalPath"/>
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;
}
}
/// <summary>
/// Finalizes an instance of the <see cref="FileSystemProvider"/> class.
/// </summary>
~FileSystemProvider()
{
Dispose(false);
}
/// <inheritdoc />
public event Action<string>? ResourceChanged;
/// <summary>
/// Gets the file system path from which files are retrieved.
/// </summary>
public string FileSystemPath { get; }
/// <inheritdoc />
public bool IsImmutable { get; }
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void Start(CancellationToken cancellationToken)
{
if (_watcher != null)
{
_watcher.Changed += Watcher_ChangedOrDeleted;
_watcher.Deleted += Watcher_ChangedOrDeleted;
_watcher.Renamed += Watcher_Renamed;
_watcher.EnableRaisingEvents = true;
}
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public Stream OpenFile(string path) => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
/// <inheritdoc />
public IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
=> new DirectoryInfo(path).EnumerateFileSystemInfos()
.Select(fsi => GetMappedResourceInfo(mimeTypeProvider, fsi));
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources;
/// <see langword="false"/> to release only unmanaged resources.</param>
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);
}
}