-
Notifications
You must be signed in to change notification settings - Fork 177
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor virtual path management of StaticFilesModule (fixes #272 et…
… al.) (#275) * Avoid creating a temporary Dictionary. * Remove excess parenthesis. * Refactor virtual path management of StaticFilesModule (fixes #272 et al.) * Determinism: - always evaluate virtual path in reverse ordinal order; - normalize URL paths to simplify code and ensure consistent mapping. * Separation of concerns: - have methods that deal with mapping URL paths to local paths, and other methods that deal with the existence of local paths. * Caching: - keep track of _how_ and _why_ paths were mapped; - discard cached mapped paths whose generating data (virtual path, default extension, default documkent name) has changed. * Support for dynamic file systems: - allow the user to deactivate path caching in situations where the contents of served directories may change over time. * Compatibility: - keep existing constructors, methods, and properties of StaticFilesModule; - keep existing exception semantics (e.g. throw InvalidOperationException instead of ArgumentException for invalid paths passed to RegisterVirtualPath). * Behavior changes: - scenarios where files could be added to a served directory after being requested (resulting in error 404) are no longer supported: said files will continue to give error 404, unless path caching is disabled. - scenarios where files are continuously added and/or deleted, which previously resulted in spurious error 404s (#272), can now be supported by disabling path caching. * Modify test that would not throw on machines where drive E: actually exists.
- Loading branch information
Showing
8 changed files
with
644 additions
and
238 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
namespace Unosquare.Labs.EmbedIO.Core | ||
{ | ||
using System; | ||
using System.IO; | ||
using System.Text.RegularExpressions; | ||
|
||
internal static class PathHelper | ||
{ | ||
private static readonly Regex _multipleSlashRegex = new Regex("//+", RegexOptions.Compiled | RegexOptions.CultureInvariant); | ||
|
||
static readonly char[] _invalidLocalPathChars = GetInvalidLocalPathChars(); | ||
|
||
public static bool IsValidUrlPath(string urlPath) => !string.IsNullOrEmpty(urlPath) && urlPath[0] == '/'; | ||
|
||
// urlPath must be a valid URL path | ||
// (not null, not empty, starting with a slash.) | ||
public static string NormalizeUrlPath(string urlPath, bool isBasePath) | ||
{ | ||
// Replace each run of multiple slashes with a single slash | ||
urlPath = _multipleSlashRegex.Replace(urlPath, "/"); | ||
|
||
// The root path needs no further checking. | ||
var length = urlPath.Length; | ||
if (length == 1) | ||
return urlPath; | ||
|
||
// Base URL paths must end with a slash; | ||
// non-base URL paths must NOT end with a slash. | ||
// The final slash is irrelevant for the URL itself | ||
// (it has to map the same way with or without it) | ||
// but makes comparing and mapping URls a lot simpler. | ||
var finalPosition = length - 1; | ||
var endsWithSlash = urlPath[finalPosition] == '/'; | ||
return isBasePath | ||
? (endsWithSlash ? urlPath : urlPath + "/") | ||
: (endsWithSlash ? urlPath.Substring(0, finalPosition) : urlPath); | ||
} | ||
|
||
public static string EnsureValidUrlPath(string urlPath, bool isBasePath) | ||
{ | ||
if (urlPath == null) | ||
{ | ||
throw new InvalidOperationException("URL path is null,"); | ||
} | ||
|
||
if (urlPath.Length == 0) | ||
{ | ||
throw new InvalidOperationException("URL path is empty."); | ||
} | ||
|
||
if (urlPath[0] != '/') | ||
{ | ||
throw new InvalidOperationException($"URL path \"{urlPath}\"does not start with a slash."); | ||
} | ||
|
||
return NormalizeUrlPath(urlPath, isBasePath); | ||
} | ||
|
||
public static string EnsureValidLocalPath(string localPath) | ||
{ | ||
if (localPath == null) | ||
{ | ||
throw new InvalidOperationException("Local path is null."); | ||
} | ||
|
||
if (localPath.Length == 0) | ||
{ | ||
throw new InvalidOperationException("Local path is empty."); | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(localPath)) | ||
{ | ||
throw new InvalidOperationException("Local path contains only white space."); | ||
} | ||
|
||
if (localPath.IndexOfAny(_invalidLocalPathChars) >= 0) | ||
{ | ||
throw new InvalidOperationException($"Local path \"{localPath}\"contains one or more invalid characters."); | ||
} | ||
|
||
return localPath; | ||
} | ||
|
||
private static char[] GetInvalidLocalPathChars() | ||
{ | ||
var systemChars = Path.GetInvalidPathChars(); | ||
var p = systemChars.Length; | ||
var result = new char[p + 2]; | ||
Array.Copy(systemChars, result, p); | ||
result[p++] = '*'; | ||
result[p] = '?'; | ||
return result; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
namespace Unosquare.Labs.EmbedIO.Core | ||
{ | ||
using System; | ||
|
||
[Flags] | ||
internal enum PathMappingResult | ||
{ | ||
/// <summary> | ||
/// The mask used to extract the mapping result. | ||
/// </summary> | ||
MappingMask = 0xF, | ||
|
||
/// <summary> | ||
/// The path was not found. | ||
/// </summary> | ||
NotFound = 0, | ||
|
||
/// <summary> | ||
/// The path was mapped to a file. | ||
/// </summary> | ||
IsFile = 0x1, | ||
|
||
/// <summary> | ||
/// The path was mapped to a directory. | ||
/// </summary> | ||
IsDirectory = 0x2, | ||
|
||
/// <summary> | ||
/// The default extension has been appended to the path. | ||
/// </summary> | ||
DefaultExtensionUsed = 0x1000, | ||
|
||
/// <summary> | ||
/// The default document name has been appended to the path. | ||
/// </summary> | ||
DefaultDocumentUsed = 0x2000, | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
src/Unosquare.Labs.EmbedIO/Core/ReverseOrdinalStringComparer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
namespace Unosquare.Labs.EmbedIO.Core | ||
{ | ||
using System; | ||
using System.Collections.Generic; | ||
|
||
// Sorts strings in reverse order to obtain the evaluation order of virtual paths | ||
internal sealed class ReverseOrdinalStringComparer : IComparer<string> | ||
{ | ||
private static readonly IComparer<string> _directComparer = StringComparer.Ordinal; | ||
|
||
private ReverseOrdinalStringComparer() | ||
{ | ||
} | ||
|
||
public static IComparer<string> Instance { get; } = new ReverseOrdinalStringComparer(); | ||
|
||
public int Compare(string x, string y) => _directComparer.Compare(y, x); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
namespace Unosquare.Labs.EmbedIO.Core | ||
{ | ||
using System; | ||
using System.IO; | ||
|
||
internal sealed class VirtualPath | ||
{ | ||
public VirtualPath(string baseUrlPath, string baseLocalPath) | ||
{ | ||
BaseUrlPath = PathHelper.EnsureValidUrlPath(baseUrlPath, true); | ||
try | ||
{ | ||
BaseLocalPath = Path.GetFullPath(PathHelper.EnsureValidLocalPath(baseLocalPath)); | ||
} | ||
#pragma warning disable CA1031 | ||
catch (Exception e) | ||
{ | ||
throw new InvalidOperationException($"Cannot determine the full local path for \"{baseLocalPath}\".", e); | ||
} | ||
#pragma warning restore CA1031 | ||
} | ||
|
||
public string BaseUrlPath { get; } | ||
|
||
public string BaseLocalPath { get; } | ||
|
||
internal bool CanMapUrlPath(string urlPath) => urlPath.StartsWith(BaseUrlPath, StringComparison.Ordinal); | ||
|
||
internal bool TryMapUrlPathLoLocalPath(string urlPath, out string localPath) | ||
{ | ||
if (!CanMapUrlPath(urlPath)) | ||
{ | ||
localPath = null; | ||
return false; | ||
} | ||
|
||
var relativeUrlPath = urlPath.Substring(BaseUrlPath.Length); | ||
localPath = Path.Combine(BaseLocalPath, relativeUrlPath.Replace('/', Path.DirectorySeparatorChar)); | ||
return true; | ||
} | ||
} | ||
} |
Oops, something went wrong.