1 module hunt.http.routing.handler.ResourceHandler; 2 3 import hunt.http.routing.handler.Util; 4 5 import hunt.http.routing.RoutingContext; 6 7 import hunt.http.Version; 8 import hunt.http.HttpHeader; 9 import hunt.http.HttpStatus; 10 import hunt.http.Util; 11 12 import hunt.logging; 13 import hunt.Exceptions; 14 import hunt.util.DateTime : TimeUnit; 15 import hunt.util.AcceptMimeType; 16 import hunt.util.MimeTypeUtils; 17 import hunt.util.MimeType; 18 19 import std.algorithm; 20 import std.array; 21 import std.conv; 22 import std.datetime; 23 import std.file; 24 import std.path; 25 import std.format; 26 import std.stdio; 27 import std.string; 28 29 /** 30 * 31 */ 32 abstract class AbstractResourceHandler : RouteHandler { 33 34 enum string CurrentRequestFile = "CurrentRequestFile"; 35 36 /** 37 * If directory listing is enabled. 38 */ 39 private bool _isListingEnabled = false; 40 private string _virtualPath; 41 private string _basePath; 42 // private string requestPath; 43 private size_t _bufferSize = 1024; 44 private bool _cachable = true; 45 private Duration _cacheTime = -1.seconds; 46 private int _slashNumberInVirtualPath = 0; 47 48 this(string virtualPath, string actualPath) { 49 assert(virtualPath[$-1] == '/'); 50 string rootPath = dirName(thisExePath); 51 _basePath = buildNormalizedPath(rootPath, actualPath); 52 _virtualPath = virtualPath; 53 _slashNumberInVirtualPath = cast(int)count(virtualPath, "/"); 54 } 55 56 protected string basePath() { 57 return _basePath; 58 } 59 60 // protected string requestFile() { 61 // return requestPath; 62 // } 63 64 // bool isBasePath() { 65 // return _basePath == requestPath; 66 // } 67 68 size_t bufferSize() { 69 return _bufferSize; 70 } 71 72 AbstractResourceHandler bufferSize(size_t size) { 73 _bufferSize = size; 74 return this; 75 } 76 77 bool cachable() { 78 return _cachable; 79 } 80 81 AbstractResourceHandler cachable(bool flag) { 82 _cachable = flag; 83 return this; 84 } 85 86 Duration cacheTime() { 87 return _cacheTime; 88 } 89 90 AbstractResourceHandler cacheTime(Duration t) { 91 _cacheTime = t; 92 return this; 93 } 94 95 bool isListingEnabled() { 96 return _isListingEnabled; 97 } 98 99 AbstractResourceHandler isListingEnabled(bool flag) { 100 _isListingEnabled = flag; 101 return this; 102 } 103 104 void handle(RoutingContext context) { 105 // string requestPath = context.getURI().getPath(); 106 string requestPath = context.getRequest().originalPath(); 107 version(HUNT_HTTP_DEBUG) infof("requestPath: %s, virtualPath: %s", requestPath, _virtualPath); 108 bool isDirectory = true; 109 110 if(requestPath.length <= 1) { 111 requestPath = _basePath; 112 } else { 113 // mapping virtual path which contains multiparts like /a/b/c to the actual base 114 isDirectory = requestPath[$-1] == '/'; 115 116 string[] parts = split(requestPath, "/"); 117 parts = parts[_slashNumberInVirtualPath .. $]; 118 119 string p = buildPath(parts); 120 version(HUNT_HTTP_DEBUG) tracef("stripped path: %s", p); 121 requestPath = buildNormalizedPath(_basePath, p); // no tailing '/' 122 if(isDirectory) requestPath ~= "/"; 123 } 124 125 version(HUNT_HTTP_DEBUG) tracef("actual path: %s, base: %s", requestPath, _basePath); 126 127 128 if(requestPath.exists()) { 129 130 try { 131 context.setAttribute(CurrentRequestFile, requestPath); 132 render(context, HttpStatus.OK_200, null); 133 context.succeed(true); 134 } catch(Exception ex) { 135 version(HUNT_DEBUG) errorf("http handler exception", ex.msg); 136 if (!context.isCommitted()) { 137 render(context, HttpStatus.INTERNAL_SERVER_ERROR_500, ex); 138 context.fail(ex); 139 } 140 } 141 } else { 142 version(HUNT_DEBUG) { 143 warningf("Failed to get resource %s from base %s, as the path did not exist", 144 requestPath, _basePath); 145 } 146 context.next(); 147 } 148 } 149 150 abstract void render(RoutingContext context, int status, Exception ex); 151 } 152 153 154 155 /** 156 * 157 */ 158 class DefaultResourceHandler : AbstractResourceHandler { 159 private MimeTypeUtils _mimetypes; 160 161 this(string virtualPath, string actualPath) { 162 super(virtualPath, actualPath); 163 _mimetypes = new MimeTypeUtils(); 164 } 165 166 override void render(RoutingContext context, int status, Exception t) { 167 context.setStatus(status); 168 169 HttpStatusCode code = HttpStatus.getCode(status); 170 if(code == HttpStatusCode.Null) 171 code = HttpStatusCode.INTERNAL_SERVER_ERROR; 172 173 string requestPath = context.getURI().getPath(); 174 175 version(HUNY_HTTP_DEBUG) { 176 tracef("path: %s, status: %d", requestPath, status); 177 } 178 179 // string title = status.to!string() ~ " " ~ code.getMessage(); 180 string title = format("Directory Listing - %s", requestPath); 181 string content; 182 if(status == HttpStatus.NOT_FOUND_404) { 183 content = "The resource " ~ requestPath ~ " is not found"; 184 } else if(status == HttpStatus.INTERNAL_SERVER_ERROR_500) { 185 content = "The server internal error. <br/>" ~ (t !is null ? t.msg : ""); 186 } else { 187 string requestFile = context.getAttribute(CurrentRequestFile).get!string(); 188 if(requestFile.isDir()) { 189 version(HUNT_HTTP_DEBUG) { 190 tracef("Try to list a directory: %s", requestFile); 191 } 192 if(isListingEnabled()) { 193 content = renderFileList(basePath(), requestFile, format(`Index of %s`, requestPath)); 194 } else { 195 content = format(`Index of %s`, requestPath); 196 } 197 198 } else { 199 version(HUNT_HTTP_DEBUG) { 200 tracef("Rendering a file: %s", requestFile); 201 } 202 handleRequestFile(context, requestFile); 203 return; 204 } 205 } 206 207 renderDefaults(context, title, content); 208 } 209 210 private void renderDefaults(RoutingContext context, string title, string content) { 211 212 context.responseHeader(HttpHeader.CONTENT_TYPE, "text/html"); 213 214 context.write("<!DOCTYPE html>\n") 215 .write("<html>\n") 216 .write("<head>\n") 217 .write("<title>") 218 .write(title) 219 .write("</title>\n") 220 .write("</head>\n") 221 .write("<body>\n") 222 .write("<p>" ~ content ~ "</p>\n") 223 .write("<hr/>\n") 224 .write("</body>\n") 225 .end("</html>\n"); 226 } 227 228 private void handleRequestFile(RoutingContext context, string requestFile) { 229 230 if(cachable() && cacheTime() > Duration.zero()) { 231 context.responseHeader(HttpHeader.CACHE_CONTROL, 232 "public, max-age=" ~ cacheTime().total!(TimeUnit.Second).to!string()); 233 234 auto expireTime = Clock.currTime(UTC()) + cacheTime(); 235 context.responseHeader(HttpHeader.EXPIRES, CommonUtil.toRFC822DateTimeString(expireTime)); 236 } 237 238 // 239 string mime = _mimetypes.getMimeByExtension(requestFile); 240 version(HUNT_HTTP_DEBUG) infof("MIME type: %s for %s", mime, requestFile); 241 if(!mime.empty()) { 242 context.getResponseHeaders().put(HttpHeader.CONTENT_TYPE, mime); 243 244 if(mime == "application/json") { 245 RoutingHandlerUtils.renderFileContent(context, requestFile, bufferSize()); 246 } else { 247 AcceptMimeType[] acceptMimes = MimeTypeUtils.parseAcceptMIMETypes(mime); 248 if(acceptMimes.length > 0 && acceptMimes[0].getParentType() == "text") { 249 // show the content of file 250 RoutingHandlerUtils.renderFileContent(context, requestFile, bufferSize()); 251 } else { 252 RoutingHandlerUtils.downloadFile(context, requestFile); 253 } 254 } 255 context.end(); 256 context.succeed(true); 257 } else { 258 context.getResponseHeaders().put(HttpHeader.CONTENT_TYPE, MimeType.APPLICATION_OCTET_STREAM_VALUE); 259 RoutingHandlerUtils.downloadFile(context, requestFile); 260 context.end(); 261 context.succeed(true); 262 } 263 } 264 265 static string convertFileSize(ulong size) { 266 if(size < 1024) { 267 return size.to!string(); 268 } else if(size < 1024*1024) { 269 return format!("%d KB")(size/1024); 270 } else if(size < 1024*1024*1024) { 271 return format!("%d MB")(size/(1024*1024)); 272 } else if(size < 1024*1024*1024*1024) { 273 return format!("%d GB")(size/(1024*1024*1024)); 274 } else { 275 return format!("%d TB")(size/(1024*1024*1024*1024)); 276 } 277 } 278 279 static string renderFileList(string basePath, string requestFile, string title) { 280 Appender!string sb; 281 sb.put(format!("<h1>%s</h1><hr>\n")(title)); 282 sb.put("<table id='thetable' style='width: 800px;border-collapse: collapse;'>\n"); 283 sb.put("<thead>\n"); 284 sb.put("<tr><th width='auto'>Name</th><th width='200px'>Last Modified</th><th width='60px'>Size</th></tr>\n"); 285 sb.put("</thead>\n"); 286 287 sb.put("<tbody>\n"); 288 289 if(basePath != requestFile) { 290 sb.put("<tr><td><a href='../'>../</a></td><td> </td><td> </td></tr>\n"); 291 } 292 293 foreach(DirEntry file; dirEntries(requestFile, SpanMode.shallow)) { 294 string fileName = baseName(file.name); 295 string fileTime = (cast(DateTime)(file.timeLastModified)).toSimpleString(); 296 if(file.isDir()) { 297 string item = `<tr><td><a href='%1$s/'>%1$s/</a></td><td>%2$s</td><td>--</td></tr>`; 298 sb.put(format(item, fileName, fileTime)); 299 sb.put("\n"); 300 } else { 301 string item = `<tr><td><a href='%1$s'>%1$s</a></td><td>%2$s</td><td>%3$s</td></tr>`; 302 sb.put(format(item, fileName, fileTime, convertFileSize(file.size))); 303 sb.put("\n"); 304 } 305 } 306 sb.put("</tbody>\n"); 307 sb.put("</table>\n"); 308 // sb.put("<tfoot>\n"); 309 // sb.put("<tr><th class='loc footer' colspan='3'>Powered by Hunt-HTTP</th></tr>\n"); 310 // sb.put("</tfoot>\n"); 311 312 return sb.data; 313 } 314 }