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>&nbsp;</td><td>&nbsp;</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 }