2016-11-23 17:22:23 +00:00
import ' dart:async ' ;
import ' package:angel_framework/angel_framework.dart ' ;
2017-09-23 21:57:54 +00:00
import ' package:file/file.dart ' ;
2018-08-28 14:58:28 +00:00
import ' package:http_parser/http_parser.dart ' ;
2017-11-18 06:12:59 +00:00
import ' package:path/path.dart ' as p ;
2017-01-25 22:40:41 +00:00
2016-11-23 17:22:23 +00:00
final RegExp _param = new RegExp ( r':([A-Za-z0-9_]+)(\((.+)\))?' ) ;
final RegExp _straySlashes = new RegExp ( r'(^/+)|(/+$)' ) ;
String _pathify ( String path ) {
var p = path . replaceAll ( _straySlashes , ' ' ) ;
Map < String , String > replace = { } ;
for ( Match match in _param . allMatches ( p ) ) {
if ( match [ 3 ] ! = null ) replace [ match [ 0 ] ] = ' : ${ match [ 1 ] } ' ;
}
replace . forEach ( ( k , v ) {
p = p . replaceAll ( k , v ) ;
} ) ;
return p ;
}
2017-02-27 00:19:34 +00:00
/// A static server plug-in.
2017-09-23 21:57:54 +00:00
class VirtualDirectory {
2016-11-23 20:14:05 +00:00
String _prefix ;
2016-11-23 17:22:23 +00:00
Directory _source ;
2017-02-27 00:19:34 +00:00
/// The directory to serve files from.
2016-11-23 17:22:23 +00:00
Directory get source = > _source ;
2017-02-27 00:19:34 +00:00
/// An optional callback to run before serving files.
2017-09-23 21:57:54 +00:00
final Function ( File file , RequestContext req , ResponseContext res ) callback ;
final Angel app ;
final FileSystem fileSystem ;
2017-02-27 00:19:34 +00:00
/// Filenames to be resolved within directories as indices.
final Iterable < String > indexFileNames ;
/// An optional public path to map requests to.
2016-11-23 17:22:23 +00:00
final String publicPath ;
2017-11-18 06:12:59 +00:00
/// If `true` (default: `false`), then if a directory does not contain any of the specific [indexFileNames], a default directory listing will be served.
final bool allowDirectoryListing ;
2018-07-09 17:38:22 +00:00
/// If `true` (default: `true`), then files will be opened as streams and piped into the request.
///
/// If not, the response buffer will be used instead.
2018-08-28 14:58:28 +00:00
final bool useBuffer ;
2018-07-09 17:38:22 +00:00
2017-09-23 21:57:54 +00:00
VirtualDirectory ( this . app , this . fileSystem ,
2016-11-23 17:22:23 +00:00
{ Directory source ,
this . indexFileNames: const [ ' index.html ' ] ,
2017-01-25 22:40:41 +00:00
this . publicPath: ' / ' ,
2017-11-18 06:12:59 +00:00
this . callback ,
2018-07-09 17:38:22 +00:00
this . allowDirectoryListing: false ,
2018-10-21 00:24:44 +00:00
this . useBuffer: false } ) {
2016-11-23 20:14:05 +00:00
_prefix = publicPath . replaceAll ( _straySlashes , ' ' ) ;
2016-11-23 17:22:23 +00:00
if ( source ! = null ) {
_source = source ;
} else {
2017-09-23 21:57:54 +00:00
String dirPath = app . isProduction ? ' ./build/web ' : ' ./web ' ;
_source = fileSystem . directory ( dirPath ) ;
2017-06-16 02:05:06 +00:00
}
}
2017-09-23 21:57:54 +00:00
/// Responds to incoming HTTP requests.
Future < bool > handleRequest ( RequestContext req , ResponseContext res ) {
2018-11-13 21:25:17 +00:00
if ( req . method ! = ' GET ' & & req . method ! = ' HEAD ' )
return new Future < bool > . value ( true ) ;
2017-09-23 21:57:54 +00:00
var path = req . path . replaceAll ( _straySlashes , ' ' ) ;
2017-06-16 02:05:06 +00:00
2017-09-23 21:57:54 +00:00
if ( _prefix ? . isNotEmpty = = true & & ! path . startsWith ( _prefix ) )
return new Future < bool > . value ( true ) ;
2017-06-16 02:05:06 +00:00
2017-09-23 21:57:54 +00:00
return servePath ( path , req , res ) ;
2017-06-16 02:05:06 +00:00
}
2017-09-23 21:57:54 +00:00
/// A handler that serves the file at the given path, unless the user has requested that path.
2017-11-18 06:12:59 +00:00
///
/// You can also limit this functionality to specific values of the `Accept` header, ex. `text/html`.
/// If [accepts] is `null`, OR at least one of the content types in [accepts] is present,
/// the view will be served.
2018-08-28 14:58:28 +00:00
RequestHandler pushState ( String path , { Iterable accepts } ) {
2017-09-23 21:57:54 +00:00
var vPath = path . replaceAll ( _straySlashes , ' ' ) ;
if ( _prefix ? . isNotEmpty = = true ) vPath = ' $ _prefix / $ vPath ' ;
2017-06-16 02:05:06 +00:00
2017-09-23 21:57:54 +00:00
return ( RequestContext req , ResponseContext res ) {
var path = req . path . replaceAll ( _straySlashes , ' ' ) ;
if ( path = = vPath ) return new Future < bool > . value ( true ) ;
2017-11-18 06:12:59 +00:00
if ( accepts ? . isNotEmpty = = true ) {
2018-07-09 17:38:22 +00:00
if ( ! accepts . any ( ( x ) = > req . accepts ( x , strict: true ) ) )
return new Future < bool > . value ( true ) ;
2017-11-18 06:12:59 +00:00
}
2017-09-23 21:57:54 +00:00
return servePath ( vPath , req , res ) ;
} ;
2017-02-22 23:43:27 +00:00
}
2017-09-23 21:57:54 +00:00
/// Writes the file at the given virtual [path] to a response.
Future < bool > servePath (
String path , RequestContext req , ResponseContext res ) async {
2017-02-22 23:43:27 +00:00
if ( _prefix . isNotEmpty ) {
2017-06-20 19:57:03 +00:00
// Only replace the *first* incidence
// Resolve: https://github.com/angel-dart/angel/issues/41
path = path . replaceFirst ( new RegExp ( ' ^ ' + _pathify ( _prefix ) ) , ' ' ) ;
2017-02-22 23:43:27 +00:00
}
if ( path . isEmpty ) path = ' . ' ;
2017-06-20 19:57:03 +00:00
path = path . replaceAll ( _straySlashes , ' ' ) ;
2017-02-22 23:43:27 +00:00
var absolute = source . absolute . uri . resolve ( path ) . toFilePath ( ) ;
2018-11-13 21:25:17 +00:00
var parent = source . absolute . uri . toFilePath ( ) ;
if ( ! p . isWithin ( parent , absolute ) & & ! p . equals ( parent , absolute ) )
return true ;
2017-09-23 21:57:54 +00:00
var stat = await fileSystem . stat ( absolute ) ;
2017-11-18 06:12:59 +00:00
return await serveStat ( absolute , path , stat , req , res ) ;
2017-02-22 23:43:27 +00:00
}
2017-09-23 21:57:54 +00:00
/// Writes the file at the path given by the [stat] to a response.
2018-07-09 17:38:22 +00:00
Future < bool > serveStat ( String absolute , String relative , FileStat stat ,
RequestContext req , ResponseContext res ) async {
2018-08-28 14:58:28 +00:00
if ( stat . type = = FileSystemEntityType . directory )
2017-09-23 21:57:54 +00:00
return await serveDirectory (
2017-11-18 06:12:59 +00:00
fileSystem . directory ( absolute ) , relative , stat , req , res ) ;
2018-08-28 14:58:28 +00:00
else if ( stat . type = = FileSystemEntityType . file )
2017-09-23 21:57:54 +00:00
return await serveFile ( fileSystem . file ( absolute ) , stat , req , res ) ;
2018-08-28 14:58:28 +00:00
else if ( stat . type = = FileSystemEntityType . link ) {
2017-09-23 21:57:54 +00:00
var link = fileSystem . link ( absolute ) ;
2017-02-22 23:43:27 +00:00
return await servePath ( await link . resolveSymbolicLinks ( ) , req , res ) ;
} else
return true ;
}
2016-11-23 20:14:05 +00:00
2017-09-23 21:57:54 +00:00
/// Serves the index file of a [directory], if it exists.
2018-07-09 17:38:22 +00:00
Future < bool > serveDirectory ( Directory directory , String relative ,
FileStat stat , RequestContext req , ResponseContext res ) async {
2017-06-16 02:05:06 +00:00
for ( String indexFileName in indexFileNames ) {
final index =
2017-09-23 21:57:54 +00:00
fileSystem . file ( directory . absolute . uri . resolve ( indexFileName ) ) ;
2017-06-16 02:05:06 +00:00
if ( await index . exists ( ) ) {
2018-11-13 21:25:17 +00:00
if ( req . method = = ' HEAD ' ) return false ;
2017-06-16 02:05:06 +00:00
return await serveFile ( index , stat , req , res ) ;
}
}
2017-11-18 06:12:59 +00:00
if ( allowDirectoryListing = = true ) {
2018-11-13 21:25:17 +00:00
if ( req . method = = ' HEAD ' ) return false ;
2018-08-28 14:58:28 +00:00
res . contentType = new MediaType ( ' text ' , ' html ' ) ;
2017-11-18 06:12:59 +00:00
res
. . write ( ' <!DOCTYPE html> ' )
. . write ( ' <html> ' )
. . write (
' <head><meta name="viewport" content="width=device-width,initial-scale=1"> ' )
. . write ( ' <style>ul { list-style-type: none; }</style> ' )
2018-11-13 23:37:31 +00:00
. . write ( ' </head><body> ' ) ;
2017-11-18 06:12:59 +00:00
res . write ( ' <li><a href="..">..</a></li> ' ) ;
List < FileSystemEntity > entities = await directory
. list ( followLinks: false )
. toList ( )
. then ( ( l ) = > new List . from ( l ) ) ;
entities . sort ( ( a , b ) {
if ( a is Directory ) {
if ( b is Directory ) return a . path . compareTo ( b . path ) ;
return - 1 ;
} else if ( a is File ) {
if ( b is Directory )
return 1 ;
else if ( b is File ) return a . path . compareTo ( b . path ) ;
return - 1 ;
} else if ( b is Link ) return a . path . compareTo ( b . path ) ;
return 1 ;
} ) ;
for ( var entity in entities ) {
var stub = p . basename ( entity . path ) ;
var href = stub ;
String type ;
if ( entity is File )
type = ' [File] ' ;
else if ( entity is Directory )
type = ' [Directory] ' ;
else if ( entity is Link ) type = ' [Link] ' ;
2018-07-09 17:38:22 +00:00
if ( relative . isNotEmpty ) href = ' / ' + relative + ' / ' + stub ;
2017-11-18 06:12:59 +00:00
2018-07-09 17:38:22 +00:00
if ( entity is Directory ) href + = ' / ' ;
2017-11-18 06:12:59 +00:00
res . write ( ' <li><a href=" $ href "> $ type $ stub </a></li> ' ) ;
}
res . . write ( ' </body></html> ' ) ;
return false ;
}
2017-06-16 02:05:06 +00:00
return true ;
}
void _ensureContentTypeAllowed ( String mimeType , RequestContext req ) {
2017-09-23 21:57:54 +00:00
var value = req . headers . value ( ' accept ' ) ;
2017-06-16 02:05:06 +00:00
bool acceptable = value = = null | |
2017-06-16 03:13:01 +00:00
value ? . isNotEmpty ! = true | |
( mimeType ? . isNotEmpty = = true & & value ? . contains ( mimeType ) = = true ) | |
2017-06-16 03:05:07 +00:00
value ? . contains ( ' */* ' ) = = true ;
2017-06-16 02:05:06 +00:00
if ( ! acceptable )
throw new AngelHttpException (
new UnsupportedError (
' Client requested $ value , but server wanted to send $ mimeType . ' ) ,
2017-09-23 21:57:54 +00:00
statusCode: 406 ,
2017-06-16 02:05:06 +00:00
message: ' 406 Not Acceptable ' ) ;
}
2017-09-23 21:57:54 +00:00
/// Writes the contents of a file to a response.
2017-06-16 02:05:06 +00:00
Future < bool > serveFile (
File file , FileStat stat , RequestContext req , ResponseContext res ) async {
2018-11-13 21:25:17 +00:00
if ( req . method = = ' HEAD ' ) return false ;
2017-06-16 02:05:06 +00:00
if ( callback ! = null ) {
var r = callback ( file , req , res ) ;
r = r is Future ? await r : r ;
2018-07-09 17:38:22 +00:00
return r = = true ;
//if (r != null && r != true) return r;
2017-06-16 02:05:06 +00:00
}
2018-11-13 21:25:17 +00:00
var type =
app . mimeTypeResolver . lookup ( file . path ) ? ? ' application/octet-stream ' ;
2017-06-16 02:05:06 +00:00
_ensureContentTypeAllowed ( type , req ) ;
2018-08-28 14:58:28 +00:00
res . contentType = new MediaType . parse ( type ) ;
2017-06-16 02:05:06 +00:00
2018-11-13 21:25:17 +00:00
if ( useBuffer = = true ) res . useBuffer ( ) ;
await res . streamFile ( file ) ;
2017-06-16 02:05:06 +00:00
return false ;
}
2016-11-23 17:22:23 +00:00
}