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:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user