Abstract
CL-WEBDAV is a WebDAV server written in Common Lisp. It aims to be as flexible as possible, allowing you to completely customize the way resources are handled.The code comes with a BSD-style license so you can basically do with it whatever you want.
Download shortcut: http://weitz.de/files/cl-webdav.tar.gz.
The current development version of CL-WEBDAV can be found at GitHub, https://github.com/edicl/cl-webdav. This is the one to send patches against. Use at your own risk.
An
unofficial Mercurial
repository of older versions is available
at http://arcanes.fr.eu.org/~pierre/2007/02/weitz/
thanks to Pierre Thierry.
CL-USER 1 > (push (dav:create-dav-dispatcher 'dav:file-resource) tbnl:*dispatch-table*) (#<Closure ((METHOD CL-WEBDAV:CREATE-DAV-DISPATCHER (SYMBOL)) . 1) 200B8992> HUNCHENTOOT:DEFAULT-DISPATCHER) CL-USER 2 > (tbnl:start-server :port 4242) CL-USER 2 > (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242)) #<HUNCHENTOOT::SERVER 200BCBCF>You should now be able to connect to
http://localhost:4242/
using a DAV client
like cadaver and you
should see the files in the /tmp/
directory.
If you want to send patches, please read this first.
I ended up not using CL-WEBDAV for the tutorial, though, as I came to the conclusion that there was too much stuff in it that would distract from the actual subject. But as it was more or less complete, I invested some more time for testing, polishing, cleanup, and documentation, and then released it.
The initial release has been tested mildly with LispWorks on Windows and with SBCL on Linux against cadaver, WebDrive, and the native Windows XP client. I currently (April 2007) don't use CL-WEBDAV myself, so I can't say much about its usefulness, but here's a list of potential pitfalls that come to mind:
RESOURCE-CREATION-DATE
method for file resources, but it can certainly
be done using implementation-specific functionality. I'll happily
accept patches for this.
FILE-RESOURCE
class doesn't use Common Lisp's
pathname
algebra but rather deals with strings for obvious reasons.
Again, I'll gladly
accept patches to make
this more elegant.
RESOURCE
and then calls
generic functions on these object to actually manipulate the resources
represented by these objects. So, in order to implement your own
WebDAV server, you are supposed to
subclass RESOURCE
and implement a
couple of methods. CL-WEBDAV comes with
two example classes which
demonstrate how this can be done.
[Standard class]
resource
This is the base class you'll have to subclass if you want to create your own custom WebDAV server. Each object of this class represents one resource on the server and most of the time these objects are created by the server using only the:SCRIPT-NAME
initarg. If you need more initialization to happen, write an:AFTER
method forINITIALIZE-INSTANCE
.See the file
file-resources.lisp
for an example of a subclass ofRESOURCE
.
[Generic accessor]
resource-script-name resource => script-name
(setf (resource-script-name resource) script-name)
The base classRESOURCE
has only one slot which can be manipulated with this accessor. If a resource is created by the server, then it will fill this slot with the script name that was used to access the resource. In this case you should only read the slot's value.If you create your own
RESOURCE
objects (for example inRESOURCE-CHILDREN
), you should set the value of this slot to a string that could be used as a script name to retrieve the resource. There are several places where CL-WEBDAV is calling this function internally, so it is important that you use a meaningful value.
[Special variable]
*resource-class*
Whenever a DAV handler is executed, this variable should be bound to the resource class which is to be used. If you're usingCREATE-DAV-DISPATCHER
, this will already be taken care of for you, so you can ignore this variable.
FILE-RESOURCE
where
this work is already done for you.
[Generic function]
resource-exists resource => generalized-boolean
This function must return a true value if the resourceresource
exists on the server andNIL
otherwise. You must specialize this generic function for your own classes.
[Generic function]
resource-children resource => children
This function must return a list of all children ofresource
(which themselves areRESOURCE
objects). You must specialize this generic function for your own classes.
[Generic function]
resource-parent resource => parent
This function must return aRESOURCE
object which is the parent resource ofresource
orNIL
if there is no parent. You must specialize this generic function for your own classes.
[Generic function]
resource-collection-p resource => generalized-boolean
This function must return a true value iff the resourceresource
is a collection. You must specialize this generic function for your own classes.
[Generic function]
resource-write-date resource => universal-time
This function must return a universal time denoting the time the resourceresource
was last modified. You must specialize this generic function for your own classes.
[Generic function]
resource-length resource => length
This function must return an integer denoting the length of the resourceresource
in octets. You must specialize this generic function for your own classes.
[Generic function]
resource-display-name resource => display-name
This function must return a string which, according to the WebDAV RFC, "provides a name for the resource that is suitable for presentation to a user." You must specialize this generic function for your own classes.
[Generic function]
send-content resource stream => whatever
This function is called for GET requests and must send the complete contents of the (non-collection) resourceresource
to the (flexi) streamstream
. The return value is irrelevant.
[Generic function]
get-content resource stream length => whatever
This function is called for PUT requests and must readlength
octets of data from the (flexi) streamstream
and store them in a place appropriate for the resourceresource
. The return value is irrelevant.
[Generic function]
remove-resource resource => whatever
This function must completely remove the resourceresource
. It doesn't have to deal with dead properties, and it can assume thatresource
doesn't have children in case it's a collection. The return value is irrelevant.
[Generic function]
move-resource source destination => whatever
This function must "move" the (contents of the) resourcesource
in such a way that it can in the future be accessed asdestination
. It doesn't have to deal with dead properties, and it can assume thatsource
doesn't have children in case it's a collection. The return value is irrelevant.
[Generic function]
copy-resource source destination => whatever
This function must "copy" the (contents of the) resourcesource
in such a way that the copy can in the future be accessed asdestination
. It doesn't have to deal with dead properties, and it can assume thatsource
doesn't have children in case it's a collection. The return value is irrelevant.
[Generic function]
create-collection resource => whatever
This function must create a collection resource that in the future can be accessed asresource
. The return value is irrelevant.
[Generic function]
accept-request-p resource-class request => generalized-boolean
This must be a function which accepts a Hunchentoot request objectrequest
and returns a generalized boolean denoting whetherrequest
is a resource the DAV server wants to handle. It will be called by the dispatcher created withCREATE-DAV-DISPATCHER
. Usually, you'll want to look at the script name of the request or something like that - see the classFILE-RESOURCE
for an example.Note that you specialize this function on the resource class (or its name) and not on the resource like the other functions in this subsection.
[Generic function]
resource-creation-date resource => universal-time
This function must return a universal time denoting the time the resourceresource
was created. There's a default method which returnsRESOURCE-WRITE-DATE
, but most likely you'll want to specialize this for you own classes and return a more meaningful value.
[Generic function]
resource-content-type resource => type-string
This function must return a string denoting the MIME type of the resourceresource
. It will only be called ifresource
is not a collection. There's a default method which always returns"application/octet-stream"
, but most likely you'll want to specialize this for your own classes.
[Generic function]
resource-content-language resource => language
This function should return eitherNIL
or a language tag as defined in section 14.13 of RFC 2068. If the value returned by this function is notNIL
, it will also be used as theContent-Language
header returned for GET requests. There's a default method which always returnsNIL
.
[Generic function]
resource-source resource => xmls-node
This function should return eitherNIL
or a DAV "source" XML node (structured as an XMLS node) that, according to the WebDAV RFC, "identifies the resource that contains the unprocessed source of the link's source." There's a default method which always returnsNIL
.
[Generic function]
resource-etag resource => etag
This function should return an ETag for the resourceresource
orNIL
. If the value returned by this function is notNIL
, it will also be used as theETag
header returned for GET requests. There's a default method which synthesizes a value based on the script name and the write date of the resource, and in most cases you probably don't need to specialize this function.
[Generic function]
resource-type resource => xmls-node
This function should return either NIL or a DAV "resourcetype" XML node (structured as an XMLS node) that, according to the WebDAV RFC, "specifies the nature of the resource." There's a default method which returns something fitting for collections andNIL
otherwise, and in most cases you probably don't need to specialize this function.
[Generic function]
resource-uri-prefix resource => prefix-string
This function must return a string which is the part of a resource's HTTPS or HTTPS URI that comprises the scheme, the host, and the port and ends with a slash - something like"http://localhost:4242/"
or"https://www.lisp.org/"
.The default method synthesizes this from the information Hunchentoot provides and usually you only have to write your own method if you're sitting behind a proxy.
RESOURCE-TYPE
you'll need to handle XML
data. You can probably get away without it, but in case you need them,
this section collects the relevant CL-WEBDAV functions to manipulate
XML.
We're representing XML as XMLS nodes which are very similar to CXML's XMLS nodes but try to get namespaces right because they don't purport to be compatible with XMLS.
[Function]
xmls-node-p thing => generalized-boolean
Checks whetherthing
is an XMLS node.
[Function]
local-name thing => local-name
Returns the local name of the XMLS node or attributething
.
[Function]
namespace-uri thing => namespace-uri
Returns the namespace URI (which can beNIL
) of the XMLS node or attributething
.
[Accessor]
node-attributes xmls-node => attributes
(setf (node-attributes xmls-node) attributes)
Returns or sets the list of attributes of the XMLS nodexmls-node
.
[Accessor]
node-children xmls-node => children
(setf (node-children xmls-node) children)
Returns or sets the list of children of the XMLS nodexmls-node
.
[Function]
dav-node local-name &rest children => xmls-node
Returns an XMLS node with the local namelocal-name
, the namespace URI"DAV:"
, and the childrenchildren
(a list of XMLS nodes and/or strings).
[Function]
parse-dav octets &optional root-name => xmls-node
Accepts an arrayoctets
of octets representing a DAV XML node and converts it into the corresponding XMLS node. According to the WebDAV RFC, non-DAV elements are skipped unless they appear in positions (like in a "prop" element) where arbitrary elements are allowed. Ifroot-name
is given, it should be the local name (a string) of a DAV node. In this case, the XML is validated. This function is expected to be called from within a Hunchentoot request and callsABORT-REQUEST-HANDLER
with a return code of+HTTP-BAD-REQUEST+
if a parsing error occurs or if the XML is invalid.This is kind of the inverse operation to
SERIALIZE-XMLS-NODE
.
[Function]
serialize-xmls-node xmls-node => octet-vector
Serializesxmls-node
to a vector of octets which is returned.This is kind of the inverse operation to
PARSE-DAV
and very similar toCXML-XMLS:MAP-NODE
.
[Generic function]
get-dead-properties resource => property-list
This function must return all dead properties of the resourceresource
as a list of XML elements structured as XMLS nodes. There's a default method but you should definitely specialize this for production servers.
[Generic function]
remove-dead-property resource property => whatever
This function must remove the currently stored dead property designated byproperty
(an XMLS node) of the resourceresource
. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.
[Generic function]
set-dead-property resource property => whatever
This function must replace the currently stored dead property designated byproperty
(an XMLS node) of the resourceresource
withproperty
, i.e.property
doubles as the property itself and as the property designator. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.
[Generic function]
remove-dead-properties resource => whatever
This function must remove all dead properties of the resourceresource
. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.
[Generic function]
move-dead-properties source destination => whatever
This function must move all dead properties of the resourcesource
to the resourcedestination
. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.
[Generic function]
copy-dead-properties source destination => whatever
This function must copy all dead properties of the resourcesource
to the resourcedestination
. There's a default method but you should definitely specialize this for production servers. The return value is irrelevant.
CREATE-DAV-DISPATCHER
is for. The other functions and variables in this section you'll
rarely need, if ever.
[Generic function]
create-dav-dispatcher resource-class &optional ms-workaround-p => dispatcher
Creates and returns a dispatcher for the classresource-class
which must be a subclass ofRESOURCE
. Ifms-workaround-p
is true (which is the default), OPTIONS requests are always handled irrespective of the results ofACCEPT-REQUEST-P
- this is needed to work around problems with some Microsoft DAV clients.
[Function]
options-handler => nil
The handler for OPTIONS requests. Output is basically determined by*ALLOWED-METHODS*
and*DAV-COMPLIANCE-CLASSES*
.
[Function]
options-dispatcher request => handler
A dispatcher which'll dispatch toOPTIONS-HANDLER
in case of an OPTIONS request and decline otherwise. This is only useful if you want to cater to Microsoft DAV clients which always unconditionally send OPTIONS requests to the"/"
root resource. Sigh...If you use
CREATE-DAV-DISPATCHER
with a true value forms-workaround-p
, you don't need this dispatcher.
[Special variable]
*allowed-methods*
The list of methods (as keywords) returned by theAllow
header in case of OPTIONS requests (and also utilized by the handler for MKCOL). The initial value is the list(:options :get :head :delete :propfind :proppatch :put :copy :move :mkcol)Can be adapted to allow for more methods, but for a WebDAV server at least the methods above should be listed.
[Special variable]
*dav-compliance-classes*
A sorted list of DAV compliance classes reported in theDAV
header when answering OPTIONS requests. It doesn't make much sense to have more then class 1 in here as long as there's no lock support, i.e. the initial value is the list(1)
.
FILE-RESOURCE
maps
URIs in a straightforward way to (a subtree of) your local file
system. This is probably the most common way to serve DAV resources,
and if you want to do it like this, you can
subclass FILE-RESOURCE
and
adapt it without the need to
implement loads of methods. Of course, you
can go wild and store resources in a database, or generate them
dynamically, or whatever, but then this class is not for you and you
have to invent your own subclass
of RESOURCE
.
Again, this is not meant to be ready for a production system. You should at least make sure that properties are persisted.
[Standard class]
file-resource
A subclass ofRESOURCE
representing resources which are mapped to a subtree of the local file system.
[Generic function]
file-resource-base-path-namestring resource-class => namestring
This generic function is called for subclasses ofFILE-RESOURCE
to determine the base pathname that's currently being used, i.e. the part of the filesystem where the files served by the DAV server are stored. The function must return the namestring of the truename of an absolute pathname denoting a directory, specifically it must return a string starting and ending with slashes. (Note: This should work on Windows as well.) You can specialize this function (either on the class or on the name of the class) if you want.The default method returns the current value of
*FILE-RESOURCE-BASE-PATH-NAMESTRING*
.
[Special variable]
*file-resource-base-path-namestring*
The value of this variable is the return value of the default method forFILE-RESOURCE-BASE-PATH-NAMESTRING
. It should be the namestring of the truename of an absolute pathname denoting a directory, specifically it must return a string starting and ending with slashes. (Note: This should work on Windows as well.)The initial value is the result of calling
(namestring (truename (ensure-directories-exist "/tmp/")))meaning that this directory will be created on your machine at load time if it doesn't exist (which is possible if you're on Windows and pretty unlikely on Linux or Unix).
[Generic function]
file-resource-base-uri resource-class => uri
This generic function is called for subclasses ofFILE-RESOURCE
to determine the base URI that's currently being used, i.e. the prefix the script name of a resource's URI must have in order to be valid. (In other words: this URI represents the top-level collection of the DAV server.) The function must return a string which starts with a slash if it's not empty and does not end with a slash and is not URL-encoded. You can specialize this function (either on the class or on the name of the class) if you want.The default method returns the current value of
*FILE-RESOURCE-BASE-URI*
.
[Special variable]
*file-resource-base-uri*
The value of this variable is the return value of the default method forFILE-RESOURCE-BASE-URI
. It should be a string which starts with a slash if it's not empty and does not end with a slash and is not URL-encoded.The initial value is
""
(the empty string).
[Standard class]
authorized-file-resource
A subclass ofFILE-RESOURCE
representing file resources which are associated with a certain user.
*allowed-methods*
*dav-compliance-classes*
*file-resource-base-path-namestring*
*file-resource-base-uri*
*resource-class*
accept-request-p
authorized-file-resource
copy-dead-properties
copy-resource
create-collection
create-dav-dispatcher
dav-node
file-resource
file-resource-base-path-namestring
file-resource-base-uri
get-content
get-dead-properties
iso-8601-date
local-name
move-dead-properties
move-resource
namespace-uri
node-attributes
node-children
options-dispatcher
options-handler
parse-dav
remove-dead-properties
remove-dead-property
remove-resource
resource
resource-children
resource-collection-p
resource-content-language
resource-content-type
resource-creation-date
resource-display-name
resource-etag
resource-exists
resource-length
resource-parent
resource-script-name
resource-source
resource-type
resource-uri-prefix
resource-write-date
send-content
serialize-xmls-node
set-dead-property
xmls-node-p
This documentation was prepared with DOCUMENTATION-TEMPLATE.
$Header: /usr/local/cvsrep/cl-webdav/doc/index.html,v 1.20 2009/02/17 20:17:30 edi Exp $