OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library shelf_static.static_handler; |
| 6 |
| 7 import 'dart:io'; |
| 8 |
| 9 import 'package:http_parser/http_parser.dart'; |
| 10 import 'package:mime/mime.dart' as mime; |
| 11 import 'package:path/path.dart' as p; |
| 12 import 'package:shelf/shelf.dart'; |
| 13 |
| 14 import 'directory_listing.dart'; |
| 15 import 'util.dart'; |
| 16 |
| 17 // TODO option to exclude hidden files? |
| 18 |
| 19 /// Creates a Shelf [Handler] that serves files from the provided |
| 20 /// [fileSystemPath]. |
| 21 /// |
| 22 /// Accessing a path containing symbolic links will succeed only if the resolved |
| 23 /// path is within [fileSystemPath]. To allow access to paths outside of |
| 24 /// [fileSystemPath], set [serveFilesOutsidePath] to `true`. |
| 25 /// |
| 26 /// When a existing directory is requested and a [defaultDocument] is specified |
| 27 /// the directory is checked for a file with that name. If it exists, it is |
| 28 /// served. |
| 29 /// |
| 30 /// If no [defaultDocument] is found and [listDirectories] is true, then the |
| 31 /// handler produces a listing of the directory. |
| 32 Handler createStaticHandler(String fileSystemPath, |
| 33 {bool serveFilesOutsidePath: false, String defaultDocument, |
| 34 bool listDirectories: false}) { |
| 35 var rootDir = new Directory(fileSystemPath); |
| 36 if (!rootDir.existsSync()) { |
| 37 throw new ArgumentError('A directory corresponding to fileSystemPath ' |
| 38 '"$fileSystemPath" could not be found'); |
| 39 } |
| 40 |
| 41 fileSystemPath = rootDir.resolveSymbolicLinksSync(); |
| 42 |
| 43 if (defaultDocument != null) { |
| 44 if (defaultDocument != p.basename(defaultDocument)) { |
| 45 throw new ArgumentError('defaultDocument must be a file name.'); |
| 46 } |
| 47 } |
| 48 |
| 49 return (Request request) { |
| 50 var segs = [fileSystemPath]..addAll(request.url.pathSegments); |
| 51 |
| 52 var fsPath = p.joinAll(segs); |
| 53 |
| 54 var entityType = FileSystemEntity.typeSync(fsPath, followLinks: true); |
| 55 |
| 56 File file = null; |
| 57 |
| 58 if (entityType == FileSystemEntityType.FILE) { |
| 59 file = new File(fsPath); |
| 60 } else if (entityType == FileSystemEntityType.DIRECTORY) { |
| 61 file = _tryDefaultFile(fsPath, defaultDocument); |
| 62 if (file == null && listDirectories) { |
| 63 var uri = request.requestedUri; |
| 64 if (!uri.path.endsWith('/')) return _redirectToAddTrailingSlash(uri); |
| 65 return listDirectory(fileSystemPath, fsPath); |
| 66 } |
| 67 } |
| 68 |
| 69 if (file == null) { |
| 70 return new Response.notFound('Not Found'); |
| 71 } |
| 72 |
| 73 if (!serveFilesOutsidePath) { |
| 74 var resolvedPath = file.resolveSymbolicLinksSync(); |
| 75 |
| 76 // Do not serve a file outside of the original fileSystemPath |
| 77 if (!p.isWithin(fileSystemPath, resolvedPath)) { |
| 78 return new Response.notFound('Not Found'); |
| 79 } |
| 80 } |
| 81 |
| 82 // when serving the default document for a directory, if the requested |
| 83 // path doesn't end with '/', redirect to the path with a trailing '/' |
| 84 var uri = request.requestedUri; |
| 85 if (entityType == FileSystemEntityType.DIRECTORY && |
| 86 !uri.path.endsWith('/')) { |
| 87 return _redirectToAddTrailingSlash(uri); |
| 88 } |
| 89 |
| 90 var fileStat = file.statSync(); |
| 91 var ifModifiedSince = request.ifModifiedSince; |
| 92 |
| 93 if (ifModifiedSince != null) { |
| 94 var fileChangeAtSecResolution = toSecondResolution(fileStat.changed); |
| 95 if (!fileChangeAtSecResolution.isAfter(ifModifiedSince)) { |
| 96 return new Response.notModified(); |
| 97 } |
| 98 } |
| 99 |
| 100 var headers = <String, String>{ |
| 101 HttpHeaders.CONTENT_LENGTH: fileStat.size.toString(), |
| 102 HttpHeaders.LAST_MODIFIED: formatHttpDate(fileStat.changed) |
| 103 }; |
| 104 |
| 105 var contentType = mime.lookupMimeType(file.path); |
| 106 if (contentType != null) { |
| 107 headers[HttpHeaders.CONTENT_TYPE] = contentType; |
| 108 } |
| 109 |
| 110 return new Response.ok(file.openRead(), headers: headers); |
| 111 }; |
| 112 } |
| 113 |
| 114 Response _redirectToAddTrailingSlash(Uri uri) { |
| 115 var location = new Uri( |
| 116 scheme: uri.scheme, |
| 117 userInfo: uri.userInfo, |
| 118 host: uri.host, |
| 119 port: uri.port, |
| 120 path: uri.path + '/', |
| 121 query: uri.query); |
| 122 |
| 123 return new Response.movedPermanently(location.toString()); |
| 124 } |
| 125 |
| 126 File _tryDefaultFile(String dirPath, String defaultFile) { |
| 127 if (defaultFile == null) return null; |
| 128 |
| 129 var filePath = p.join(dirPath, defaultFile); |
| 130 |
| 131 var file = new File(filePath); |
| 132 |
| 133 if (file.existsSync()) { |
| 134 return file; |
| 135 } |
| 136 |
| 137 return null; |
| 138 } |
OLD | NEW |