Got at least one data fetching method working; turns out, we can't use a patched LogicStack to get the data
This commit is contained in:
20
Vendor/EmbedIO-3.5.2/Files/DirectoryLister.cs
vendored
Normal file
20
Vendor/EmbedIO-3.5.2/Files/DirectoryLister.cs
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
using EmbedIO.Files.Internal;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides standard directory listers for <see cref="FileModule"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="IDirectoryLister"/>
|
||||
public static class DirectoryLister
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Gets an <see cref="IDirectoryLister"/> interface
|
||||
/// that produces a HTML listing of a directory.</para>
|
||||
/// <para>The output of the returned directory lister
|
||||
/// is the same as a directory listing obtained
|
||||
/// by EmbedIO version 2.</para>
|
||||
/// </summary>
|
||||
public static IDirectoryLister Html => HtmlDirectoryLister.Instance;
|
||||
}
|
||||
}
|
||||
185
Vendor/EmbedIO-3.5.2/Files/FileCache.Section.cs
vendored
Normal file
185
Vendor/EmbedIO-3.5.2/Files/FileCache.Section.cs
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using EmbedIO.Files.Internal;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
public sealed partial class FileCache
|
||||
{
|
||||
internal class Section
|
||||
{
|
||||
private readonly object _syncRoot = new object();
|
||||
private readonly Dictionary<string, FileCacheItem> _items = new Dictionary<string, FileCacheItem>(StringComparer.Ordinal);
|
||||
private long _totalSize;
|
||||
private string? _oldestKey;
|
||||
private string? _newestKey;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
ClearCore();
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(string path, FileCacheItem item)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
AddItemCore(path, item);
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string path)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
RemoveItemCore(path);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGet(string path, out FileCacheItem item)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (!_items.TryGetValue(path, out item))
|
||||
return false;
|
||||
|
||||
RefreshItemCore(path, item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal long GetLeastRecentUseTime()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _oldestKey == null ? long.MaxValue : _items[_oldestKey].LastUsedAt;
|
||||
}
|
||||
}
|
||||
|
||||
// Removes least recently used item.
|
||||
// Returns size of removed item.
|
||||
internal long RemoveLeastRecentItem()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return RemoveLeastRecentItemCore();
|
||||
}
|
||||
}
|
||||
|
||||
internal long GetTotalSize()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _totalSize;
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateTotalSize(long delta)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_totalSize += delta;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearCore()
|
||||
{
|
||||
_items.Clear();
|
||||
_totalSize = 0;
|
||||
_oldestKey = null;
|
||||
_newestKey = null;
|
||||
}
|
||||
|
||||
// Adds an item as most recently used.
|
||||
private void AddItemCore(string path, FileCacheItem item)
|
||||
{
|
||||
item.PreviousKey = _newestKey;
|
||||
item.NextKey = null;
|
||||
item.LastUsedAt = TimeBase.ElapsedTicks;
|
||||
|
||||
if (_newestKey != null)
|
||||
_items[_newestKey].NextKey = path;
|
||||
|
||||
_newestKey = path;
|
||||
|
||||
_items[path] = item;
|
||||
_totalSize += item.SizeInCache;
|
||||
}
|
||||
|
||||
// Removes an item.
|
||||
private void RemoveItemCore(string path)
|
||||
{
|
||||
if (!_items.TryGetValue(path, out var item))
|
||||
return;
|
||||
|
||||
if (_oldestKey == path)
|
||||
_oldestKey = item.NextKey;
|
||||
|
||||
if (_newestKey == path)
|
||||
_newestKey = item.PreviousKey;
|
||||
|
||||
if (item.PreviousKey != null)
|
||||
_items[item.PreviousKey].NextKey = item.NextKey;
|
||||
|
||||
if (item.NextKey != null)
|
||||
_items[item.NextKey].PreviousKey = item.PreviousKey;
|
||||
|
||||
item.PreviousKey = null;
|
||||
item.NextKey = null;
|
||||
|
||||
_items.Remove(path);
|
||||
_totalSize -= item.SizeInCache;
|
||||
}
|
||||
|
||||
// Removes the least recently used item.
|
||||
// returns size of removed item.
|
||||
private long RemoveLeastRecentItemCore()
|
||||
{
|
||||
var path = _oldestKey;
|
||||
if (path == null)
|
||||
return 0;
|
||||
|
||||
var item = _items[path];
|
||||
|
||||
if ((_oldestKey = item.NextKey) != null)
|
||||
_items[_oldestKey].PreviousKey = null;
|
||||
|
||||
if (_newestKey == path)
|
||||
_newestKey = null;
|
||||
|
||||
item.PreviousKey = null;
|
||||
item.NextKey = null;
|
||||
|
||||
_items.Remove(path);
|
||||
_totalSize -= item.SizeInCache;
|
||||
return item.SizeInCache;
|
||||
}
|
||||
|
||||
// Moves an item to most recently used.
|
||||
private void RefreshItemCore(string path, FileCacheItem item)
|
||||
{
|
||||
item.LastUsedAt = TimeBase.ElapsedTicks;
|
||||
|
||||
if (_newestKey == path)
|
||||
return;
|
||||
|
||||
if (_oldestKey == path)
|
||||
_oldestKey = item.NextKey;
|
||||
|
||||
if (item.PreviousKey != null)
|
||||
_items[item.PreviousKey].NextKey = item.NextKey;
|
||||
|
||||
if (item.NextKey != null)
|
||||
_items[item.NextKey].PreviousKey = item.PreviousKey;
|
||||
|
||||
item.PreviousKey = _newestKey;
|
||||
item.NextKey = null;
|
||||
|
||||
_items[_newestKey!].NextKey = path;
|
||||
_newestKey = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
178
Vendor/EmbedIO-3.5.2/Files/FileCache.cs
vendored
Normal file
178
Vendor/EmbedIO-3.5.2/Files/FileCache.cs
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EmbedIO.Internal;
|
||||
using Swan.Threading;
|
||||
using Swan.Logging;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
#pragma warning disable CA1001 // Type owns disposable field '_cleaner' but is not disposable - _cleaner has its own dispose semantics.
|
||||
/// <summary>
|
||||
/// A cache where one or more instances of <see cref="FileModule"/> can store hashes and file contents.
|
||||
/// </summary>
|
||||
public sealed partial class FileCache
|
||||
#pragma warning restore CA1001
|
||||
{
|
||||
/// <summary>
|
||||
/// The default value for the <see cref="MaxSizeKb"/> property.
|
||||
/// </summary>
|
||||
public const int DefaultMaxSizeKb = 10240;
|
||||
|
||||
/// <summary>
|
||||
/// The default value for the <see cref="MaxFileSizeKb"/> property.
|
||||
/// </summary>
|
||||
public const int DefaultMaxFileSizeKb = 200;
|
||||
|
||||
private static readonly Stopwatch TimeBase = Stopwatch.StartNew();
|
||||
|
||||
private static readonly object DefaultSyncRoot = new object();
|
||||
private static FileCache? _defaultInstance;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Section> _sections = new ConcurrentDictionary<string, Section>(StringComparer.Ordinal);
|
||||
private int _sectionCount; // Because ConcurrentDictionary<,>.Count is locking.
|
||||
private int _maxSizeKb = DefaultMaxSizeKb;
|
||||
private int _maxFileSizeKb = DefaultMaxFileSizeKb;
|
||||
private PeriodicTask? _cleaner;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="FileCache"/> instance used by <see cref="FileModule"/>.
|
||||
/// </summary>
|
||||
public static FileCache Default
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_defaultInstance != null)
|
||||
return _defaultInstance;
|
||||
|
||||
lock (DefaultSyncRoot)
|
||||
{
|
||||
if (_defaultInstance == null)
|
||||
_defaultInstance = new FileCache();
|
||||
}
|
||||
|
||||
return _defaultInstance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets the maximum total size of cached data in kilobytes (1 kilobyte = 1024 bytes).</para>
|
||||
/// <para>The default value for this property is stored in the <see cref="DefaultMaxSizeKb"/> constant field.</para>
|
||||
/// <para>Setting this property to a value less lower han 1 has the same effect as setting it to 1.</para>
|
||||
/// </summary>
|
||||
public int MaxSizeKb
|
||||
{
|
||||
get => _maxSizeKb;
|
||||
set => _maxSizeKb = Math.Max(value, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets the maximum size of a single cached file in kilobytes (1 kilobyte = 1024 bytes).</para>
|
||||
/// <para>A single file's contents may be present in a cache more than once, if the file
|
||||
/// is requested with different <c>Accept-Encoding</c> request headers. This property acts as a threshold
|
||||
/// for the uncompressed size of a file.</para>
|
||||
/// <para>The default value for this property is stored in the <see cref="DefaultMaxFileSizeKb"/> constant field.</para>
|
||||
/// <para>Setting this property to a value lower than 0 has the same effect as setting it to 0, in fact
|
||||
/// completely disabling the caching of file contents for this cache.</para>
|
||||
/// <para>This property cannot be set to a value higher than 2097151; in other words, it is not possible
|
||||
/// to cache files bigger than two Gigabytes (1 Gigabyte = 1048576 kilobytes) minus 1 kilobyte.</para>
|
||||
/// </summary>
|
||||
public int MaxFileSizeKb
|
||||
{
|
||||
get => _maxFileSizeKb;
|
||||
set => _maxFileSizeKb = Math.Min(Math.Max(value, 0), 2097151);
|
||||
}
|
||||
|
||||
// Cast as IDictionary because we WANT an exception to be thrown if the name exists.
|
||||
// It would mean that something is very, very wrong.
|
||||
internal Section AddSection(string name)
|
||||
{
|
||||
var section = new Section();
|
||||
(_sections as IDictionary<string, Section>).Add(name, section);
|
||||
|
||||
if (Interlocked.Increment(ref _sectionCount) == 1)
|
||||
_cleaner = new PeriodicTask(TimeSpan.FromMinutes(1), CheckMaxSize);
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
internal void RemoveSection(string name)
|
||||
{
|
||||
_sections.TryRemove(name, out _);
|
||||
|
||||
if (Interlocked.Decrement(ref _sectionCount) == 0)
|
||||
{
|
||||
_cleaner?.Dispose();
|
||||
_cleaner = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckMaxSize(CancellationToken cancellationToken)
|
||||
{
|
||||
var timeKeeper = new TimeKeeper();
|
||||
var maxSizeKb = _maxSizeKb;
|
||||
var initialSizeKb = ComputeTotalSize() / 1024L;
|
||||
|
||||
if (initialSizeKb <= maxSizeKb)
|
||||
{
|
||||
$"Total size = {initialSizeKb}/{_maxSizeKb}kb, not purging.".Debug(nameof(FileCache));
|
||||
return;
|
||||
}
|
||||
|
||||
$"Total size = {initialSizeKb}/{_maxSizeKb}kb, purging...".Debug(nameof(FileCache));
|
||||
|
||||
var removedCount = 0;
|
||||
var removedSize = 0L;
|
||||
var totalSizeKb = initialSizeKb;
|
||||
var threshold = 973L * maxSizeKb / 1024L; // About 95% of maximum allowed size
|
||||
while (totalSizeKb > threshold)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var section = GetSectionWithLeastRecentItem();
|
||||
if (section == null)
|
||||
return;
|
||||
|
||||
removedSize += section.RemoveLeastRecentItem();
|
||||
removedCount++;
|
||||
|
||||
await Task.Yield();
|
||||
|
||||
totalSizeKb = ComputeTotalSize() / 1024L;
|
||||
}
|
||||
|
||||
$"Purge completed in {timeKeeper.ElapsedTime}ms: removed {removedCount} items ({removedSize / 1024L}kb). Total size is now {totalSizeKb}kb."
|
||||
.Debug(nameof(FileCache));
|
||||
}
|
||||
|
||||
// Enumerate key / value pairs because the Keys and Values property
|
||||
// of ConcurrentDictionary<,> have snapshot semantics,
|
||||
// while GetEnumerator enumerates without locking.
|
||||
private long ComputeTotalSize()
|
||||
=> _sections.Sum(pair => pair.Value.GetTotalSize());
|
||||
|
||||
private Section? GetSectionWithLeastRecentItem()
|
||||
{
|
||||
Section? result = null;
|
||||
var earliestTime = long.MaxValue;
|
||||
foreach (var pair in _sections)
|
||||
{
|
||||
var section = pair.Value;
|
||||
var time = section.GetLeastRecentUseTime();
|
||||
|
||||
if (time < earliestTime)
|
||||
{
|
||||
result = section;
|
||||
earliestTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
635
Vendor/EmbedIO-3.5.2/Files/FileModule.cs
vendored
Normal file
635
Vendor/EmbedIO-3.5.2/Files/FileModule.cs
vendored
Normal file
@@ -0,0 +1,635 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EmbedIO.Files.Internal;
|
||||
using EmbedIO.Internal;
|
||||
using EmbedIO.Utilities;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// A module serving files and directory listings from an <see cref="IFileProvider"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="WebModuleBase" />
|
||||
public class FileModule : WebModuleBase, IDisposable, IMimeTypeCustomizer
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Default value for <see cref="DefaultDocument"/>.</para>
|
||||
/// </summary>
|
||||
public const string DefaultDocumentName = "index.html";
|
||||
|
||||
private readonly string _cacheSectionName = UniqueIdGenerator.GetNext();
|
||||
private readonly MimeTypeCustomizer _mimeTypeCustomizer = new MimeTypeCustomizer();
|
||||
private readonly ConcurrentDictionary<string, MappedResourceInfo>? _mappingCache;
|
||||
|
||||
private FileCache _cache = FileCache.Default;
|
||||
private bool _contentCaching = true;
|
||||
private string? _defaultDocument = DefaultDocumentName;
|
||||
private string? _defaultExtension;
|
||||
private IDirectoryLister? _directoryLister;
|
||||
private FileRequestHandlerCallback _onMappingFailed = FileRequestHandler.ThrowNotFound;
|
||||
private FileRequestHandlerCallback _onDirectoryNotListable = FileRequestHandler.ThrowUnauthorized;
|
||||
private FileRequestHandlerCallback _onMethodNotAllowed = FileRequestHandler.ThrowMethodNotAllowed;
|
||||
|
||||
private FileCache.Section? _cacheSection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileModule"/> class,
|
||||
/// using the specified cache.
|
||||
/// </summary>
|
||||
/// <param name="baseRoute">The base route.</param>
|
||||
/// <param name="provider">An <see cref="IFileProvider"/> interface that provides access
|
||||
/// to actual files and directories.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <see langword="null"/>.</exception>
|
||||
public FileModule(string baseRoute, IFileProvider provider)
|
||||
: base(baseRoute)
|
||||
{
|
||||
Provider = Validate.NotNull(nameof(provider), provider);
|
||||
_mappingCache = Provider.IsImmutable
|
||||
? new ConcurrentDictionary<string, MappedResourceInfo>()
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="FileModule"/> class.
|
||||
/// </summary>
|
||||
~FileModule()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsFinalHandler => true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IFileProvider"/>interface that provides access
|
||||
/// to actual files and directories served by this module.
|
||||
/// </summary>
|
||||
public IFileProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="FileCache"/> used by this module to store hashes and,
|
||||
/// optionally, file contents and rendered directory listings.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
|
||||
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
|
||||
public FileCache Cache
|
||||
{
|
||||
get => _cache;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
_cache = Validate.NotNull(nameof(value), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets a value indicating whether this module caches the contents of files
|
||||
/// and directory listings.</para>
|
||||
/// <para>Note that the actual representations of files are stored in <see cref="FileCache"/>;
|
||||
/// thus, for example, if a file is always requested with an <c>Accept-Encoding</c> of <c>gzip</c>,
|
||||
/// only the gzipped contents of the file will be cached.</para>
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
|
||||
public bool ContentCaching
|
||||
{
|
||||
get => _contentCaching;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
_contentCaching = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets the name of the default document served, if it exists, instead of a directory listing
|
||||
/// when the path of a requested URL maps to a directory.</para>
|
||||
/// <para>The default value for this property is the <see cref="DefaultDocumentName"/> constant.</para>
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
|
||||
public string? DefaultDocument
|
||||
{
|
||||
get => _defaultDocument;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
_defaultDocument = string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets the default extension appended to requested URL paths that do not map
|
||||
/// to any file or directory. Defaults to <see langword="null"/>.</para>
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
|
||||
/// <exception cref="ArgumentException">This property is being set to a non-<see langword="null"/>,
|
||||
/// non-empty string that does not start with a period (<c>.</c>).</exception>
|
||||
public string? DefaultExtension
|
||||
{
|
||||
get => _defaultExtension;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_defaultExtension = null;
|
||||
}
|
||||
else if (value![0] != '.')
|
||||
{
|
||||
throw new ArgumentException("Default extension does not start with a period.", nameof(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
_defaultExtension = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets the <see cref="IDirectoryLister"/> interface used to generate
|
||||
/// directory listing in this module.</para>
|
||||
/// <para>A value of <see langword="null"/> (the default) disables the generation
|
||||
/// of directory listings.</para>
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
|
||||
public IDirectoryLister? DirectoryLister
|
||||
{
|
||||
get => _directoryLister;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
_directoryLister = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets a <see cref="FileRequestHandlerCallback"/> that is called whenever
|
||||
/// the requested URL path could not be mapped to any file or directory.</para>
|
||||
/// <para>The default is <see cref="FileRequestHandler.ThrowNotFound"/>.</para>
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
|
||||
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
|
||||
/// <seealso cref="FileRequestHandler"/>
|
||||
public FileRequestHandlerCallback OnMappingFailed
|
||||
{
|
||||
get => _onMappingFailed;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
_onMappingFailed = Validate.NotNull(nameof(value), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets a <see cref="FileRequestHandlerCallback"/> that is called whenever
|
||||
/// the requested URL path has been mapped to a directory, but directory listing has been
|
||||
/// disabled by setting <see cref="DirectoryLister"/> to <see langword="null"/>.</para>
|
||||
/// <para>The default is <see cref="FileRequestHandler.ThrowUnauthorized"/>.</para>
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
|
||||
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
|
||||
/// <seealso cref="FileRequestHandler"/>
|
||||
public FileRequestHandlerCallback OnDirectoryNotListable
|
||||
{
|
||||
get => _onDirectoryNotListable;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
_onDirectoryNotListable = Validate.NotNull(nameof(value), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets a <see cref="FileRequestHandlerCallback"/> that is called whenever
|
||||
/// the requested URL path has been mapped to a file or directory, but the request's
|
||||
/// HTTP method is neither <c>GET</c> nor <c>HEAD</c>.</para>
|
||||
/// <para>The default is <see cref="FileRequestHandler.ThrowMethodNotAllowed"/>.</para>
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The module's configuration is locked.</exception>
|
||||
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
|
||||
/// <seealso cref="FileRequestHandler"/>
|
||||
public FileRequestHandlerCallback OnMethodNotAllowed
|
||||
{
|
||||
get => _onMethodNotAllowed;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
_onMethodNotAllowed = Validate.NotNull(nameof(value), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
string IMimeTypeProvider.GetMimeType(string extension)
|
||||
=> _mimeTypeCustomizer.GetMimeType(extension);
|
||||
|
||||
bool IMimeTypeProvider.TryDetermineCompression(string mimeType, out bool preferCompression)
|
||||
=> _mimeTypeCustomizer.TryDetermineCompression(mimeType, out preferCompression);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddCustomMimeType(string extension, string mimeType)
|
||||
=> _mimeTypeCustomizer.AddCustomMimeType(extension, mimeType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PreferCompression(string mimeType, bool preferCompression)
|
||||
=> _mimeTypeCustomizer.PreferCompression(mimeType, preferCompression);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the part of <see cref="Cache"/> used by this module.
|
||||
/// </summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
_mappingCache?.Clear();
|
||||
_cacheSection?.Clear();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
if (_cacheSection != null)
|
||||
Provider.ResourceChanged -= _cacheSection.Remove;
|
||||
|
||||
if (Provider is IDisposable disposableProvider)
|
||||
disposableProvider.Dispose();
|
||||
|
||||
if (_cacheSection != null)
|
||||
Cache.RemoveSection(_cacheSectionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnBeforeLockConfiguration()
|
||||
{
|
||||
base.OnBeforeLockConfiguration();
|
||||
|
||||
_mimeTypeCustomizer.Lock();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnStart(CancellationToken cancellationToken)
|
||||
{
|
||||
base.OnStart(cancellationToken);
|
||||
|
||||
_cacheSection = Cache.AddSection(_cacheSectionName);
|
||||
Provider.ResourceChanged += _cacheSection.Remove;
|
||||
Provider.Start(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnRequestAsync(IHttpContext context)
|
||||
{
|
||||
MappedResourceInfo? info;
|
||||
|
||||
var path = context.RequestedPath;
|
||||
|
||||
// Map the URL path to a mapped resource.
|
||||
// DefaultDocument and DefaultExtension are handled here.
|
||||
// Use the mapping cache if it exists.
|
||||
if (_mappingCache == null)
|
||||
{
|
||||
info = MapUrlPath(path, context);
|
||||
}
|
||||
else if (!_mappingCache.TryGetValue(path, out info))
|
||||
{
|
||||
info = MapUrlPath(path, context);
|
||||
if (info != null)
|
||||
_ = _mappingCache.AddOrUpdate(path, info, (_, __) => info);
|
||||
}
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
// If mapping failed, send a "404 Not Found" response, or whatever OnMappingFailed chooses to do.
|
||||
// For example, it may return a default resource (think a folder of images and an imageNotFound.jpg),
|
||||
// or redirect the request.
|
||||
await OnMappingFailed(context, null).ConfigureAwait(false);
|
||||
}
|
||||
else if (!IsHttpMethodAllowed(context.Request, out var sendResponseBody))
|
||||
{
|
||||
// If there is a mapped resource, check that the HTTP method is either GET or HEAD.
|
||||
// Otherwise, send a "405 Method Not Allowed" response, or whatever OnMethodNotAllowed chooses to do.
|
||||
await OnMethodNotAllowed(context, info).ConfigureAwait(false);
|
||||
}
|
||||
else if (info.IsDirectory && DirectoryLister == null)
|
||||
{
|
||||
// If a directory listing was requested, but there is no DirectoryLister,
|
||||
// send a "403 Unauthorized" response, or whatever OnDirectoryNotListable chooses to do.
|
||||
// For example, one could prefer to send "404 Not Found" instead.
|
||||
await OnDirectoryNotListable(context, info).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleResource(context, info, sendResponseBody).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Tells whether a request's HTTP method is suitable for processing by FileModule
|
||||
// and, if so, whether a response body must be sent.
|
||||
private static bool IsHttpMethodAllowed(IHttpRequest request, out bool sendResponseBody)
|
||||
{
|
||||
switch (request.HttpVerb)
|
||||
{
|
||||
case HttpVerbs.Head:
|
||||
sendResponseBody = false;
|
||||
return true;
|
||||
case HttpVerbs.Get:
|
||||
sendResponseBody = true;
|
||||
return true;
|
||||
default:
|
||||
sendResponseBody = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepares response headers for a "200 OK" or "304 Not Modified" response.
|
||||
// RFC7232, Section 4.1
|
||||
private static void PreparePositiveResponse(IHttpResponse response, MappedResourceInfo info, string contentType, string entityTag, Action<IHttpResponse> setCompression)
|
||||
{
|
||||
setCompression(response);
|
||||
response.ContentType = contentType;
|
||||
response.Headers.Set(HttpHeaderNames.ETag, entityTag);
|
||||
response.Headers.Set(HttpHeaderNames.LastModified, HttpDate.Format(info.LastModifiedUtc));
|
||||
response.Headers.Set(HttpHeaderNames.CacheControl, "max-age=0, must-revalidate");
|
||||
response.Headers.Set(HttpHeaderNames.AcceptRanges, "bytes");
|
||||
}
|
||||
|
||||
// Attempts to map a module-relative URL path to a mapped resource,
|
||||
// handling DefaultDocument and DefaultExtension.
|
||||
// Returns null if not found.
|
||||
// Directories mus be returned regardless of directory listing being enabled.
|
||||
private MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
|
||||
{
|
||||
var result = Provider.MapUrlPath(urlPath, mimeTypeProvider);
|
||||
|
||||
// If urlPath maps to a file, no further searching is needed.
|
||||
if (result?.IsFile ?? false)
|
||||
return result;
|
||||
|
||||
// Look for a default document.
|
||||
// Don't append an additional slash if the URL path is "/".
|
||||
// The default document, if found, must be a file, not a directory.
|
||||
if (DefaultDocument != null)
|
||||
{
|
||||
var defaultDocumentPath = urlPath + (urlPath.Length > 1 ? "/" : string.Empty) + DefaultDocument;
|
||||
var defaultDocumentResult = Provider.MapUrlPath(defaultDocumentPath, mimeTypeProvider);
|
||||
if (defaultDocumentResult?.IsFile ?? false)
|
||||
return defaultDocumentResult;
|
||||
}
|
||||
|
||||
// Try to apply default extension (but not if the URL path is "/",
|
||||
// i.e. the only normalized, non-base URL path that ends in a slash).
|
||||
// When the default extension is applied, the result must be a file.
|
||||
if (DefaultExtension != null && urlPath.Length > 1)
|
||||
{
|
||||
var defaultExtensionResult = Provider.MapUrlPath(urlPath + DefaultExtension, mimeTypeProvider);
|
||||
if (defaultExtensionResult?.IsFile ?? false)
|
||||
return defaultExtensionResult;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task HandleResource(IHttpContext context, MappedResourceInfo info, bool sendResponseBody)
|
||||
{
|
||||
// Try to extract resource information from cache.
|
||||
var cachingThreshold = 1024L * Cache.MaxFileSizeKb;
|
||||
if (!_cacheSection!.TryGet(info.Path, out var cacheItem))
|
||||
{
|
||||
// Resource information not yet cached
|
||||
cacheItem = new FileCacheItem(_cacheSection, info.LastModifiedUtc, info.Length);
|
||||
_cacheSection.Add(info.Path, cacheItem);
|
||||
}
|
||||
else if (!Provider.IsImmutable)
|
||||
{
|
||||
// Check whether the resource has changed.
|
||||
// If so, discard the cache item and create a new one.
|
||||
if (cacheItem.LastModifiedUtc != info.LastModifiedUtc || cacheItem.Length != info.Length)
|
||||
{
|
||||
_cacheSection.Remove(info.Path);
|
||||
cacheItem = new FileCacheItem(_cacheSection, info.LastModifiedUtc, info.Length);
|
||||
_cacheSection.Add(info.Path, cacheItem);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Now we have a cacheItem for the resource.
|
||||
* It may have been just created, or it may or may not have a cached content,
|
||||
* depending upon the value of the ContentCaching property,
|
||||
* the size of the resource, and the value of the
|
||||
* MaxFileSizeKb of our Cache.
|
||||
*/
|
||||
|
||||
// If the content type is not a valid MIME type, assume the default.
|
||||
var contentType = info.ContentType ?? DirectoryLister?.ContentType ?? MimeType.Default;
|
||||
var mimeType = MimeType.StripParameters(contentType);
|
||||
if (!MimeType.IsMimeType(mimeType, false))
|
||||
contentType = mimeType = MimeType.Default;
|
||||
|
||||
// Next we're going to apply proactive negotiation
|
||||
// to determine whether we agree with the client upon the compression
|
||||
// (or lack of it) to use for the resource.
|
||||
//
|
||||
// The combination of partial responses and entity compression
|
||||
// is not really standardized and could lead to a world of pain.
|
||||
// Thus, if there is a Range header in the request, try to negotiate for no compression.
|
||||
// Later, if there is compression anyway, we will ignore the Range header.
|
||||
if (!context.TryDetermineCompression(mimeType, out var preferCompression))
|
||||
preferCompression = true;
|
||||
preferCompression &= context.Request.Headers.Get(HttpHeaderNames.Range) == null;
|
||||
if (!context.Request.TryNegotiateContentEncoding(preferCompression, out var compressionMethod, out var setCompressionInResponse))
|
||||
{
|
||||
// If negotiation failed, the returned callback will do the right thing.
|
||||
setCompressionInResponse(context.Response);
|
||||
return;
|
||||
}
|
||||
|
||||
var entityTag = info.GetEntityTag(compressionMethod);
|
||||
|
||||
// Send a "304 Not Modified" response if applicable.
|
||||
//
|
||||
// RFC7232, Section 3.3: "A recipient MUST ignore If-Modified-Since
|
||||
// if the request contains an If-None-Match header field."
|
||||
if (context.Request.CheckIfNoneMatch(entityTag, out var ifNoneMatchExists)
|
||||
|| (!ifNoneMatchExists && context.Request.CheckIfModifiedSince(info.LastModifiedUtc, out _)))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.NotModified;
|
||||
PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* At this point we know the response is "200 OK",
|
||||
* unless the request is a range request.
|
||||
*
|
||||
* RFC7233, Section 3.1: "The Range header field is evaluated after evaluating the precondition
|
||||
* header fields defined in RFC7232, and only if the result in absence
|
||||
* of the Range header field would be a 200 (OK) response. In other
|
||||
* words, Range is ignored when a conditional GET would result in a 304
|
||||
* (Not Modified) response."
|
||||
*/
|
||||
|
||||
// Before evaluating ranges, we must know the content length.
|
||||
// This is easy for files, as it is stored in info.Length.
|
||||
// Directories always have info.Length == 0; therefore,
|
||||
// unless the directory listing is cached, we must generate it now
|
||||
// (and cache it while we're there, if applicable).
|
||||
var content = cacheItem.GetContent(compressionMethod);
|
||||
if (info.IsDirectory && content == null)
|
||||
{
|
||||
long uncompressedLength;
|
||||
(content, uncompressedLength) = await GenerateDirectoryListingAsync(context, info, compressionMethod)
|
||||
.ConfigureAwait(false);
|
||||
if (ContentCaching && uncompressedLength <= cachingThreshold)
|
||||
_ = cacheItem.SetContent(compressionMethod, content);
|
||||
}
|
||||
|
||||
var contentLength = content?.Length ?? info.Length;
|
||||
|
||||
// Ignore range request is compression is enabled
|
||||
// (or should I say forced, since negotiation has tried not to use it).
|
||||
var partialStart = 0L;
|
||||
var partialUpperBound = contentLength - 1;
|
||||
var isPartial = compressionMethod == CompressionMethod.None
|
||||
&& context.Request.IsRangeRequest(contentLength, entityTag, info.LastModifiedUtc, out partialStart, out partialUpperBound);
|
||||
var responseContentLength = contentLength;
|
||||
|
||||
if (isPartial)
|
||||
{
|
||||
// Prepare a "206 Partial Content" response.
|
||||
responseContentLength = partialUpperBound - partialStart + 1;
|
||||
context.Response.StatusCode = (int)HttpStatusCode.PartialContent;
|
||||
PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse);
|
||||
context.Response.Headers.Set(HttpHeaderNames.ContentRange, $"bytes {partialStart}-{partialUpperBound}/{contentLength}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prepare a "200 OK" response.
|
||||
PreparePositiveResponse(context.Response, info, contentType, entityTag, setCompressionInResponse);
|
||||
}
|
||||
|
||||
// If it's a HEAD request, we're done.
|
||||
if (!sendResponseBody)
|
||||
return;
|
||||
|
||||
// If content must be sent AND cached, first read it and store it.
|
||||
// If the requested resource is a directory, we have already listed it by now,
|
||||
// so it must be a file for content to be null.
|
||||
if (content == null && ContentCaching && contentLength <= cachingThreshold)
|
||||
{
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
using (var compressor = new CompressionStream(memoryStream, compressionMethod))
|
||||
{
|
||||
using var source = Provider.OpenFile(info.Path);
|
||||
await source.CopyToAsync(compressor, WebServer.StreamCopyBufferSize, context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
content = memoryStream.ToArray();
|
||||
responseContentLength = content.Length;
|
||||
}
|
||||
|
||||
_ = cacheItem.SetContent(compressionMethod, content);
|
||||
}
|
||||
|
||||
// Transfer cached content if present.
|
||||
if (content != null)
|
||||
{
|
||||
context.Response.ContentLength64 = responseContentLength;
|
||||
var offset = isPartial ? (int) partialStart : 0;
|
||||
await context.Response.OutputStream.WriteAsync(content, offset, (int)responseContentLength, context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Read and transfer content without caching.
|
||||
using (var source = Provider.OpenFile(info.Path))
|
||||
{
|
||||
context.Response.SendChunked = true;
|
||||
|
||||
if (isPartial)
|
||||
{
|
||||
var buffer = new byte[WebServer.StreamCopyBufferSize];
|
||||
if (source.CanSeek)
|
||||
{
|
||||
source.Position = partialStart;
|
||||
}
|
||||
else
|
||||
{
|
||||
var skipLength = (int)partialStart;
|
||||
while (skipLength > 0)
|
||||
{
|
||||
var read = await source.ReadAsync(buffer, 0, Math.Min(skipLength, buffer.Length), context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
skipLength -= read;
|
||||
}
|
||||
}
|
||||
|
||||
var transferSize = responseContentLength;
|
||||
while (transferSize >= WebServer.StreamCopyBufferSize)
|
||||
{
|
||||
var read = await source.ReadAsync(buffer, 0, WebServer.StreamCopyBufferSize, context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await context.Response.OutputStream.WriteAsync(buffer, 0, read, context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
transferSize -= read;
|
||||
}
|
||||
|
||||
if (transferSize > 0)
|
||||
{
|
||||
var read = await source.ReadAsync(buffer, 0, (int)transferSize, context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await context.Response.OutputStream.WriteAsync(buffer, 0, read, context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var compressor = new CompressionStream(context.Response.OutputStream, compressionMethod);
|
||||
await source.CopyToAsync(compressor, WebServer.StreamCopyBufferSize, context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uses DirectoryLister to generate a directory listing asynchronously.
|
||||
// Returns a tuple of the generated content and its *uncompressed* length
|
||||
// (useful to decide whether it can be cached).
|
||||
private async Task<(byte[], long)> GenerateDirectoryListingAsync(
|
||||
IHttpContext context,
|
||||
MappedResourceInfo info,
|
||||
CompressionMethod compressionMethod)
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var stream = new CompressionStream(memoryStream, compressionMethod);
|
||||
|
||||
await DirectoryLister!.ListDirectoryAsync(
|
||||
info,
|
||||
context.Request.Url.AbsolutePath,
|
||||
Provider.GetDirectoryEntries(info.Path, context),
|
||||
stream,
|
||||
context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (memoryStream.ToArray(), stream.UncompressedLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
282
Vendor/EmbedIO-3.5.2/Files/FileModuleExtensions.cs
vendored
Normal file
282
Vendor/EmbedIO-3.5.2/Files/FileModuleExtensions.cs
vendored
Normal file
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="FileModule"/> and derived classes.
|
||||
/// </summary>
|
||||
public static class FileModuleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the <see cref="FileCache"/> used by a module to store hashes and,
|
||||
/// optionally, file contents and rendered directory listings.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <param name="value">An instance of <see cref="FileCache"/>.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.Cache">Cache</see> property
|
||||
/// set to <paramref name="value"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
|
||||
/// <seealso cref="FileModule.Cache"/>
|
||||
public static TModule WithCache<TModule>(this TModule @this, FileCache value)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.Cache = value;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a value indicating whether a module caches the contents of files
|
||||
/// and directory listings.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <param name="value"><see langword="true"/> to enable caching of contents;
|
||||
/// <see langword="false"/> to disable it.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.ContentCaching">ContentCaching</see> property
|
||||
/// set to <paramref name="value"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <seealso cref="FileModule.ContentCaching"/>
|
||||
public static TModule WithContentCaching<TModule>(this TModule @this, bool value)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.ContentCaching = value;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables caching of file contents and directory listings on a module.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.ContentCaching">ContentCaching</see> property
|
||||
/// set to <see langword="true"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <seealso cref="FileModule.ContentCaching"/>
|
||||
public static TModule WithContentCaching<TModule>(this TModule @this)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.ContentCaching = true;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables caching of file contents and directory listings on a module.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <param name="maxFileSizeKb"><see langword="true"/> sets the maximum size of a single cached file in kilobytes</param>
|
||||
/// <param name="maxSizeKb"><see langword="true"/> sets the maximum total size of cached data in kilobytes</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.ContentCaching">ContentCaching</see> property
|
||||
/// set to <see langword="true"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <seealso cref="FileModule.ContentCaching"/>
|
||||
public static TModule WithContentCaching<TModule>(this TModule @this, int maxFileSizeKb, int maxSizeKb)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.ContentCaching = true;
|
||||
@this.Cache.MaxFileSizeKb = maxFileSizeKb;
|
||||
@this.Cache.MaxSizeKb = maxSizeKb;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables caching of file contents and directory listings on a module.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.ContentCaching">ContentCaching</see> property
|
||||
/// set to <see langword="false"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <seealso cref="FileModule.ContentCaching"/>
|
||||
public static TModule WithoutContentCaching<TModule>(this TModule @this)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.ContentCaching = false;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the name of the default document served, if it exists, instead of a directory listing
|
||||
/// when the path of a requested URL maps to a directory.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <param name="value">The name of the default document.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.DefaultDocument">DefaultDocument</see> property
|
||||
/// set to <paramref name="value"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <seealso cref="FileModule.DefaultDocument"/>
|
||||
public static TModule WithDefaultDocument<TModule>(this TModule @this, string value)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.DefaultDocument = value;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the name of the default document to <see langword="null"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.DefaultDocument">DefaultDocument</see> property
|
||||
/// set to <see langword="null"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <seealso cref="FileModule.DefaultDocument"/>
|
||||
public static TModule WithoutDefaultDocument<TModule>(this TModule @this)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.DefaultDocument = null;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default extension appended to requested URL paths that do not map
|
||||
/// to any file or directory.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <param name="value">The default extension.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.DefaultExtension">DefaultExtension</see> property
|
||||
/// set to <paramref name="value"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="value"/> is a non-<see langword="null"/>,
|
||||
/// non-empty string that does not start with a period (<c>.</c>).</exception>
|
||||
/// <seealso cref="FileModule.DefaultExtension"/>
|
||||
public static TModule WithDefaultExtension<TModule>(this TModule @this, string value)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.DefaultExtension = value;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default extension to <see langword="null"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.DefaultExtension">DefaultExtension</see> property
|
||||
/// set to <see langword="null"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <seealso cref="FileModule.DefaultExtension"/>
|
||||
public static TModule WithoutDefaultExtension<TModule>(this TModule @this)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.DefaultExtension = null;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="IDirectoryLister"/> interface used to generate
|
||||
/// directory listing in a module.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <param name="value">An <see cref="IDirectoryLister"/> interface, or <see langword="null"/>
|
||||
/// to disable the generation of directory listings.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.DirectoryLister">DirectoryLister</see> property
|
||||
/// set to <paramref name="value"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <seealso cref="FileModule.DirectoryLister"/>
|
||||
public static TModule WithDirectoryLister<TModule>(this TModule @this, IDirectoryLister value)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.DirectoryLister = value;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a module's <see cref="FileModule.DirectoryLister">DirectoryLister</see> property
|
||||
/// to <see langword="null"/>, disabling the generation of directory listings.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.DirectoryLister">DirectoryLister</see> property
|
||||
/// set to <see langword="null"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <seealso cref="FileModule.DirectoryLister"/>
|
||||
public static TModule WithoutDirectoryLister<TModule>(this TModule @this)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.DirectoryLister = null;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a <see cref="FileRequestHandlerCallback"/> that is called by a module whenever
|
||||
/// the requested URL path could not be mapped to any file or directory.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <param name="callback">The method to call.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.OnMappingFailed">OnMappingFailed</see> property
|
||||
/// set to <paramref name="callback"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is <see langword="null"/>.</exception>
|
||||
/// <seealso cref="FileModule.OnMappingFailed"/>
|
||||
/// <seealso cref="FileRequestHandler"/>
|
||||
public static TModule HandleMappingFailed<TModule>(this TModule @this, FileRequestHandlerCallback callback)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.OnMappingFailed = callback;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a <see cref="FileRequestHandlerCallback"/> that is called by a module whenever
|
||||
/// the requested URL path has been mapped to a directory, but directory listing has been
|
||||
/// disabled.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <param name="callback">The method to call.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.OnDirectoryNotListable">OnDirectoryNotListable</see> property
|
||||
/// set to <paramref name="callback"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is <see langword="null"/>.</exception>
|
||||
/// <seealso cref="FileModule.OnDirectoryNotListable"/>
|
||||
/// <seealso cref="FileRequestHandler"/>
|
||||
public static TModule HandleDirectoryNotListable<TModule>(this TModule @this, FileRequestHandlerCallback callback)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.OnDirectoryNotListable = callback;
|
||||
return @this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a <see cref="FileRequestHandlerCallback"/> that is called by a module whenever
|
||||
/// the requested URL path has been mapped to a file or directory, but the request's
|
||||
/// HTTP method is neither <c>GET</c> nor <c>HEAD</c>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModule">The type of the module on which this method is called.</typeparam>
|
||||
/// <param name="this">The module on which this method is called.</param>
|
||||
/// <param name="callback">The method to call.</param>
|
||||
/// <returns><paramref name="this"/> with its <see cref="FileModule.OnMethodNotAllowed">OnMethodNotAllowed</see> property
|
||||
/// set to <paramref name="callback"/>.</returns>
|
||||
/// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">The configuration of <paramref name="this"/> is locked.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is <see langword="null"/>.</exception>
|
||||
/// <seealso cref="FileModule.OnMethodNotAllowed"/>
|
||||
/// <seealso cref="FileRequestHandler"/>
|
||||
public static TModule HandleMethodNotAllowed<TModule>(this TModule @this, FileRequestHandlerCallback callback)
|
||||
where TModule : FileModule
|
||||
{
|
||||
@this.OnMethodNotAllowed = callback;
|
||||
return @this;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Vendor/EmbedIO-3.5.2/Files/FileRequestHandler.cs
vendored
Normal file
53
Vendor/EmbedIO-3.5.2/Files/FileRequestHandler.cs
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides standard handler callbacks for <see cref="FileModule"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="FileRequestHandlerCallback"/>
|
||||
public static class FileRequestHandler
|
||||
{
|
||||
#pragma warning disable CA1801 // Unused parameters - Must respect FileRequestHandlerCallback signature.
|
||||
/// <summary>
|
||||
/// <para>Unconditionally passes a request down the module chain.</para>
|
||||
/// </summary>
|
||||
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
|
||||
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
|
||||
/// otherwise, <see langword="null"/>.</param>
|
||||
/// <returns>This method never returns; it throws an exception instead.</returns>
|
||||
public static Task PassThrough(IHttpContext context, MappedResourceInfo? info)
|
||||
=> throw RequestHandler.PassThrough();
|
||||
|
||||
/// <summary>
|
||||
/// <para>Unconditionally sends a <c>403 Unauthorized</c> response.</para>
|
||||
/// </summary>
|
||||
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
|
||||
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
|
||||
/// otherwise, <see langword="null"/>.</param>
|
||||
/// <returns>This method never returns; it throws a <see cref="HttpException"/> instead.</returns>
|
||||
public static Task ThrowUnauthorized(IHttpContext context, MappedResourceInfo? info)
|
||||
=> throw HttpException.Unauthorized();
|
||||
|
||||
/// <summary>
|
||||
/// <para>Unconditionally sends a <c>404 Not Found</c> response.</para>
|
||||
/// </summary>
|
||||
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
|
||||
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
|
||||
/// otherwise, <see langword="null"/>.</param>
|
||||
/// <returns>This method never returns; it throws a <see cref="HttpException"/> instead.</returns>
|
||||
public static Task ThrowNotFound(IHttpContext context, MappedResourceInfo? info)
|
||||
=> throw HttpException.NotFound();
|
||||
|
||||
/// <summary>
|
||||
/// <para>Unconditionally sends a <c>405 Method Not Allowed</c> response.</para>
|
||||
/// </summary>
|
||||
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
|
||||
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
|
||||
/// otherwise, <see langword="null"/>.</param>
|
||||
/// <returns>This method never returns; it throws a <see cref="HttpException"/> instead.</returns>
|
||||
public static Task ThrowMethodNotAllowed(IHttpContext context, MappedResourceInfo? info)
|
||||
=> throw HttpException.MethodNotAllowed();
|
||||
#pragma warning restore CA1801
|
||||
}
|
||||
}
|
||||
13
Vendor/EmbedIO-3.5.2/Files/FileRequestHandlerCallback.cs
vendored
Normal file
13
Vendor/EmbedIO-3.5.2/Files/FileRequestHandlerCallback.cs
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// A callback used to handle a request in <see cref="FileModule"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
|
||||
/// <param name="info">If the requested path has been successfully mapped to a resource (file or directory), the result of the mapping;
|
||||
/// otherwise, <see langword="null"/>.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
public delegate Task FileRequestHandlerCallback(IHttpContext context, MappedResourceInfo? info);
|
||||
}
|
||||
202
Vendor/EmbedIO-3.5.2/Files/FileSystemProvider.cs
vendored
Normal file
202
Vendor/EmbedIO-3.5.2/Files/FileSystemProvider.cs
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
35
Vendor/EmbedIO-3.5.2/Files/IDirectoryLister.cs
vendored
Normal file
35
Vendor/EmbedIO-3.5.2/Files/IDirectoryLister.cs
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an object that can render a directory listing to a stream.
|
||||
/// </summary>
|
||||
public interface IDirectoryLister
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the MIME type of generated directory listings.
|
||||
/// </summary>
|
||||
string ContentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously generate a directory listing.
|
||||
/// </summary>
|
||||
/// <param name="info">A <see cref="MappedResourceInfo"/> containing information about
|
||||
/// the directory which is to be listed.</param>
|
||||
/// <param name="absoluteUrlPath">The absolute URL path that was mapped to <paramref name="info"/>.</param>
|
||||
/// <param name="entries">An enumeration of the entries in the directory represented by <paramref name="info"/>.</param>
|
||||
/// <param name="stream">A <see cref="Stream"/> to which the directory listing must be written.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
Task ListDirectoryAsync(
|
||||
MappedResourceInfo info,
|
||||
string absoluteUrlPath,
|
||||
IEnumerable<MappedResourceInfo> entries,
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
61
Vendor/EmbedIO-3.5.2/Files/IFileProvider.cs
vendored
Normal file
61
Vendor/EmbedIO-3.5.2/Files/IFileProvider.cs
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an object that can provide files and/or directories to be served by a <see cref="FileModule"/>.
|
||||
/// </summary>
|
||||
public interface IFileProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Occurs when a file or directory provided by this instance is modified or removed.</para>
|
||||
/// <para>The event's parameter is the provider-specific path of the resource that changed.</para>
|
||||
/// </summary>
|
||||
event Action<string> ResourceChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the files and directories provided by this instance
|
||||
/// will never change.
|
||||
/// </summary>
|
||||
bool IsImmutable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Signals a file provider that the web server is starting.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to stop the web server.</param>
|
||||
void Start(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a URL path to a provider-specific path.
|
||||
/// </summary>
|
||||
/// <param name="urlPath">The URL path.</param>
|
||||
/// <param name="mimeTypeProvider">An <see cref="IMimeTypeProvider"/> interface to use
|
||||
/// for determining the MIME type of a file.</param>
|
||||
/// <returns>A provider-specific path identifying a file or directory,
|
||||
/// or <see langword="null"/> if this instance cannot provide a resource associated
|
||||
/// to <paramref name="urlPath"/>.</returns>
|
||||
MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a file for reading.
|
||||
/// </summary>
|
||||
/// <param name="path">The provider-specific path for the file.</param>
|
||||
/// <returns>
|
||||
/// <para>A readable <see cref="Stream"/> of the file's contents.</para>
|
||||
/// </returns>
|
||||
Stream OpenFile(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumeration of the entries of a directory.
|
||||
/// </summary>
|
||||
/// <param name="path">The provider-specific path for the directory.</param>
|
||||
/// <param name="mimeTypeProvider">An <see cref="IMimeTypeProvider"/> interface to use
|
||||
/// for determining the MIME type of files.</param>
|
||||
/// <returns>An enumeration of <see cref="MappedResourceInfo"/> objects identifying the entries
|
||||
/// in the directory identified by <paramref name="path"/>.</returns>
|
||||
IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider);
|
||||
}
|
||||
}
|
||||
12
Vendor/EmbedIO-3.5.2/Files/Internal/Base64Utility.cs
vendored
Normal file
12
Vendor/EmbedIO-3.5.2/Files/Internal/Base64Utility.cs
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace EmbedIO.Files.Internal
|
||||
{
|
||||
internal static class Base64Utility
|
||||
{
|
||||
// long is 8 bytes
|
||||
// base64 of 8 bytes is 12 chars, but the last one is padding
|
||||
public static string LongToBase64(long value)
|
||||
=> Convert.ToBase64String(BitConverter.GetBytes(value)).Substring(0, 11);
|
||||
}
|
||||
}
|
||||
28
Vendor/EmbedIO-3.5.2/Files/Internal/EntityTag.cs
vendored
Normal file
28
Vendor/EmbedIO-3.5.2/Files/Internal/EntityTag.cs
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace EmbedIO.Files.Internal
|
||||
{
|
||||
internal static class EntityTag
|
||||
{
|
||||
public static string Compute(DateTime lastModifiedUtc, long length, CompressionMethod compressionMethod)
|
||||
{
|
||||
var sb = new StringBuilder()
|
||||
.Append('"')
|
||||
.Append(Base64Utility.LongToBase64(lastModifiedUtc.Ticks))
|
||||
.Append(Base64Utility.LongToBase64(length));
|
||||
|
||||
switch (compressionMethod)
|
||||
{
|
||||
case CompressionMethod.Deflate:
|
||||
sb.Append('-').Append(CompressionMethodNames.Deflate);
|
||||
break;
|
||||
case CompressionMethod.Gzip:
|
||||
sb.Append('-').Append(CompressionMethodNames.Gzip);
|
||||
break;
|
||||
}
|
||||
|
||||
return sb.Append('"').ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
164
Vendor/EmbedIO-3.5.2/Files/Internal/FileCacheItem.cs
vendored
Normal file
164
Vendor/EmbedIO-3.5.2/Files/Internal/FileCacheItem.cs
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using EmbedIO.Internal;
|
||||
|
||||
namespace EmbedIO.Files.Internal
|
||||
{
|
||||
internal sealed class FileCacheItem
|
||||
{
|
||||
#pragma warning disable SA1401 // Field should be private - performance is a stronger concern here.
|
||||
// These fields create a sort of linked list of items
|
||||
// inside the cache's dictionary.
|
||||
// Their purpose is to keep track of items
|
||||
// in order from least to most recently used.
|
||||
internal string? PreviousKey;
|
||||
internal string? NextKey;
|
||||
internal long LastUsedAt;
|
||||
#pragma warning restore SA1401
|
||||
|
||||
// Size of a pointer in bytes
|
||||
private static readonly long SizeOfPointer = Environment.Is64BitProcess ? 8 : 4;
|
||||
|
||||
// Size of a WeakReference<T> in bytes
|
||||
private static readonly long SizeOfWeakReference = Environment.Is64BitProcess ? 16 : 32;
|
||||
|
||||
// Educated guess about the size of an Item in memory (see comments on constructor).
|
||||
// 3 * SizeOfPointer + total size of fields, rounded up to a multiple of 16.
|
||||
//
|
||||
// Computed as follows:
|
||||
//
|
||||
// * for 32-bit:
|
||||
// - initialize count to 3 (number of "hidden" pointers that compose the object header)
|
||||
// - for every field / auto property, in order of declaration:
|
||||
// - increment count by 1 for reference types, 2 for long and DateTime
|
||||
// (as of time of writing there are no fields of other types here)
|
||||
// - increment again by 1 if this field "weighs" 1 and the next one "weighs" 2
|
||||
// (padding for field alignment)
|
||||
// - multiply count by 4 (size of a pointer)
|
||||
// - if the result is not a multiple of 16, round it up to next multiple of 16
|
||||
//
|
||||
// * for 64-bit:
|
||||
// - initialize count to 3 (number of "hidden" pointers that compose the object header)
|
||||
// - for every field / auto property, in order of declaration, increment count by 1
|
||||
// (at the time of writing there are no fields here that need padding on 64-bit)
|
||||
// - multiply count by 8 (size of a pointer)
|
||||
// - if the result is not a multiple of 16, round it up to next multiple of 16
|
||||
private static readonly long SizeOfItem = Environment.Is64BitProcess ? 96 : 128;
|
||||
|
||||
private readonly object _syncRoot = new object();
|
||||
|
||||
// Used to update total size of section.
|
||||
// Weak reference avoids circularity.
|
||||
private readonly WeakReference<FileCache.Section> _section;
|
||||
|
||||
// There are only 3 possible compression methods,
|
||||
// hence a dictionary (or two dictionaries) would be overkill.
|
||||
private byte[]? _uncompressedContent;
|
||||
private byte[]? _gzippedContent;
|
||||
private byte[]? _deflatedContent;
|
||||
|
||||
internal FileCacheItem(FileCache.Section section, DateTime lastModifiedUtc, long length)
|
||||
{
|
||||
_section = new WeakReference<FileCache.Section>(section);
|
||||
|
||||
LastModifiedUtc = lastModifiedUtc;
|
||||
Length = length;
|
||||
|
||||
// There is no way to know the actual size of an object at runtime.
|
||||
// This method makes some educated guesses, based on the following
|
||||
// article (among others):
|
||||
// https://codingsight.com/precise-computation-of-clr-object-size/
|
||||
// PreviousKey and NextKey values aren't counted in
|
||||
// because they are just references to existing strings.
|
||||
SizeInCache = SizeOfItem + SizeOfWeakReference;
|
||||
}
|
||||
|
||||
public DateTime LastModifiedUtc { get; }
|
||||
|
||||
public long Length { get; }
|
||||
|
||||
// This is the (approximate) in-memory size of this object.
|
||||
// It is NOT the length of the cache resource!
|
||||
public long SizeInCache { get; private set; }
|
||||
|
||||
public byte[]? GetContent(CompressionMethod compressionMethod)
|
||||
{
|
||||
// If there are both entity tag and content, use them.
|
||||
switch (compressionMethod)
|
||||
{
|
||||
case CompressionMethod.Deflate:
|
||||
if (_deflatedContent != null) return _deflatedContent;
|
||||
break;
|
||||
case CompressionMethod.Gzip:
|
||||
if (_gzippedContent != null) return _gzippedContent;
|
||||
break;
|
||||
default:
|
||||
if (_uncompressedContent != null) return _uncompressedContent;
|
||||
break;
|
||||
}
|
||||
|
||||
// Try to convert existing content, if any.
|
||||
byte[]? content;
|
||||
if (_uncompressedContent != null)
|
||||
{
|
||||
content = CompressionUtility.ConvertCompression(_uncompressedContent, CompressionMethod.None, compressionMethod);
|
||||
}
|
||||
else if (_gzippedContent != null)
|
||||
{
|
||||
content = CompressionUtility.ConvertCompression(_gzippedContent, CompressionMethod.Gzip, compressionMethod);
|
||||
}
|
||||
else if (_deflatedContent != null)
|
||||
{
|
||||
content = CompressionUtility.ConvertCompression(_deflatedContent, CompressionMethod.Deflate, compressionMethod);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No content whatsoever.
|
||||
return null;
|
||||
}
|
||||
|
||||
return SetContent(compressionMethod, content);
|
||||
}
|
||||
|
||||
public byte[]? SetContent(CompressionMethod compressionMethod, byte[]? content)
|
||||
{
|
||||
// This is the bare minimum locking we need
|
||||
// to ensure we don't mess sizes up.
|
||||
byte[]? oldContent;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
switch (compressionMethod)
|
||||
{
|
||||
case CompressionMethod.Deflate:
|
||||
oldContent = _deflatedContent;
|
||||
_deflatedContent = content;
|
||||
break;
|
||||
case CompressionMethod.Gzip:
|
||||
oldContent = _gzippedContent;
|
||||
_gzippedContent = content;
|
||||
break;
|
||||
default:
|
||||
oldContent = _uncompressedContent;
|
||||
_uncompressedContent = content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var sizeDelta = GetSizeOf(content) - GetSizeOf(oldContent);
|
||||
SizeInCache += sizeDelta;
|
||||
if (_section.TryGetTarget(out var section))
|
||||
section.UpdateTotalSize(sizeDelta);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Round up to a multiple of 16
|
||||
private static long RoundUpTo16(long n)
|
||||
{
|
||||
var remainder = n % 16;
|
||||
return remainder > 0 ? n + (16 - remainder) : n;
|
||||
}
|
||||
|
||||
// The size of a byte array is 3 * SizeOfPointer + 1 (size of byte) * Length
|
||||
private static long GetSizeOf(byte[]? arr) => arr == null ? 0 : RoundUpTo16(3 * SizeOfPointer) + arr.Length;
|
||||
}
|
||||
}
|
||||
73
Vendor/EmbedIO-3.5.2/Files/Internal/HtmlDirectoryLister.cs
vendored
Normal file
73
Vendor/EmbedIO-3.5.2/Files/Internal/HtmlDirectoryLister.cs
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EmbedIO.Utilities;
|
||||
using Swan;
|
||||
|
||||
namespace EmbedIO.Files.Internal
|
||||
{
|
||||
internal class HtmlDirectoryLister : IDirectoryLister
|
||||
{
|
||||
private static readonly Lazy<IDirectoryLister> LazyInstance = new Lazy<IDirectoryLister>(() => new HtmlDirectoryLister());
|
||||
|
||||
private HtmlDirectoryLister()
|
||||
{
|
||||
}
|
||||
|
||||
public static IDirectoryLister Instance => LazyInstance.Value;
|
||||
|
||||
public string ContentType { get; } = MimeType.Html + "; encoding=" + WebServer.DefaultEncoding.WebName;
|
||||
|
||||
public async Task ListDirectoryAsync(
|
||||
MappedResourceInfo info,
|
||||
string absoluteUrlPath,
|
||||
IEnumerable<MappedResourceInfo> entries,
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int MaxEntryLength = 50;
|
||||
const int SizeIndent = -20; // Negative for right alignment
|
||||
|
||||
if (!info.IsDirectory)
|
||||
throw SelfCheck.Failure($"{nameof(HtmlDirectoryLister)}.{nameof(ListDirectoryAsync)} invoked with a file, not a directory.");
|
||||
|
||||
var encodedPath = WebUtility.HtmlEncode(absoluteUrlPath);
|
||||
using var text = new StreamWriter(stream, WebServer.DefaultEncoding);
|
||||
text.Write("<html><head><title>Index of ");
|
||||
text.Write(encodedPath);
|
||||
text.Write("</title></head><body><h1>Index of ");
|
||||
text.Write(encodedPath);
|
||||
text.Write("</h1><hr/><pre>");
|
||||
|
||||
if (encodedPath.Length > 1)
|
||||
text.Write("<a href='../'>../</a>\n");
|
||||
|
||||
entries = entries.ToArray();
|
||||
|
||||
foreach (var directory in entries.Where(m => m.IsDirectory).OrderBy(e => e.Name))
|
||||
{
|
||||
text.Write($"<a href=\"{Uri.EscapeDataString(directory.Name)}\">{WebUtility.HtmlEncode(directory.Name)}</a>");
|
||||
text.Write(new string(' ', Math.Max(1, MaxEntryLength - directory.Name.Length + 1)));
|
||||
text.Write(HttpDate.Format(directory.LastModifiedUtc));
|
||||
text.Write('\n');
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
foreach (var file in entries.Where(m => m.IsFile).OrderBy(e => e.Name))
|
||||
{
|
||||
text.Write($"<a href=\"{Uri.EscapeDataString(file.Name)}\">{WebUtility.HtmlEncode(file.Name)}</a>");
|
||||
text.Write(new string(' ', Math.Max(1, MaxEntryLength - file.Name.Length + 1)));
|
||||
text.Write(HttpDate.Format(file.LastModifiedUtc));
|
||||
text.Write($" {file.Length.ToString("#,###", CultureInfo.InvariantCulture),SizeIndent}\n");
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
text.Write("</pre><hr/></body></html>");
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Vendor/EmbedIO-3.5.2/Files/Internal/MappedResourceInfoExtensions.cs
vendored
Normal file
8
Vendor/EmbedIO-3.5.2/Files/Internal/MappedResourceInfoExtensions.cs
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace EmbedIO.Files.Internal
|
||||
{
|
||||
internal static class MappedResourceInfoExtensions
|
||||
{
|
||||
public static string GetEntityTag(this MappedResourceInfo @this, CompressionMethod compressionMethod)
|
||||
=> EntityTag.Compute(@this.LastModifiedUtc, @this.Length, compressionMethod);
|
||||
}
|
||||
}
|
||||
80
Vendor/EmbedIO-3.5.2/Files/MappedResourceInfo.cs
vendored
Normal file
80
Vendor/EmbedIO-3.5.2/Files/MappedResourceInfo.cs
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains information about a resource served via an <see cref="IFileProvider"/>.
|
||||
/// </summary>
|
||||
public sealed class MappedResourceInfo
|
||||
{
|
||||
private MappedResourceInfo(string path, string name, DateTime lastModifiedUtc, long length, string? contentType)
|
||||
{
|
||||
Path = path;
|
||||
Name = name;
|
||||
LastModifiedUtc = lastModifiedUtc;
|
||||
Length = length;
|
||||
ContentType = contentType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance represents a directory.
|
||||
/// </summary>
|
||||
public bool IsDirectory => ContentType == null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance represents a file.
|
||||
/// </summary>
|
||||
public bool IsFile => ContentType != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a unique, provider-specific path for the resource.
|
||||
/// </summary>
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the resource, as it would appear in a directory listing.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UTC date and time of the last modification made to the resource.
|
||||
/// </summary>
|
||||
public DateTime LastModifiedUtc { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <para>If <see cref="IsDirectory"/> is <see langword="false"/>, gets the length of the file, expressed in bytes.</para>
|
||||
/// <para>If <see cref="IsDirectory"/> is <see langword="true"/>, this property is always zero.</para>
|
||||
/// </summary>
|
||||
public long Length { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <para>If <see cref="IsDirectory"/> is <see langword="false"/>, gets a MIME type describing the kind of contents of the file.</para>
|
||||
/// <para>If <see cref="IsDirectory"/> is <see langword="true"/>, this property is always <see langword="null"/>.</para>
|
||||
/// </summary>
|
||||
public string? ContentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates and returns a new instance of the <see cref="MappedResourceInfo"/> class,
|
||||
/// representing a file.
|
||||
/// </summary>
|
||||
/// <param name="path">A unique, provider-specific path for the file.</param>
|
||||
/// <param name="name">The name of the file, as it would appear in a directory listing.</param>
|
||||
/// <param name="lastModifiedUtc">The UTC date and time of the last modification made to the file.</param>
|
||||
/// <param name="size">The length of the file, expressed in bytes.</param>
|
||||
/// <param name="contentType">A MIME type describing the kind of contents of the file.</param>
|
||||
/// <returns>A newly-constructed instance of <see cref="MappedResourceInfo"/>.</returns>
|
||||
public static MappedResourceInfo ForFile(string path, string name, DateTime lastModifiedUtc, long size, string contentType)
|
||||
=> new MappedResourceInfo(path, name, lastModifiedUtc, size, contentType ?? MimeType.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates and returns a new instance of the <see cref="MappedResourceInfo"/> class,
|
||||
/// representing a directory.
|
||||
/// </summary>
|
||||
/// <param name="path">A unique, provider-specific path for the directory.</param>
|
||||
/// <param name="name">The name of the directory, as it would appear in a directory listing.</param>
|
||||
/// <param name="lastModifiedUtc">The UTC date and time of the last modification made to the directory.</param>
|
||||
/// <returns>A newly-constructed instance of <see cref="MappedResourceInfo"/>.</returns>
|
||||
public static MappedResourceInfo ForDirectory(string path, string name, DateTime lastModifiedUtc)
|
||||
=> new MappedResourceInfo(path, name, lastModifiedUtc, 0, null);
|
||||
}
|
||||
}
|
||||
94
Vendor/EmbedIO-3.5.2/Files/ResourceFileProvider.cs
vendored
Normal file
94
Vendor/EmbedIO-3.5.2/Files/ResourceFileProvider.cs
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using EmbedIO.Utilities;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to embedded resources to a <see cref="FileModule"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="IFileProvider" />
|
||||
public class ResourceFileProvider : IFileProvider
|
||||
{
|
||||
private readonly DateTime _fileTime = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ResourceFileProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly where served files are contained as embedded resources.</param>
|
||||
/// <param name="pathPrefix">A string to prepend to provider-specific paths
|
||||
/// to form the name of a manifest resource in <paramref name="assembly"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="assembly"/> is <see langword="null"/>.</exception>
|
||||
public ResourceFileProvider(Assembly assembly, string pathPrefix)
|
||||
{
|
||||
Assembly = Validate.NotNull(nameof(assembly), assembly);
|
||||
PathPrefix = pathPrefix ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<string> ResourceChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly where served files are contained as embedded resources.
|
||||
/// </summary>
|
||||
public Assembly Assembly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string that is prepended to provider-specific paths to form the name of a manifest resource in <see cref="Assembly"/>.
|
||||
/// </summary>
|
||||
public string PathPrefix { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsImmutable => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Start(CancellationToken cancellationToken)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
|
||||
{
|
||||
var resourceName = PathPrefix + urlPath.Replace('/', '.');
|
||||
|
||||
long size;
|
||||
try
|
||||
{
|
||||
using var stream = Assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null || stream == Stream.Null)
|
||||
return null;
|
||||
|
||||
size = stream.Length;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lastSlashPos = urlPath.LastIndexOf('/');
|
||||
var name = urlPath.Substring(lastSlashPos + 1);
|
||||
|
||||
return MappedResourceInfo.ForFile(
|
||||
resourceName,
|
||||
name,
|
||||
_fileTime,
|
||||
size,
|
||||
mimeTypeProvider.GetMimeType(Path.GetExtension(name)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Stream OpenFile(string path) => Assembly.GetManifestResourceStream(path);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
|
||||
=> Enumerable.Empty<MappedResourceInfo>();
|
||||
}
|
||||
}
|
||||
110
Vendor/EmbedIO-3.5.2/Files/ZipFileProvider.cs
vendored
Normal file
110
Vendor/EmbedIO-3.5.2/Files/ZipFileProvider.cs
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
using EmbedIO.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace EmbedIO.Files
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to files contained in a <c>.zip</c> file to a <see cref="FileModule"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="IFileProvider" />
|
||||
public class ZipFileProvider : IDisposable, IFileProvider
|
||||
{
|
||||
private readonly ZipArchive _zipArchive;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ZipFileProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="zipFilePath">The zip file path.</param>
|
||||
public ZipFileProvider(string zipFilePath)
|
||||
: this(new FileStream(Validate.LocalPath(nameof(zipFilePath), zipFilePath, true), FileMode.Open))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ZipFileProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream that contains the archive.</param>
|
||||
/// <param name="leaveOpen"><see langword="true"/> to leave the stream open after the web server
|
||||
/// is disposed; otherwise, <see langword="false"/>.</param>
|
||||
public ZipFileProvider(Stream stream, bool leaveOpen = false)
|
||||
{
|
||||
_zipArchive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="ZipFileProvider"/> class.
|
||||
/// </summary>
|
||||
~ZipFileProvider()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<string> ResourceChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsImmutable => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Start(CancellationToken cancellationToken)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public MappedResourceInfo? MapUrlPath(string urlPath, IMimeTypeProvider mimeTypeProvider)
|
||||
{
|
||||
if (urlPath.Length == 1)
|
||||
return null;
|
||||
|
||||
urlPath = Uri.UnescapeDataString(urlPath);
|
||||
|
||||
var entry = _zipArchive.GetEntry(urlPath.Substring(1));
|
||||
if (entry == null)
|
||||
return null;
|
||||
|
||||
return MappedResourceInfo.ForFile(
|
||||
entry.FullName,
|
||||
entry.Name,
|
||||
entry.LastWriteTime.DateTime,
|
||||
entry.Length,
|
||||
mimeTypeProvider.GetMimeType(Path.GetExtension(entry.Name)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Stream OpenFile(string path)
|
||||
=> _zipArchive.GetEntry(path)?.Open() ?? throw new FileNotFoundException($"\"{path}\" cannot be found in Zip archive.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<MappedResourceInfo> GetDirectoryEntries(string path, IMimeTypeProvider mimeTypeProvider)
|
||||
=> Enumerable.Empty<MappedResourceInfo>();
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_zipArchive.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user