leibowitz
Experimental Common Lisp object storage abstraction for Unix file systems
leibowitz/web/routes.lisp
Download raw file: web/routes.lisp
;;; HTTP endpoint handlers
(in-package :leibowitz.web)
(static-resource stylesheet "/static/style.css")
(static-resource script "/static/fluff.js")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Top-level pages
(leibowitz-route (index-page lib "/") (limit offset sort-by direction view)
;; FIXME: `list-data' is the single-source-of-truth when it comes to
;; validating its parameters; it would
(let ((limit (if limit (parse-integer limit) 50))
(offset (if offset (parse-integer offset) 0))
(sort-by (if sort-by (intern (string-upcase sort-by) :keyword)
:modified))
(direction (if direction (intern (string-upcase direction) :keyword)
:descending))
(view (if view (intern (string-upcase view) :keyword)
:tile)))
(make-page lib
:here "/"
:title "All | Leibowitz Web"
:sidebar (make-datum-listing-sidebar lib)
:body (list-data-as-html lib view
:sort-by sort-by
:direction direction
:limit limit
:offset offset)
:total (library-data-quantity lib)
:limit limit :offset offset)))
(leibowitz-route (tags-page lib "/tags") ()
(make-page lib
:here "/tags"
:title "Tags | Leibowitz Web"
:sidebar `((:section ""))
:body (list-tags-as-html lib)))
(leibowitz-route (tree-page lib "/tree") (dir view sort-by direction)
(let ((dir (if dir dir (user-homedir-pathname)))
;; FIXME: Standardize the parsing of these somewhere!
(sort-by (if sort-by (intern (string-upcase sort-by) :keyword)
:modified))
(direction (if direction (intern (string-upcase direction) :keyword)
:descending))
(view (if view (intern (string-upcase view) :keyword)
:tile)))
(if (and (uiop:directory-exists-p dir)
(uiop:absolute-pathname-p (uiop:parse-unix-namestring dir)))
(make-page lib
:here "/tree"
:title (format NIL "~A | Leibowitz Web" dir)
:header (make-tree-breadcrumbs "Tree | Leibowitz Web" dir)
:sidebar (make-tree-sidebar dir)
:body (nconc
(list-data-as-html lib view :dir dir :sort-by sort-by
:direction direction)
(make-tree-unindexed-section lib dir)))
(return-404 lib (format NIL "Directory ~S does not exist" dir)))))
;; FIXME: parameter validations hould be in the core, just parse here
(leibowitz-route (search-page lib "/search") (q limit offset sort-by direction view)
(let ((limit (if limit (parse-integer limit) 50))
(offset (if offset (parse-integer offset) 0))
(sort-by (if sort-by (intern (string-upcase sort-by) :keyword)
:modified))
(direction (if direction (intern (string-upcase direction) :keyword)
:descending))
(view (if view (intern (string-upcase view) :keyword)
:tile)))
(make-page lib
:here "/search"
:title "Search | Leibowitz Web"
:sidebar `((:section ""))
:body (if q
(list-search-results-as-html
lib q limit offset sort-by direction view)
`(,(make-search-page-search-box lib)))
;; FIXME; :total here is the number of results,
;; figure out a way to get that or perhaps replace
;; numeric pagination with a more button. As a
;; marginally acceptable but inaccurate placeholder
;; we'll go with the number of data
:total (library-data-quantity lib)
:limit limit :offset offset)))
(leibowitz-route (type-view-page lib "/type/:major/:minor")
(limit offset sort-by direction view)
(if (and major minor)
(let ((type (format NIL "~A/~A" major minor))
(limit (if limit (parse-integer limit) 50))
(offset (if offset (parse-integer offset) 0))
(sort-by (if sort-by (intern (string-upcase sort-by) :keyword)
:modified))
(direction (if direction (intern (string-upcase direction) :keyword)
:descending))
(view (if view (intern (string-upcase view) :keyword)
:tile)))
(make-page lib
:here (format NIL "/type/~A" type)
:title (format NIL "List of ~A files | Leibowitz Web" type)
:sidebar `((:section ""))
:body (list-data-as-html lib view
:sort-by sort-by
:direction direction
:limit limit
:offset offset
:type type)
:total (let ((total (find-if (lambda (c) (equal (car c) type))
(library-all-file-types lib))))
(if total (cdr total) 0))
:limit limit
:offset offset))
(return-404 lib (format NIL "Invalid mime type requested: ~A/~A" major minor))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Uploading data
(leibowitz-route (new-page lib "/new") ()
(make-page lib
:here "new"
:title "Upload a New Datum | Leibowitz Web"
:body `((:section :class "upload-form"
(:form :method "post" :action "/new"
:enctype "multipart/form-data"
(:fieldset
(:legend "Upload A File")
(:input :type "file" :name "file")
(:button "Upload")))))))
(leibowitz-route (new-datum lib ("/new" :method :post)) ()
(let ((file (hunchentoot:post-parameter "file")))
(if file
(let* ((temp (nth 0 file))
(name (nth 1 file))
;; FIXME: Upload directory needs to be something
;; user-configurable that makes sense in the context of
;; collections. As-is this breaks when the current working
;; directory is not a subdirectory of library's :homedir
;; initarg (passed to collection-homedir). While we're on
;; that topic, the collection API feels simultaneously
;; half-baked (what's even doing??) and overengineered
;; (whyyyy God are there so many things to keep track of)
(dest (merge-pathnames (uiop:parse-unix-namestring name))))
(if (probe-file dest)
;; FIXME: create a conflict-resolution page that should also
;; be used for copying and moving files.
(format NIL "You uploaded ~S, but ~S already exists!" name dest)
(progn
(uiop:copy-file temp dest)
(let ((datum (car (index lib dest))))
(hunchentoot:redirect (format NIL "/datum?id=~A"
(datum-id datum)))))))
(hunchentoot:redirect "/new"))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Datum view machinery
(leibowitz-route (datum-view lib "/datum") (id)
(if id
(handler-case
(let ((d (get-datum lib id :error T)))
(make-page lib
:title (format NIL "~A | Leibowitz Web" (datum-title d))
:header (make-tree-breadcrumbs (datum-title d)
(uiop:parse-unix-namestring id))
:sidebar (datum-html-sidebar lib d)
:body (make-datum-view-page lib d)))
(datum-not-indexed ()
(return-404 lib (format NIL "Datum with ID ~S not found" id))))
(hunchentoot:redirect "/")))
(leibowitz-route (datum-raw lib "/raw") (id)
(if id
(let ((d (get-datum lib id)))
(if d
(progn
(setf (hunchentoot:content-type*) (datum-kind d))
(injest-raw-datum d))
(return-404 lib (format NIL "Datum with ID ~S not found" id))))
(hunchentoot:redirect "/")))
(leibowitz-route (datum-thumbnail lib "/thumbnail") (id)
(alx:if-let ((datum (and id (get-datum lib id))))
(handler-case
(let ((thumbnailer:*thumbnail-cache-dir* (library-thumbnail-cache-dir lib)))
(setf (hunchentoot:content-type*) "image/jpeg")
(hunchentoot:handle-static-file
(thumbnailer:get-thumbnail (datum-id datum) (datum-kind datum))))
(thumbnailer:source-file-not-accessible () (return-404 lib))
(thumbnailer:thumbnail-creation-failed () (return-404 lib))
(thumbnailer:unsupported-file-type () (return-404 lib)))
(return-404 lib)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Tag view machinery
(leibowitz-route (tag-view lib "/tag") (name limit offset sort-by direction view)
(if name
(let ((tag (get-tag lib name))
(limit (if limit (parse-integer limit) 50))
(offset (if offset (parse-integer offset) 0))
;; The library uses keywords internally, but here we'll
;; keep these as strings for creating pagination urls
(sort-by (if sort-by sort-by "modified"))
(direction (if direction direction "descending"))
(view (if view view "tile")))
(if tag
(make-page lib
:title (format NIL "~A | Leibowitz Web" name)
:sidebar (make-tag-view-sidebar lib tag)
:body (make-tag-view-page
lib tag (intern (string-upcase view) :keyword)
:tags (list tag)
:sort-by (intern (string-upcase sort-by) :keyword)
:direction (intern (string-upcase direction) :keyword)
:limit limit
:offset offset)
:here "/tag"
:total (tag-count tag)
:limit limit
:offset offset
:more-params (format NIL "name=~A&sort-by=~A&direction=~A&view=~A"
(url name) sort-by direction view))
(return-404 lib (format NIL "Tag named ~S not found" name))))
(hunchentoot:redirect "/tags")))
;; FIXME: for the redirects here, we really should get the full URL so
;; that the user doesn't lose their place
(leibowitz-route (edit-tag lib ("/tag" :method :post)) (name)
(let ((predicates (collect-lines (hunchentoot:post-parameter "tags")))
;; FIXME: Add support for recursive deletion to the core!
;; (delete-children (hunchentoot:post-parameter "delete-children"))
;; (delete-data (hunchentoot:post-parameter "delete-data"))
(move-to (hunchentoot:post-parameter "move-to"))
(copy-to (hunchentoot:post-parameter "copy-to"))
(delete (hunchentoot:post-parameter "delete"))
(desc (hunchentoot:post-parameter "description"))
(ajax (hunchentoot:post-parameter "ajax")))
(handler-case
(cond (move-to
(move-tag lib name move-to :merge T)
(hunchentoot:redirect (format NIL "/tag?name=~A" (url move-to))))
(copy-to
(copy-tag lib name copy-to)
(hunchentoot:redirect (format NIL "/tag?name=~A" (url copy-to))))
(delete
(del-tag lib name)
(hunchentoot:redirect "/tags"))
(desc
(let ((tag (get-tag lib name)))
(setf (tag-label tag) desc)
(add-tag lib tag))
(hunchentoot:redirect (format NIL "/tag?name=~A" (url name))))
(predicates
(add-tag-predicate lib name predicates :replace T)
(if ajax
(html-snippet (make-tag-view-sidebar lib (get-tag lib name)))
(hunchentoot:redirect (format NIL "/tag?name=~A" (url name))))))
(no-such-tag ()
(return-404 lib (format NIL "Tag with NAME ~S not found" name))))))
;;; FIXME: Per the new forms, when :retroactive and :replace are both
;;; passed to `datum-add-predicates', we should also go through and
;;; REMOVE the old tags from all affected data! Wire up the forms!
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Editing data
(leibowitz-route (edit-datum lib ("/datum" :method :post)) (id)
(let ((move-to (hunchentoot:post-parameter "move-to"))
(copy-to (hunchentoot:post-parameter "copy-to"))
(delete (hunchentoot:post-parameter "delete"))
(tags (collect-lines (hunchentoot:post-parameter "tags")))
(ajax (hunchentoot:post-parameter "ajax")))
;; FIXME: some validation would be good, though this endpoint
;; really should only be used internally
(handler-case
;; FIXME: Add conflict resolution page to handle overwrites.
(cond (move-to
(move-datum lib id move-to :overwrite NIL)
(hunchentoot:redirect (format NIL "/datum?id=~A" (url move-to))))
(copy-to
(copy-datum lib id copy-to :overwrite NIL)
(hunchentoot:redirect (format NIL "/datum?id=~A" (url copy-to))))
(delete
(del-datum lib id)
(hunchentoot:redirect "/"))
(tags
(add-datum-tags lib id tags :replace T)
(if ajax
(let ((datum (get-datum lib id :error T)))
(html-snippet (datum-html-sidebar lib datum)))
(hunchentoot:redirect (format NIL "/datum?id=~A" (url id))))))
(datum-not-indexed ()
(return-404 lib (format NIL "Datum with ID ~S not found" id))))))
(leibowitz-route (index-datum lib ("/index" :method :post)) (path)
(if path
(progn
(index lib path)
(hunchentoot:redirect (format NIL "/~A=~A"
(if (uiop:directory-exists-p path)
"tree?dir" "datum?id")
(url path))))
(return-400 lib "You didn't specify what you want me to index")))