Component static-file-handler

You are here: All Systems / araneida / main / static-file-handler

(in-package :araneida)

;;; XXX Should abstract this somehow so that it can be done on things
;;; other than filename suffix

(defvar *content-types*
  '(("html" "text/html")
    ("gif" "image/gif")
    ("jpg" "image/jpeg")
    ("png" "image/png")
    ("css" "text/css")
    ("class" "application/octet-stream")
    ("doc" "application/octet-stream")
    ("zip" "application/octet-stream")
    ("gz" "application/octet-stream")
    ("ASF" "video/x-ms-asf")
    ("tar" "application/octet-stream")
    ("avi" "video/x-msvideo")
    ("txt" "text/plain")
)
)


(defun read-mime-types (filename)
  "Read a standard-format mime.types file and return an alist suitable for
assigning to *content-types*"

  (labels ((chop-comment (string)
                         (subseq string 0 (position #\# string))
)

           (collect-extns (type extns)
                          (loop for e in (split extns)
                                if (> (length e) 0)
                                collect (list e type)
)
)
)

    (with-open-file (in filename :direction :input)
      (let ((eof (gensym)))
        (loop for line = (read-line in nil eof)
              until (eq line eof)
              for (type extns) =  (remove-if-empty
                                   (split (chop-comment line) 2)
)

              append (collect-extns type extns)
)
)
)
)
)


(defun copy-stream (from to)
  "Copy into TO from FROM until end of the input file.  Do not
translate or otherwise maul anything."

; We used to catch sequence type mismatches, but given bivalent streams these days....
 (let ((buf (make-array 4096 :element-type (stream-element-type from))))
    (do ((pos (read-sequence buf from) (read-sequence buf from)))
        ((= 0 pos) nil)      
      (write-sequence buf to :end pos)
)
)
)


;; a host lisp compatibility file can override this to set the
;; appropriate external format for reading in files to send with
;; send-file
(defvar *open-external-format-arguments* nil)

(defun send-file (r file-name &key content-type)
  (let ((stream (request-stream r))
        (content-type
         (or content-type
             (cadr (assoc (or (pathname-type file-name) "txt") *content-types*
                          :test #'string=
)
)
)
)

        (in (apply #'open file-name :direction :input
                   *open-external-format-arguments*
)
)
)

    (unwind-protect
         (progn
           (request-send-headers r :content-type content-type
                                 :conditional t
                                 :content-length (file-length in)
                                 :last-modified (file-write-date in)
)

           (copy-stream in stream)
)

      (close in)
)
)
)


(defclass static-file-handler (handler)
  ((pathname :initarg :pathname :accessor static-file-pathname
             :documentation "Root pathname for URI components to merge against.  Requests may not be made outside this hierarchy"
)

   (default-name :initarg :default-name :accessor static-file-default-name
                 :initform "index.html"
)
)
)


(defmethod handle-request-response
    ((handler static-file-handler) method request)
  ;; chop arg-string into /-delimited components.
 ;; remove .. components along with the component preceding them
 (let* ((path (cons :relative
                     (loop for p on
                           (nreverse (split
                                      (request-unhandled-part request)
                                      nil "/"
)
)

                           if (string-equal (car p) "..")
                           do (setf p (cdr p))
                           else collect (car p) into v
                           finally (return (nreverse v))
)
)
)

         (name (let ((n  (car (last path))))
                 (if (> (length n) 0) n nil)
)
)

         (path (butlast path))
         (dot-pos (and name (position #\. name :from-end t)))
         (extension (and dot-pos (subseq name (1+ dot-pos))))
         (name (urlstring-unescape (if dot-pos  (subseq name 0  dot-pos) name)))
         (file (make-pathname :name name :directory path :type extension))
         (fnam (merge-pathnames file (static-file-pathname handler)))
)

    (when (and (pathname-name fnam)
               (probe-file fnam)
               (not (pathname-name (truename fnam)))
)

      (request-redirect request
                        (concatenate 'string (request-urlstring request) "/")
)

      (return-from handle-request-response t)
)

    (when (not (pathname-name fnam))
      (setf fnam (merge-pathnames fnam (static-file-default-name handler)))
)

    (with-file-error-handlers
        (progn
          (send-file request fnam)
          t
)

      (format nil "Can't read ~S: ~A~%" fnam c)
)
)
)

Lisppaste pastes can be made by anyone at any time. Imagine a fearsomely comprehensive disclaimer of liability. Now fear, comprehensively.