Skip to content

Commit ce11f09

Browse files
authored
Static files from resources (#174)
* Fix issue with regex websocket * Adding new module and unit testing * Refactor to file modules * Code Review * Codacy * Codacy * Upgrade SWAN
1 parent a4f7ce0 commit ce11f09

25 files changed

+453
-263
lines changed

src/Unosquare.Labs.EmbedIO.Samples/WebSocketsSample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ protected override void OnClientConnected(WebSocketContext context)
152152
var process = new Process
153153
{
154154
EnableRaisingEvents = true,
155-
StartInfo = new ProcessStartInfo()
155+
StartInfo = new ProcessStartInfo
156156
{
157157
CreateNoWindow = true,
158158
ErrorDialog = false,

src/Unosquare.Labs.EmbedIO/Constants/Strings.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,5 @@ internal static class Strings
4949
#else
5050
StringComparer.OrdinalIgnoreCase;
5151
#endif
52-
53-
/// <summary>
54-
/// The static file string comparer
55-
/// </summary>
56-
internal static StringComparer StaticFileStringComparer { get; } =
57-
#if !NETSTANDARD1_3 && !UWP
58-
StringComparer.InvariantCulture;
59-
#else
60-
StringComparer.Ordinal;
61-
#endif
6252
}
6353
}

src/Unosquare.Labs.EmbedIO/Extensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ public static Dictionary<string, object> RequestRegexUrlParams(
302302
if (validateFunc == null) validateFunc = () => false;
303303
if (requestPath == basePath && !validateFunc()) return new Dictionary<string, object>();
304304

305-
var regex = new Regex(RouteParamRegex.Replace(basePath, RegexRouteReplace));
305+
var regex = new Regex(RouteParamRegex.Replace(basePath, RegexRouteReplace), RegexOptions.IgnoreCase);
306306
var match = regex.Match(requestPath);
307307

308308
var pathParts = basePath.Split('/');
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
namespace Unosquare.Labs.EmbedIO.Modules
2+
{
3+
using System;
4+
using Swan;
5+
using System.Collections.Generic;
6+
using System.Collections.ObjectModel;
7+
using System.IO;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Constants;
11+
#if NET47
12+
using System.Net;
13+
#else
14+
using Net;
15+
#endif
16+
17+
public abstract class FileModuleBase
18+
: WebModuleBase
19+
{
20+
/// <summary>
21+
/// The maximum gzip input length
22+
/// </summary>
23+
protected const int MaxGzipInputLength = 4 * 1024 * 1024;
24+
25+
/// <summary>
26+
/// The chunk size for sending files
27+
/// </summary>
28+
private const int ChunkSize = 256 * 1024;
29+
30+
private readonly Lazy<Dictionary<string, string>> _mimeTypes =
31+
new Lazy<Dictionary<string, string>>(
32+
() =>
33+
new Dictionary<string, string>(Constants.MimeTypes.DefaultMimeTypes, Strings.StandardStringComparer));
34+
35+
/// <summary>
36+
/// Gets the collection holding the MIME types.
37+
/// </summary>
38+
/// <value>
39+
/// The MIME types.
40+
/// </value>
41+
public Lazy<ReadOnlyDictionary<string, string>> MimeTypes
42+
=>
43+
new Lazy<ReadOnlyDictionary<string, string>>(
44+
() => new ReadOnlyDictionary<string, string>(_mimeTypes.Value));
45+
46+
/// <summary>
47+
/// The default headers
48+
/// </summary>
49+
public Dictionary<string, string> DefaultHeaders { get; } = new Dictionary<string, string>();
50+
51+
/// <summary>
52+
/// Gets or sets a value indicating whether [use gzip].
53+
/// </summary>
54+
/// <value>
55+
/// <c>true</c> if [use gzip]; otherwise, <c>false</c>.
56+
/// </value>
57+
public bool UseGzip { get; set; }
58+
59+
/// <summary>
60+
/// Writes the file asynchronous.
61+
/// </summary>
62+
/// <param name="usingPartial">if set to <c>true</c> [using partial].</param>
63+
/// <param name="partialHeader">The partial header.</param>
64+
/// <param name="fileSize">Size of the file.</param>
65+
/// <param name="context">The context.</param>
66+
/// <param name="buffer">The buffer.</param>
67+
/// <param name="ct">The ct.</param>
68+
/// <returns>A task representing the write action.</returns>
69+
protected Task WriteFileAsync(
70+
bool usingPartial,
71+
string partialHeader,
72+
long fileSize,
73+
HttpListenerContext context,
74+
Stream buffer,
75+
CancellationToken ct)
76+
{
77+
long lowerByteIndex = 0;
78+
79+
if (usingPartial &&
80+
CalculateRange(partialHeader, fileSize, out lowerByteIndex, out var upperByteIndex))
81+
{
82+
if (upperByteIndex > fileSize)
83+
{
84+
// invalid partial request
85+
context.Response.StatusCode = 416;
86+
context.Response.AddHeader(Headers.ContentRanges, $"bytes */{fileSize}");
87+
88+
return Task.FromResult(0);
89+
}
90+
91+
if (upperByteIndex == fileSize)
92+
{
93+
context.Response.ContentLength64 = buffer.Length;
94+
}
95+
else
96+
{
97+
context.Response.StatusCode = 206;
98+
context.Response.ContentLength64 = upperByteIndex - lowerByteIndex + 1;
99+
100+
context.Response.AddHeader(Headers.ContentRanges,
101+
$"bytes {lowerByteIndex}-{upperByteIndex}/{fileSize}");
102+
}
103+
}
104+
else
105+
{
106+
if (UseGzip &&
107+
context.RequestHeader(Headers.AcceptEncoding).Contains(Headers.CompressionGzip) &&
108+
buffer.Length < MaxGzipInputLength &&
109+
110+
// Ignore audio/video from compression
111+
context.Response.ContentType?.StartsWith("audio") == false &&
112+
context.Response.ContentType?.StartsWith("video") == false)
113+
{
114+
// Perform compression if available
115+
buffer = buffer.Compress();
116+
context.Response.AddHeader(Headers.ContentEncoding, Headers.CompressionGzip);
117+
lowerByteIndex = 0;
118+
}
119+
120+
context.Response.ContentLength64 = buffer.Length;
121+
}
122+
123+
return WriteToOutputStream(context.Response, buffer, lowerByteIndex, ct);
124+
}
125+
126+
/// <summary>
127+
/// Sets the default cache headers.
128+
/// </summary>
129+
/// <param name="response">The response.</param>
130+
protected void SetDefaultCacheHeaders(HttpListenerResponse response)
131+
{
132+
response.AddHeader(Headers.CacheControl,
133+
DefaultHeaders.GetValueOrDefault(Headers.CacheControl, "private"));
134+
response.AddHeader(Headers.Pragma, DefaultHeaders.GetValueOrDefault(Headers.Pragma, string.Empty));
135+
response.AddHeader(Headers.Expires, DefaultHeaders.GetValueOrDefault(Headers.Expires, string.Empty));
136+
}
137+
138+
private static async Task WriteToOutputStream(
139+
HttpListenerResponse response,
140+
Stream buffer,
141+
long lowerByteIndex,
142+
CancellationToken ct)
143+
{
144+
var streamBuffer = new byte[ChunkSize];
145+
long sendData = 0;
146+
var readBufferSize = ChunkSize;
147+
148+
while (true)
149+
{
150+
if (sendData + ChunkSize > response.ContentLength64) readBufferSize = (int)(response.ContentLength64 - sendData);
151+
152+
buffer.Seek(lowerByteIndex + sendData, SeekOrigin.Begin);
153+
var read = await buffer.ReadAsync(streamBuffer, 0, readBufferSize, ct);
154+
155+
if (read == 0) break;
156+
157+
sendData += read;
158+
await response.OutputStream.WriteAsync(streamBuffer, 0, readBufferSize, ct);
159+
}
160+
}
161+
162+
private static bool CalculateRange(
163+
string partialHeader,
164+
long fileSize,
165+
out long lowerByteIndex,
166+
out long upperByteIndex)
167+
{
168+
lowerByteIndex = 0;
169+
upperByteIndex = 0;
170+
171+
var range = partialHeader.Replace("bytes=", string.Empty).Split('-');
172+
173+
if (range.Length == 2 && long.TryParse(range[0], out lowerByteIndex) &&
174+
long.TryParse(range[1], out upperByteIndex))
175+
{
176+
return true;
177+
}
178+
179+
if ((range.Length == 2 && long.TryParse(range[0], out lowerByteIndex) &&
180+
string.IsNullOrWhiteSpace(range[1])) ||
181+
(range.Length == 1 && long.TryParse(range[0], out lowerByteIndex)))
182+
{
183+
upperByteIndex = (int)fileSize;
184+
return true;
185+
}
186+
187+
if (range.Length == 2 && string.IsNullOrWhiteSpace(range[0]) &&
188+
long.TryParse(range[1], out upperByteIndex))
189+
{
190+
lowerByteIndex = (int)fileSize - upperByteIndex;
191+
upperByteIndex = (int)fileSize;
192+
return true;
193+
}
194+
195+
return false;
196+
}
197+
}
198+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
namespace Unosquare.Labs.EmbedIO.Modules
2+
{
3+
using Constants;
4+
using EmbedIO;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Linq;
9+
using Swan;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using System.Reflection;
13+
#if NET47
14+
using System.Net;
15+
#else
16+
using Net;
17+
#endif
18+
19+
/// <summary>
20+
/// Represents a simple module to server resource files from the .NET assembly.
21+
/// </summary>
22+
public class ResourceFilesModule
23+
: FileModuleBase
24+
{
25+
private readonly Assembly _sourceAssembly;
26+
private readonly string _resourcePathRoot;
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="ResourceFilesModule" /> class.
30+
/// </summary>
31+
/// <param name="sourceAssembly">The source assembly.</param>
32+
/// <param name="resourcePath">The resource path.</param>
33+
/// <param name="headers">The headers.</param>
34+
/// <exception cref="ArgumentNullException">sourceAssembly</exception>
35+
/// <exception cref="ArgumentException">Path ' + fileSystemPath + ' does not exist.</exception>
36+
public ResourceFilesModule(
37+
Assembly sourceAssembly,
38+
string resourcePath,
39+
Dictionary<string, string> headers = null)
40+
{
41+
if (sourceAssembly == null)
42+
throw new ArgumentNullException(nameof(sourceAssembly));
43+
44+
if (sourceAssembly.GetName() == null)
45+
throw new ArgumentException($"Assembly '{sourceAssembly}' not valid.");
46+
47+
UseGzip = true;
48+
_sourceAssembly = sourceAssembly;
49+
_resourcePathRoot = resourcePath;
50+
51+
headers?.ForEach(DefaultHeaders.Add);
52+
53+
AddHandler(ModuleMap.AnyPath, HttpVerbs.Head, (context, ct) => HandleGet(context, ct, false));
54+
AddHandler(ModuleMap.AnyPath, HttpVerbs.Get, (context, ct) => HandleGet(context, ct));
55+
}
56+
57+
/// <inheritdoc />
58+
public override string Name => nameof(ResourceFilesModule).Humanize();
59+
60+
private static string PathResourcerize(string s) => s == "/" ? "index.html" : s.Substring(1, s.Length - 1).Replace('/', '.');
61+
62+
private async Task<bool> HandleGet(HttpListenerContext context, CancellationToken ct, bool sendBuffer = true)
63+
{
64+
Stream buffer = null;
65+
66+
try
67+
{
68+
var localPath = PathResourcerize(context.RequestPathCaseSensitive());
69+
var partialHeader = context.RequestHeader(Headers.Range);
70+
71+
$"Resource System: {localPath}".Debug();
72+
73+
buffer = _sourceAssembly.GetManifestResourceStream($"{_resourcePathRoot}.{localPath}");
74+
75+
// If buffer is null something is really wrong
76+
if (buffer == null)
77+
{
78+
return false;
79+
}
80+
81+
// check to see if the file was modified or e-tag is the same
82+
var utcFileDateString = DateTime.Now.ToUniversalTime()
83+
.ToString(Strings.BrowserTimeFormat, Strings.StandardCultureInfo);
84+
85+
SetHeaders(context.Response, localPath, utcFileDateString);
86+
87+
// HEAD (file size only)
88+
if (sendBuffer == false)
89+
{
90+
context.Response.ContentLength64 = buffer.Length;
91+
return true;
92+
}
93+
94+
await WriteFileAsync(partialHeader?.StartsWith("bytes=") == true, partialHeader, buffer.Length, context, buffer, ct);
95+
}
96+
catch (HttpListenerException)
97+
{
98+
// Connection error, nothing else to do
99+
}
100+
finally
101+
{
102+
buffer?.Dispose();
103+
}
104+
105+
return true;
106+
}
107+
108+
private void SetHeaders(HttpListenerResponse response, string localPath, string utcFileDateString)
109+
{
110+
var fileExtension = localPath.Contains(".") ? $".{localPath.Split('.').Last()}" : ".html";
111+
112+
if (MimeTypes.Value.ContainsKey(fileExtension))
113+
response.ContentType = MimeTypes.Value[fileExtension];
114+
115+
SetDefaultCacheHeaders(response);
116+
117+
response.AddHeader(Headers.LastModified, utcFileDateString);
118+
response.AddHeader(Headers.AcceptRanges, "bytes");
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)