
1 // 2 // Copyright (c) 2007, Brian Frank and Andy Frank 3 // Licensed under the Academic Free License version 3.0 4 // 5 // History: 6 // 28 Jul 07 Brian Frank Creation 7 // 8 9 ** 10 ** FileWeblet is used to service an HTTP request on a `sys::File`. 11 ** It handles all the dirty details for cache control, modification 12 ** time, ETags, etc. 13 ** 14 ** Current implementation supports ETags and Modification time 15 ** for cache validation. It does not specify any cache control 16 ** directives. 17 ** 18 class FileWeblet : Weblet 19 { 20 21 ////////////////////////////////////////////////////////////////////////// 22 // Access 23 ////////////////////////////////////////////////////////////////////////// 24 25 ** 26 ** The file being serviced by this FileWeblet (passed in constructor). 27 ** 28 readonly File file 29 30 ** 31 ** Get the modified time of the file floored to 1 second 32 ** which is the most precise that HTTP can deal with. 33 ** 34 virtual DateTime modified() 35 { 36 return file.modified.floor(1sec) 37 } 38 39 ** 40 ** Compute the ETag for the file being serviced which uniquely 41 ** identifies the file version. The default implementation is 42 ** a hash of the modified time and the file size. The result 43 ** of this method must conform to the ETag syntax and be 44 ** wrapped in quotes. 45 ** 46 virtual Str etag() 47 { 48 return "\"" + file.size.toHex + "-" + file.modified.ticks.toHex + "\"" 49 } 50 51 ////////////////////////////////////////////////////////////////////////// 52 // Weblet 53 ////////////////////////////////////////////////////////////////////////// 54 55 ** 56 ** Handle GET request for the file. 57 ** 58 override Void service() 59 { 60 this.file = (File)req.resource 61 if (this.file.isDir) throw Err.make("FileWeblet cannot process dir") 62 super.service 63 } 64 65 ** 66 ** Handle GET request for the file. 67 ** 68 override Void get() 69 { 70 // set identity headers 71 res.headers["ETag"] = etag 72 res.headers["Last-Modified"] = modified.toHttpStr 73 74 // check if we can return a 304 not modified 75 if (checkNotModified) return 76 77 // service a normal 200 78 res.statusCode = 200 79 mime := extToMime[file.ext] 80 if (mime != null) res.headers["Content-Type"] = mime 81 res.headers["Content-Length"] = file.size.toStr 82 file.in.pipe(res.out, file.size) 83 } 84 85 ** 86 ** Check if the request passed headers indicating it has 87 ** cached version of the file. If the file has not been 88 ** modified, then service the request as 304 and return 89 ** true. This method supports ETag "If-None-Match" and 90 ** "If-Modified-Since" modification time. 91 ** 92 virtual protected Bool checkNotModified() 93 { 94 // check If-Match-None 95 matchNone := req.headers["If-None-Match"] 96 if (matchNone != null) 97 { 98 etag := this.etag 99 match := WebUtil.parseList(matchNone).any |Str s->Bool| 100 { 101 return s == etag || s == "*" 102 } 103 if (match) 104 { 105 res.statusCode = 304 106 return true 107 } 108 } 109 110 // check If-Modified-Since 111 since := req.headers["If-Modified-Since"] 112 if (since != null) 113 { 114 sinceTime := DateTime.fromHttpStr(since, false) 115 if (modified == sinceTime) 116 { 117 res.statusCode = 304 118 return true 119 } 120 } 121 122 // gotta do it the hard way 123 return false 124 } 125 126 ////////////////////////////////////////////////////////////////////////// 127 // MIME 128 ////////////////////////////////////////////////////////////////////////// 129 130 ** 131 ** This is a static map of file extension to MIME type. 132 ** Right now it is based on a simple props file stored 133 ** in 'lib/ext2mime.props', but eventually needs to be 134 ** reworked as we define more of the architecture. 135 ** 136 const static Str:Str extToMime 137 static 138 { 139 Str:Str props 140 try 141 { 142 props = (Sys.homeDir + `lib/ext2mime.props`).readProps 143 } 144 catch (Err e) 145 { 146 echo("ERROR: Cannot read ext2mime.props") 147 echo(" $e") 148 props = Str:Str[:] 149 } 150 extToMime = props.toImmutable 151 } 152 }