[tbnl-devel] Serving static files

Bob Hutchison hutch at recursive.ca
Thu Jul 14 14:38:20 UTC 2005


Hi,

I have implemented a handler for serving static files using TBNL. I 
ended up doing this differently than I expected. I extended 
create-prefix-dispatcher and called it create-prefix-dispatcher/2 and a 
little macro, prefix-dispatcher/2, to simplify things a bit. 
create-prefix-dispatcher/2 does everything that 
create-prefix-dispatcher does (in the same way) and is intended to be a 
drop-in replacement.

create-prefix-dispatcher/2 looks at its page-function argument and 
handles symbols exactly as create-prefix-dispatcher did, but cons are 
handled differently.

When create-prefix-dispatcher/2 encounters a page function that is a 
cons it assumes that it is a lambda with one argument that returns a 
lambda of no arguments. create-prefix-dispatcher/2 uses gensym to 
generate a symbol for the function then setfs its symbol-function to 
the lambda returned by (funcall (eval page-function) prefix). The 
generated symbol is then handled exactly as though that symbol was 
passed in as page-function. This seems a bit awkward to me, but it 
works.

The static-directory/*-handler takes 4 arguments:
prefix -- the url prefix that matched the script-name. When this is 
removed from the
           front of the script-name, we are left with the path relative 
to the
           directory-path of the file requested.
directory-path -- the root directory of the files to be served
default-type -- the default mime-type (can be nil)
file-type-map -- an assoc list of file name extensions (e.g. ".gif") 
and a mime type.

If the content-type cannot be determined it is not set (maybe not the 
best idea, but...)

I've not been able to test this on anything other that LWM using OS/X 
10.3.9 -- so only tbnl-bivalent-streams has been tested at all.

Sorry, the documentation is a bit weak.

There is an example near the end of how to use this stuff.

Hope somebody finds this useful.

Cheers,
Bob

-------

(defun static-directory/*-handler (prefix directory-path default-type 
file-type-map)
"A TBNL handler that will serve static files located relative
to a directory.

'prefix' is what TBNL matched to the script-name (this match provided
the excuse to call this handler). If we remove the prefix from the
front of the script-name we get the path, relative to 'directory-path',
that identifies the file.

'default-type' is the default mime-type for the file, nil is okay.

'file-type-map' is an assoc list of file extensions and mime types.
"
   (labels ((determine-content-type (relative-file-path)
              (or (cdr (find-if (lambda (pair) (zerop (mismatch (car 
pair) relative-file-path :from-end t)))
                                file-type-map))
                  default-type)))
     (let* ((script-name (script-name))
            (relative-file-path (subseq script-name (mismatch prefix 
script-name)))
            (path (concatenate 'string directory-path 
relative-file-path))
            (time (or (file-write-date path)
                      (get-universal-time)))
            (content-type (determine-content-type relative-file-path)))
       (when content-type (setf (content-type) content-type))
       (unless (probe-file path)
         (setf (return-code) +http-not-found+)
         (throw 'tbnl-handler-done nil))
       #+:tbnl-bivalent-streams
       (progn
         (handle-if-modified-since time)
         (with-open-file (file path
                               :direction :input
                               :element-type '(unsigned-byte 8)
                               :if-does-not-exist nil)
           (let* ((len (file-length file))
                  (buf (make-array len :element-type '(unsigned-byte 
8))))
             (read-sequence buf file)
             (setf (header-out "Last-Modified") (rfc-1123-date time))
             buf)))
       #-:tbnl-bivalent-streams
       (let ((buf (make-array 8192
                              :element-type 'character)))
         (handle-if-modified-since time)
         (let ((str
                (with-output-to-string (out)
                  (with-open-file (file path
                                        :direction :input
                                        :if-does-not-exist nil)
                     (loop for pos = (read-sequence buf file)
                          until (zerop pos)
                          do (write-sequence buf out :end pos))))))
           (setf (header-out "Last-Modified") (rfc-1123-date time))
           str)))))

(defmacro prefix-dispatcher/2 (fn &rest args)
   "construction a function with one argument, prefix, that
returns a function of no arguments that calls fn with prefix
as the first argument followed by the args."
   `(lambda (prefix)
      (lambda ()
        (,fn prefix , at args))))

(defun create-prefix-dispatcher/2 (prefix page-function)
   "Creates a dispatch function which will dispatch to the
function denoted by PAGE-FUNCTION if the file name of the current
request starts with the string PREFIX. This is exactly what
create-prefix-dispatcher does. However, if page-function is a cons,
then it must be of the form:
     (lambda (prefix) (lambda () ...))
This lambda serves as the page function."

   (when (consp page-function)
     (let ((fn (gensym "handler"))
           (fv (funcall (eval page-function) prefix)))
       (setf (symbol-function fn) fv)
       (setf page-function fn)))
   (lambda (request)
     (let ((mismatch (mismatch (script-name request) prefix
                               :test #'char=)))
       (and (or (null mismatch)
                (>= mismatch (length prefix)))
            page-function))))

(setq *dispatch-table*
       (nconc
        (mapcar (lambda (args)
                  (apply #'create-prefix-dispatcher/2 args))
                '(("/sienna/image/" (prefix-dispatcher/2 
static-directory/*-handler
                                                         "images/"
                                                         nil
                                                         '((".jpg" . 
"image/jpeg")
                                                           (".jpeg" . 
"image/jpeg")
                                                           (".gif" . 
"image/gif"))))
                  ("/sienna/images/" (prefix-dispatcher/2 
static-directory/*-handler
                                                          "images/"
                                                          nil
                                                          '((".jpg" . 
"image/jpeg")
                                                            (".jpeg" . 
"image/jpeg")
                                                            (".gif" . 
"image/gif"))))
                  ("/sienna/css/" (prefix-dispatcher/2 
static-directory/*-handler
                                                       "css/"
                                                       nil
                                                       '((".js" . 
"text/javascript")
                                                         (".css" . 
"text/css")
                                                         (".gif" . 
"image/gif"))))
                  ("/sienna/item/" show-item)))
        (list #'default-dispatcher)))




More information about the Tbnl-devel mailing list