2022-04-02 21:15:20 +00:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Net ;
2022-07-27 23:30:52 +00:00
using System.Runtime.CompilerServices ;
2022-04-02 21:15:20 +00:00
using System.Text ;
2024-03-31 01:01:24 +00:00
2022-04-02 21:15:20 +00:00
using System.Text.RegularExpressions ;
using System.Threading.Tasks ;
2024-03-18 12:31:51 +00:00
using HeyRed.Mime ;
2024-03-31 01:01:24 +00:00
using Newtonsoft.Json ;
2022-04-02 21:15:20 +00:00
namespace Tesses.WebServer
{
//This file contains modified code from https://github.com/dajuric/simple-http
/// <summary>
/// Delegate executed when a file is about to be read from a body stream.
/// </summary>
/// <param name="fieldName">Field name.</param>
/// <param name="fileName">name of the file.</param>
/// <param name="contentType">Content type.</param>
/// <returns>Stream to be populated.</returns>
public delegate Stream OnFile ( string fieldName , string fileName , string contentType ) ;
public static class DajuricSimpleHttpExtensions
{
2022-07-27 23:30:52 +00:00
/ * Thanks to you we fixed this
public static void Print ( string text , [ CallerLineNumber ] int lineNumber = 0 )
{
Console . WriteLine ( $"[LINE {lineNumber}] {text}" ) ;
} * /
2022-04-03 14:39:54 +00:00
static void Deconstruct < T1 , T2 > ( this KeyValuePair < T1 , T2 > tuple , out T1 key , out T2 value )
{
key = tuple . Key ;
value = tuple . Value ;
}
2022-04-02 21:15:20 +00:00
const string BYTES_RANGE_HEADER = "Range" ;
static bool ParseForm ( this ServerContext ctx )
{
2022-07-27 23:30:52 +00:00
//Print("Enter ParseForm(this ServerContext ctx)");
2022-04-02 21:15:20 +00:00
var args = ctx . QueryParams ;
string content_type = ctx . RequestHeaders . GetFirst ( "Content-Type" ) ;
if ( content_type ! = "application/x-www-form-urlencoded" )
return false ;
2022-07-27 23:30:52 +00:00
//Print("Before BodyAsString");
2022-04-02 21:15:20 +00:00
var str = ctx . BodyAsString ( ) ;
2022-07-27 23:30:52 +00:00
//Print("After BodyAsString");
2022-04-02 21:15:20 +00:00
if ( str = = null )
return false ;
2022-07-27 23:30:52 +00:00
//Print("Before For Loop");
2022-04-02 21:15:20 +00:00
foreach ( var pair in str . Split ( '&' ) )
{
var nameValue = pair . Split ( '=' ) ;
if ( nameValue . Length ! = ( 1 + 1 ) )
continue ;
2022-07-27 23:30:52 +00:00
//Print($"Before Add: {nameValue[0]}: {nameValue[1]}");
2022-04-02 21:15:20 +00:00
args . Add ( nameValue [ 0 ] , WebUtility . UrlDecode ( nameValue [ 1 ] ) ) ;
2022-07-27 23:30:52 +00:00
//Print($"After Add: {nameValue[0]}: {nameValue[1]}");
2022-04-02 21:15:20 +00:00
}
return true ;
}
2022-04-24 18:56:06 +00:00
private static string BodyAsString ( this ServerContext ctx )
2022-04-02 21:15:20 +00:00
{
2022-07-27 23:30:52 +00:00
2022-04-02 21:15:20 +00:00
string str = null ;
2022-07-27 02:52:32 +00:00
using ( var reader = new StreamReader ( ctx . GetRequestStream ( ) ) )
2022-04-02 21:15:20 +00:00
{
2022-07-27 23:30:52 +00:00
//Print("Before ReadToEnd");
2022-04-02 21:15:20 +00:00
str = reader . ReadToEnd ( ) ;
2022-07-27 23:30:52 +00:00
//Print("After ReadToEnd");
2022-04-02 21:15:20 +00:00
}
return str ;
}
2022-09-01 05:26:26 +00:00
public static void SendNonSeekableStream ( this ServerContext ctx , Stream strm , long readFor = - 1 , string contentType = "application/octet-stream" )
{
try
{
2024-03-18 12:31:51 +00:00
var strm2 = new ChunkedStream ( ctx . NetworkStream , false ) ;
2022-09-01 05:26:26 +00:00
long tread = 0 ;
byte [ ] buffer = new byte [ 8 * 1024 * 1024 ] ;
int read = 0 ;
do
{
if ( readFor > - 1 ) {
read = ( int ) Math . Min ( buffer . Length , readFor - tread ) ;
} else {
read = buffer . Length ;
}
if ( read = = 0 ) break ;
read = strm . Read ( buffer , 0 , read ) ;
2024-03-18 12:31:51 +00:00
strm2 . Write ( buffer , 0 , read ) ;
2022-09-01 05:26:26 +00:00
} while ( read > 0 ) ;
} finally {
strm . Close ( ) ;
ctx . NetworkStream . Close ( ) ;
}
}
public static async Task SendNonSeekableStreamAsync ( this ServerContext ctx , Stream strm , long readFor = - 1 , string contentType = "application/octet-stream" )
{
try
{
2024-03-18 12:31:51 +00:00
var strm2 = new ChunkedStream ( ctx . NetworkStream , false ) ;
2022-09-01 05:26:26 +00:00
long tread = 0 ;
byte [ ] buffer = new byte [ 8 * 1024 * 1024 ] ;
int read = 0 ;
do
{
if ( readFor > - 1 ) {
read = ( int ) Math . Min ( buffer . Length , readFor - tread ) ;
} else {
read = buffer . Length ;
}
if ( read = = 0 ) break ;
read = await strm . ReadAsync ( buffer , 0 , read ) ;
2024-03-18 12:31:51 +00:00
await strm2 . WriteAsync ( buffer , 0 , read ) ;
2022-09-01 05:26:26 +00:00
} while ( read > 0 ) ;
} finally {
strm . Close ( ) ;
ctx . NetworkStream . Close ( ) ;
}
}
public static void SendStream ( this ServerContext ctx , Stream strm , string contentType = "application/octet-stream" )
{
//ctx.StatusCode = 200;
int start = 0 , end = ( int ) strm . Length - 1 ;
if ( ctx . RequestHeaders . ContainsKey ( BYTES_RANGE_HEADER ) & & strm . CanSeek )
{
if ( ctx . RequestHeaders [ BYTES_RANGE_HEADER ] . Count > 1 )
{
throw new NotSupportedException ( "Multiple 'Range' headers are not supported." ) ;
}
var range = ctx . RequestHeaders [ BYTES_RANGE_HEADER ] [ 0 ] . Replace ( "bytes=" , String . Empty )
. Split ( new string [ ] { "-" } , StringSplitOptions . RemoveEmptyEntries )
. Select ( x = > Int32 . Parse ( x ) )
. ToArray ( ) ;
start = ( range . Length > 0 ) ? range [ 0 ] : 0 ;
end = ( range . Length > 1 ) ? range [ 1 ] : ( int ) ( strm . Length - 1 ) ;
2022-04-02 21:15:20 +00:00
2022-09-01 05:26:26 +00:00
var hdrs = ctx . ResponseHeaders ;
hdrs . Add ( "Accept-Ranges" , "bytes" ) ;
hdrs . Add ( "Content-Range" , "bytes " + start + "-" + end + "/" + strm . Length ) ;
ctx . StatusCode = 206 ;
}
ctx . ResponseHeaders . Add ( "Content-Length" , ( end - start + 1 ) . ToString ( ) ) ;
2024-03-18 12:31:51 +00:00
ctx . WithMimeType ( contentType ) ;
2022-09-01 05:26:26 +00:00
ctx . WriteHeaders ( ) ;
if ( ! ctx . Method . Equals ( "HEAD" , StringComparison . Ordinal ) )
{
try
{
if ( strm . CanSeek )
strm . Position = start ;
strm . CopyTo ( ctx . NetworkStream , Math . Min ( 8 * 1024 * 1024 , end - start + 1 ) ) ;
}
finally
{
strm . Close ( ) ;
ctx . NetworkStream . Close ( ) ;
}
}
}
2022-04-02 21:15:20 +00:00
public static async Task SendStreamAsync ( this ServerContext ctx , Stream strm , string contentType = "application/octet-stream" )
{
//ctx.StatusCode = 200;
int start = 0 , end = ( int ) strm . Length - 1 ;
2022-06-22 20:03:20 +00:00
if ( ctx . RequestHeaders . ContainsKey ( BYTES_RANGE_HEADER ) & & strm . CanSeek )
2022-04-02 21:15:20 +00:00
{
if ( ctx . RequestHeaders [ BYTES_RANGE_HEADER ] . Count > 1 )
{
throw new NotSupportedException ( "Multiple 'Range' headers are not supported." ) ;
}
var range = ctx . RequestHeaders [ BYTES_RANGE_HEADER ] [ 0 ] . Replace ( "bytes=" , String . Empty )
. Split ( new string [ ] { "-" } , StringSplitOptions . RemoveEmptyEntries )
. Select ( x = > Int32 . Parse ( x ) )
. ToArray ( ) ;
start = ( range . Length > 0 ) ? range [ 0 ] : 0 ;
end = ( range . Length > 1 ) ? range [ 1 ] : ( int ) ( strm . Length - 1 ) ;
var hdrs = ctx . ResponseHeaders ;
hdrs . Add ( "Accept-Ranges" , "bytes" ) ;
hdrs . Add ( "Content-Range" , "bytes " + start + "-" + end + "/" + strm . Length ) ;
ctx . StatusCode = 206 ;
}
ctx . ResponseHeaders . Add ( "Content-Length" , ( end - start + 1 ) . ToString ( ) ) ;
2024-03-31 01:01:24 +00:00
ctx . WithMimeType ( contentType ) ;
2022-04-02 21:15:20 +00:00
await ctx . WriteHeadersAsync ( ) ;
if ( ! ctx . Method . Equals ( "HEAD" , StringComparison . Ordinal ) )
{
try
{
2022-06-22 20:03:20 +00:00
if ( strm . CanSeek )
2022-04-02 21:15:20 +00:00
strm . Position = start ;
2022-09-01 05:26:26 +00:00
await strm . CopyToAsync ( ctx . NetworkStream , Math . Min ( 8 * 1024 * 1024 , end - start + 1 ) ) ;
2022-04-02 21:15:20 +00:00
}
finally
{
strm . Close ( ) ;
ctx . NetworkStream . Close ( ) ;
}
}
}
2024-03-18 12:31:51 +00:00
/// <summary>
/// Send file to client (supports range partial content)
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="file">the file to serve</param>
public static async Task SendFileAsync ( this ServerContext ctx , string file )
{
if ( ! File . Exists ( file ) )
{
await ctx . SendNotFoundAsync ( ) ;
return ;
}
if ( ! ctx . RequestHeaders . ContainsKey ( BYTES_RANGE_HEADER ) & & handleIfCached ( ) )
return ;
using ( var f = File . OpenRead ( file ) )
{
await ctx . SendStreamAsync ( f , MimeTypesMap . GetMimeType ( file ) ) ;
}
2022-04-02 21:15:20 +00:00
2024-03-18 12:31:51 +00:00
bool handleIfCached ( )
{
var lastModified = File . GetLastWriteTimeUtc ( file ) ;
string etag = lastModified . Ticks . ToString ( "x" ) ;
ctx . ResponseHeaders . Add ( "ETag" , etag ) ;
ctx . ResponseHeaders . Add ( "Last-Modified" , lastModified . ToString ( "R" ) ) ;
if ( ctx . RequestHeaders . TryGetFirst ( "If-None-Match" , out var ifNoneMatch ) )
{
var eTags = ifNoneMatch . Split ( ',' ) . Select ( x = > x . Trim ( ) ) . ToArray ( ) ;
if ( eTags . Contains ( etag ) )
{
ctx . StatusCode = 304 ;
ctx . WriteHeaders ( ) ;
return true ;
}
}
if ( ctx . RequestHeaders . TryGetFirst ( "If-Modified-Since" , out var iMs ) & & DateTime . TryParse ( iMs , out DateTime ifModifiedSince ) )
{
if ( lastModified < = ifModifiedSince )
{
ctx . StatusCode = 304 ;
ctx . WriteHeaders ( ) ;
return true ;
}
}
return false ;
}
}
/// <summary>
/// Send file to client (supports range partial content)
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="file">the file to serve</param>
public static void SendFile ( this ServerContext ctx , string file )
{
if ( ! File . Exists ( file ) )
{
ctx . SendNotFound ( ) ;
return ;
}
if ( ! ctx . RequestHeaders . ContainsKey ( BYTES_RANGE_HEADER ) & & handleIfCached ( ) )
return ;
using ( var f = File . OpenRead ( file ) )
{
ctx . SendStream ( f , MimeTypesMap . GetMimeType ( file ) ) ;
}
bool handleIfCached ( )
{
var lastModified = File . GetLastWriteTimeUtc ( file ) ;
string etag = lastModified . Ticks . ToString ( "x" ) ;
ctx . ResponseHeaders . Add ( "ETag" , etag ) ;
ctx . ResponseHeaders . Add ( "Last-Modified" , lastModified . ToString ( "R" ) ) ;
if ( ctx . RequestHeaders . TryGetFirst ( "If-None-Match" , out var ifNoneMatch ) )
{
var eTags = ifNoneMatch . Split ( ',' ) . Select ( x = > x . Trim ( ) ) . ToArray ( ) ;
if ( eTags . Contains ( etag ) )
{
ctx . StatusCode = 304 ;
ctx . WriteHeaders ( ) ;
return true ;
}
}
if ( ctx . RequestHeaders . TryGetFirst ( "If-Modified-Since" , out var iMs ) & & DateTime . TryParse ( iMs , out DateTime ifModifiedSince ) )
{
if ( lastModified < = ifModifiedSince )
{
ctx . StatusCode = 304 ;
ctx . WriteHeaders ( ) ;
return true ;
}
}
return false ;
}
}
static Dictionary < string , List < HttpFile > > ParseMultipartForm ( ServerContext serverCtx , OnFile onFile )
2022-04-02 21:15:20 +00:00
{
var args = serverCtx . QueryParams ;
string content_type = serverCtx . RequestHeaders . GetFirst ( "Content-Type" ) ;
if ( content_type . StartsWith ( "multipart/form-data" , StringComparison . Ordinal ) = = false )
throw new InvalidDataException ( "Not 'multipart/form-data'." ) ;
var boundary = Regex . Match ( content_type , "boundary=(.+)" ) . Groups [ 1 ] . Value ;
boundary = "--" + boundary ;
2024-03-18 12:31:51 +00:00
var files = new Dictionary < string , List < HttpFile > > ( ) ;
2022-07-27 02:52:32 +00:00
var inputStream = new BufferedStream ( serverCtx . GetRequestStream ( ) ) ;
2022-07-27 23:30:52 +00:00
//Print("Before ParseUntillBoundaryEnd");
2022-04-02 21:15:20 +00:00
parseUntillBoundaryEnd ( inputStream , new MemoryStream ( ) , boundary ) ;
2022-07-27 23:30:52 +00:00
//Print("After ParseUntillBoundaryEnd");
2022-04-02 21:15:20 +00:00
while ( true )
{
2022-07-27 23:30:52 +00:00
//Print("Before ParseSection");
2022-04-02 21:15:20 +00:00
var ( n , v , fn , ct ) = parseSection ( inputStream , "\r\n" + boundary , onFile ) ;
2022-07-27 23:30:52 +00:00
//Print("After ParseSection");
2022-04-02 21:15:20 +00:00
if ( String . IsNullOrEmpty ( n ) ) break ;
v . Position = 0 ;
if ( ! String . IsNullOrEmpty ( fn ) )
files . Add ( n , new HttpFile ( fn , v , ct ) ) ;
else
args . Add ( n , readAsString ( v ) ) ;
}
return files ;
}
private static ( string Name , Stream Value , string FileName , string ContentType ) parseSection ( Stream source , string boundary , OnFile onFile )
{
2022-07-27 23:30:52 +00:00
//Print("Before ReadContentDisposition");
2022-04-02 21:15:20 +00:00
var ( n , fn , ct ) = readContentDisposition ( source ) ;
2022-07-27 23:30:52 +00:00
//Print("After ReadContentDisposition");
2022-04-02 21:15:20 +00:00
source . ReadByte ( ) ; source . ReadByte ( ) ; //\r\n (empty row)
var dst = String . IsNullOrEmpty ( fn ) ? new MemoryStream ( ) : onFile ( n , fn , ct ) ;
if ( dst = = null )
throw new ArgumentException ( nameof ( onFile ) , "The on-file callback must return a stream." ) ;
2022-07-27 23:30:52 +00:00
//Print("Before ParseUntillBodyEnd");
2022-04-02 21:15:20 +00:00
parseUntillBoundaryEnd ( source , dst , boundary ) ;
2022-07-27 23:30:52 +00:00
//Print("Before ParseUntillBodyEnd");
2022-04-02 21:15:20 +00:00
return ( n , dst , fn , ct ) ;
}
private static ( string Name , string FileName , string ContentType ) readContentDisposition ( Stream stream )
{
const string UTF_FNAME = "utf-8''" ;
var l = readLine ( stream ) ;
if ( String . IsNullOrEmpty ( l ) )
return ( null , null , null ) ;
//(regex matches are taken from NancyFX) and modified
var n = Regex . Match ( l , @"name=""?(?<n>[^\""]*)" ) . Groups [ "n" ] . Value ;
var f = Regex . Match ( l , @"filename\*?=""?(?<f>[^\"";]*)" ) . Groups [ "f" ] ? . Value ;
string cType = null ;
if ( ! String . IsNullOrEmpty ( f ) )
{
if ( f . StartsWith ( UTF_FNAME ) )
f = Uri . UnescapeDataString ( f . Substring ( UTF_FNAME . Length ) ) ;
l = readLine ( stream ) ;
cType = Regex . Match ( l , "Content-Type: (?<cType>.+)" ) . Groups [ "cType" ] . Value ;
}
return ( n , f , cType ) ;
}
private static void parseUntillBoundaryEnd ( Stream source , Stream destination , string boundary )
{
var checkBuffer = new byte [ boundary . Length ] ; //for boundary checking
int b , i = 0 ;
while ( ( b = source . ReadByte ( ) ) ! = - 1 )
{
if ( i = = boundary . Length ) //boundary found -> go to the end of line
{
if ( b = = '\n' ) break ;
continue ;
}
if ( b = = boundary [ i ] ) //start filling the check buffer
{
checkBuffer [ i ] = ( byte ) b ;
i + + ;
}
else
{
var idx = 0 ;
while ( idx < i ) //write the buffer data to stream
{
destination . WriteByte ( checkBuffer [ idx ] ) ;
idx + + ;
}
i = 0 ;
destination . WriteByte ( ( byte ) b ) ; //write the current byte
}
}
}
private static string readLine ( Stream stream )
{
var sb = new StringBuilder ( ) ;
int b ;
while ( ( b = stream . ReadByte ( ) ) ! = - 1 & & b ! = '\n' )
sb . Append ( ( char ) b ) ;
if ( sb . Length > 0 & & sb [ sb . Length - 1 ] = = '\r' )
sb . Remove ( sb . Length - 1 , 1 ) ;
return sb . ToString ( ) ;
}
private static string readAsString ( Stream stream )
{
var sb = new StringBuilder ( ) ;
int b ;
while ( ( b = stream . ReadByte ( ) ) ! = - 1 )
sb . Append ( ( char ) b ) ;
return sb . ToString ( ) ;
}
/// <summary>
/// Parses body of the request including form and multi-part form data.
/// </summary>
/// <param name="request">HTTP request.</param>
/// <param name="args">Key-value pairs populated by the form data by this function.</param>
/// <returns>Name-file pair collection.</returns>
public static Dictionary < string , HttpFile > ParseBody ( this ServerContext ctx )
{
2022-07-27 23:30:52 +00:00
2022-04-02 21:15:20 +00:00
return ctx . ParseBody ( ( n , fn , ct ) = > new MemoryStream ( ) ) ;
}
/// <summary>
2024-03-18 12:31:51 +00:00
/// Parses body of the request including form and multi-part form data, allowing multiple file with same key.
2022-04-02 21:15:20 +00:00
/// </summary>
/// <param name="request">HTTP request.</param>
/// <param name="args">Key-value pairs populated by the form data by this function.</param>
/// <param name="onFile">
/// Function called if a file is about to be parsed. The stream is attached to a corresponding <see cref="HttpFile"/>.
/// <para>By default, <see cref="MemoryStream"/> is used, but for large files, it is recommended to open <see cref="FileStream"/> directly.</para>
/// </param>
/// <returns>Name-file pair collection.</returns>
2024-03-18 12:31:51 +00:00
public static Dictionary < string , List < HttpFile > > ParseBodyMultiple ( this ServerContext request , OnFile onFile )
2022-04-02 21:15:20 +00:00
{
if ( request = = null )
throw new ArgumentNullException ( nameof ( request ) ) ;
if ( ! request . RequestHeaders . ContainsKey ( "Content-Type" ) )
throw new ArgumentNullException ( "request.RequestHeaders[\"Content-Type\"]" ) ;
if ( onFile = = null )
throw new ArgumentNullException ( nameof ( onFile ) ) ;
2024-03-18 12:31:51 +00:00
2022-04-02 21:15:20 +00:00
string content_type = request . RequestHeaders . GetFirst ( "Content-Type" ) ;
if ( content_type . StartsWith ( "application/x-www-form-urlencoded" , StringComparison . Ordinal ) )
{
ParseForm ( request ) ;
2024-03-18 12:31:51 +00:00
var files = new Dictionary < string , List < HttpFile > > ( ) ;
return files ;
2022-04-02 21:15:20 +00:00
}
else if ( content_type . StartsWith ( "multipart/form-data" , StringComparison . Ordinal ) )
{
2024-03-18 12:31:51 +00:00
return ParseMultipartForm ( request , onFile ) ;
2022-04-02 21:15:20 +00:00
}
else
throw new NotSupportedException ( "The body content-type is not supported." ) ;
2024-03-18 12:31:51 +00:00
}
/// <summary>
/// Parses body of the request including form and multi-part form data.
/// </summary>
/// <param name="request">HTTP request.</param>
/// <param name="args">Key-value pairs populated by the form data by this function.</param>
/// <param name="onFile">
/// Function called if a file is about to be parsed. The stream is attached to a corresponding <see cref="HttpFile"/>.
/// <para>By default, <see cref="MemoryStream"/> is used, but for large files, it is recommended to open <see cref="FileStream"/> directly.</para>
/// </param>
/// <returns>Name-file pair collection.</returns>
public static Dictionary < string , HttpFile > ParseBody ( this ServerContext request , OnFile onFile )
{
var res = ParseBodyMultiple ( request , onFile ) ;
Dictionary < string , HttpFile > files = new Dictionary < string , HttpFile > ( ) ;
foreach ( var item in res )
{
if ( item . Value . Count > 0 )
{
files . Add ( item . Key , item . Value [ 0 ] ) ;
}
}
2022-04-02 21:15:20 +00:00
return files ;
}
2024-03-31 01:01:24 +00:00
public static HttpFileResponse ParseBodyWithTempDirectory ( this ServerContext request )
{
DateTime dt = DateTime . Now ;
return request . ParseBodyWithTempDirectory ( Path . Combine ( Path . GetTempPath ( ) , $"TWSUPLOAD_{dt.ToString(" yyyyMMdd_HHmmss ")}_{ServerContext.UniqueNumber()}" ) ) ;
}
2024-03-18 12:31:51 +00:00
/// <summary>
/// Parses body of the request including form and multi-part form data, allowing multiple file with same key and storing the files in a temp directory specified by the user.
/// </summary>
/// <param name="request">HTTP request.</param>
/// <param name="tempDir">The root directory to store all the uploads in</param>
/// <returns>A HttpFileResponse Containing Paths to files, Dispose only deletes the files, so if you want to keep the files don't dispose it.</returns>
public static HttpFileResponse ParseBodyWithTempDirectory ( this ServerContext request , string tempDir )
{
Directory . CreateDirectory ( tempDir ) ;
Stream Open ( string field , string filename , string contentType )
{
string dir = Path . Combine ( tempDir , ServerContext . FixFileName ( field ) ) ;
Directory . CreateDirectory ( dir ) ;
string filename2 = Path . Combine ( dir , ServerContext . FixFileName ( filename ) ) ;
return File . Create ( filename2 ) ;
}
List < HttpFileResponseEntry > responseEntries = new List < HttpFileResponseEntry > ( ) ;
foreach ( var item in ParseBodyMultiple ( request , Open ) )
{
foreach ( var i2 in item . Value )
{
responseEntries . Add ( new HttpFileResponseEntry ( Path . Combine ( tempDir , ServerContext . FixFileName ( item . Key ) , ServerContext . FixFileName ( i2 . FileName ) ) , i2 . FileName , item . Key , i2 . ContentType ) ) ;
i2 . Dispose ( ) ;
}
}
return new HttpFileResponse ( tempDir , responseEntries ) ;
}
}
2024-03-31 01:01:24 +00:00
2024-03-18 12:31:51 +00:00
public sealed class HttpFileResponseEntry
{
public HttpFileResponseEntry ( string path , string filename , string fieldname , string contype )
{
Path = path ;
FileName = filename ;
ContentType = contype ;
FieldName = fieldname ;
}
public string FileName { get ; }
public string ContentType { get ; }
public string Path { get ; }
public string FieldName { get ; }
2024-03-31 01:01:24 +00:00
[JsonIgnore]
2024-03-18 12:31:51 +00:00
public FileInfo FileInfo = > new FileInfo ( Path ) ;
2024-03-31 01:01:24 +00:00
[JsonIgnore]
public object PrivateData { get ; set ; } = null ;
2024-03-18 12:31:51 +00:00
public Stream OpenRead ( )
{
return File . OpenRead ( Path ) ;
}
public void MoveTo ( string dest )
{
File . Move ( Path , dest ) ;
}
}
public sealed class HttpFileResponse : IDisposable
{
public HttpFileResponse ( string dir , IReadOnlyList < HttpFileResponseEntry > entries )
{
Directory = dir ;
Files = entries ;
}
public IReadOnlyList < HttpFileResponseEntry > Files { get ; }
public string Directory { get ; }
2024-03-31 01:01:24 +00:00
2024-03-18 12:31:51 +00:00
public void Dispose ( )
{
2024-03-31 01:01:24 +00:00
if ( System . IO . Directory . Exists ( Directory ) )
System . IO . Directory . Delete ( Directory , true ) ;
}
~ HttpFileResponse ( )
{
Dispose ( ) ;
2024-03-18 12:31:51 +00:00
}
2022-04-02 21:15:20 +00:00
}
/// <summary>
/// HTTP file data container.
/// </summary>
public class HttpFile : IDisposable
{
/// <summary>
/// Creates new HTTP file data container.
/// </summary>
/// <param name="fileName">File name.</param>
/// <param name="value">Data.</param>
/// <param name="contentType">Content type.</param>
internal HttpFile ( string fileName , Stream value , string contentType )
{
Value = value ;
FileName = fileName ;
ContentType = contentType ;
}
/// <summary>
/// Gets the name of the file.
/// </summary>
public string FileName { get ; private set ; }
/// <summary>
/// Gets the data.
/// <para>If a stream is created <see cref="OnFile"/> it will be closed when this HttpFile object is disposed.</para>
/// </summary>
public Stream Value { get ; private set ; }
/// <summary>
/// Content type.
/// </summary>
public string ContentType { get ; private set ; }
/// <summary>
/// Saves the data into a file.
/// <para>Directory path will be auto created if does not exists.</para>
/// </summary>
/// <param name="fileName">File path with name.</param>
/// <param name="overwrite">True to overwrite the existing file, false otherwise.</param>
/// <returns>True if the file is saved/overwritten, false otherwise.</returns>
public bool Save ( string fileName , bool overwrite = false )
{
if ( File . Exists ( Path . GetFullPath ( fileName ) ) )
return false ;
var dir = Path . GetDirectoryName ( Path . GetFullPath ( fileName ) ) ;
Directory . CreateDirectory ( dir ) ;
Value . Position = 0 ;
using ( var outStream = File . OpenWrite ( fileName ) )
Value . CopyTo ( outStream ) ;
return true ;
}
/// <summary>
/// Disposes the current instance.
/// </summary>
public void Dispose ( )
{
if ( Value ! = null )
{
Value ? . Dispose ( ) ;
Value = null ;
}
}
/// <summary>
/// Disposes the current instance.
/// </summary>
~ HttpFile ( )
{
Dispose ( ) ;
}
}
2022-04-03 14:39:54 +00:00
/// <summary>
/// Route server, Based on SimpleHTTP (Used most of the Route Source)
/// </summary>
public delegate bool ShouldProcessFunc ( ServerContext ctx ) ;
public delegate Task HttpActionAsync ( ServerContext ctx ) ;
public delegate void HttpAction ( ServerContext ctx ) ;
public class RouteServer : Server
{
2023-06-08 19:04:59 +00:00
public RouteServer ( ) : this ( new NotFoundServer ( ) )
{
}
public RouteServer ( IServer otherServer )
{
this . otherServer = otherServer ;
}
2022-04-03 14:39:54 +00:00
public List < ( ShouldProcessFunc ShouldProcessFunc , HttpActionAsync Action ) > Methods = new List < ( ShouldProcessFunc ShouldProcessFunc , HttpActionAsync Action ) > ( ) ;
2023-06-08 19:04:59 +00:00
private IServer otherServer ;
2022-04-03 14:39:54 +00:00
public override async Task GetAsync ( ServerContext ctx )
{
2023-06-08 19:04:59 +00:00
if ( ! await Process ( ctx ) )
{
await Guaranteed ( otherServer ) . GetAsync ( ctx ) ;
}
2022-04-03 14:39:54 +00:00
}
public override async Task PostAsync ( ServerContext ctx )
{
2023-06-08 19:04:59 +00:00
if ( ! await Process ( ctx ) )
{
await Guaranteed ( otherServer ) . PostAsync ( ctx ) ;
}
2022-04-03 14:39:54 +00:00
}
public override async Task OtherAsync ( ServerContext ctx )
{
2023-06-08 19:04:59 +00:00
if ( ! await Process ( ctx ) )
{
await Guaranteed ( otherServer ) . OtherAsync ( ctx ) ;
}
2022-04-03 14:39:54 +00:00
}
public override async Task OptionsAsync ( ServerContext ctx )
{
2023-06-08 19:04:59 +00:00
if ( ! await Process ( ctx ) )
{
await Guaranteed ( otherServer ) . OptionsAsync ( ctx ) ;
}
2022-04-03 14:39:54 +00:00
}
2023-06-08 19:04:59 +00:00
private async Task < bool > Process ( ServerContext ctx )
2022-04-03 14:39:54 +00:00
{
foreach ( var ( shouldProcessFunc , action ) in Methods )
{
if ( ! shouldProcessFunc ( ctx ) )
{
ctx . ResetQueryParms ( ) ;
continue ;
}
await action ( ctx ) ;
2023-06-08 19:04:59 +00:00
return true ;
2022-04-03 14:39:54 +00:00
}
2023-06-08 19:04:59 +00:00
return false ;
2022-04-03 14:39:54 +00:00
}
/// <summary>
/// Adds the specified action to the route collection.
/// <para>The order of actions defines the priority.</para>
/// </summary>
/// <param name="shouldProcess">Function defining whether the specified action should be executed or not.</param>
/// <param name="action">Action executed if the specified pattern matches the URL path.</param>
public void Add ( ShouldProcessFunc shouldProcess , HttpActionAsync action )
{
Methods . Add ( ( shouldProcess , action ) ) ;
}
/// <summary>
/// Adds the specified action to the route collection.
/// <para>The order of actions defines the priority.</para>
/// </summary>
/// <param name="url">
/// String url
/// </param>
/// <param name="action">Action executed if the specified pattern matches the URL path.</param>
/// <param name="method">HTTP method (GET, POST, DELETE, HEAD).</param>
public void Add ( string url , HttpActionAsync action , string method = "GET" )
{
Add ( ( e ) = >
{
if ( ! e . Method . Equals ( method , StringComparison . Ordinal ) )
return false ;
return e . UrlPath . Equals ( url , StringComparison . Ordinal ) ;
} ,
action ) ;
}
/// <summary>
/// Adds the specified action to the route collection.
/// <para>The order of actions defines the priority.</para>
/// </summary>
/// <param name="url">
/// String url
/// </param>
/// <param name="action">Action executed if the specified pattern matches the URL path.</param>
/// <param name="method">HTTP method (GET, POST, DELETE, HEAD).</param>
public void Add ( string url , HttpAction action , string method = "GET" )
{
Add ( ( e ) = >
{
if ( ! e . Method . Equals ( method , StringComparison . Ordinal ) )
return false ;
return e . UrlPath . Equals ( url , StringComparison . Ordinal ) ;
} ,
action ) ;
}
/// <summary>
/// Adds the specified action to the route collection.
/// <para>The order of actions defines the priority.</para>
/// </summary>
/// <param name="shouldProcess">Function defining whether the specified action should be executed or not.</param>
/// <param name="action">Action executed if the specified pattern matches the URL path.</param>
public void Add ( ShouldProcessFunc shouldProcess , HttpAction action )
{
Methods . Add ( ( shouldProcess , ( e ) = >
{
action ( e ) ;
return Task . FromResult ( true ) ;
}
) ) ;
}
}
2022-04-02 21:15:20 +00:00
}