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 ' ;
2017-01-28 16:33:22 +00:00
import ' package:mime/mime.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 ;
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 ,
this . allowDirectoryListing: 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 ) {
if ( req . method ! = ' GET ' ) return new Future < bool > . value ( true ) ;
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.
RequestMiddleware 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 ) {
if ( ! accepts . any ( req . accepts ) ) return new Future < bool > . value ( true ) ;
}
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 ( ) ;
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.
2017-11-18 06:12:59 +00:00
Future < bool > serveStat ( String absolute , String relative , FileStat stat , RequestContext req ,
2017-02-22 23:43:27 +00:00
ResponseContext res ) async {
2017-06-16 02:05:06 +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 ) ;
2017-02-22 23:43:27 +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 ) ;
2017-02-22 23:43:27 +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.
2017-11-18 06:12:59 +00:00
Future < bool > serveDirectory ( Directory directory , String relative , FileStat stat ,
2017-06-16 02:05:06 +00:00
RequestContext req , ResponseContext res ) async {
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 ( ) ) {
return await serveFile ( index , stat , req , res ) ;
}
}
2017-11-18 06:12:59 +00:00
if ( allowDirectoryListing = = true ) {
res . headers [ ' content-type ' ] = ' text/html ' ;
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> ' )
. . write ( ' </head></html><body> ' ) ;
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] ' ;
if ( relative . isNotEmpty )
href = ' / ' + relative + ' / ' + stub ;
if ( entity is Directory )
href + = ' / ' ;
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 {
res . statusCode = 200 ;
if ( callback ! = null ) {
var r = callback ( file , req , res ) ;
r = r is Future ? await r : r ;
if ( r ! = null & & r ! = true ) return r ;
}
2017-07-10 22:31:17 +00:00
var type = lookupMimeType ( file . path ) ? ? ' application/octet-stream ' ;
2017-06-16 02:05:06 +00:00
_ensureContentTypeAllowed ( type , req ) ;
2017-09-23 21:57:54 +00:00
res . headers [ ' content-type ' ] = type ;
2017-06-16 02:05:06 +00:00
2017-09-23 21:57:54 +00:00
await file . openRead ( ) . pipe ( res ) ;
2017-06-16 02:05:06 +00:00
return false ;
}
2016-11-23 17:22:23 +00:00
}