logo

class

web::FileWeblet

sys::Obj
  web::Weblet
    web::FileWeblet
   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  }