-
Notifications
You must be signed in to change notification settings - Fork 7
/
resources.lisp
411 lines (360 loc) · 18.3 KB
/
resources.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
;;; -*- Mode: LISP; Syntax: COMMON-LISP; Package: CL-WEBDAV; Base: 10 -*-
;;; $Header: /usr/local/cvsrep/cl-webdav/resources.lisp,v 1.12 2007/04/18 19:21:00 edi Exp $
;;; Copyright (c) 2007-2010, Dr. Edmund Weitz. All rights reserved.
;;; Redistribution and use in source and binary forms, with or without
;;; modification, are permitted provided that the following conditions
;;; are met:
;;; * Redistributions of source code must retain the above copyright
;;; notice, this list of conditions and the following disclaimer.
;;; * Redistributions in binary form must reproduce the above
;;; copyright notice, this list of conditions and the following
;;; disclaimer in the documentation and/or other materials
;;; provided with the distribution.
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
(in-package :cl-webdav)
(defclass resource ()
((script-name :initarg :script-name
:accessor resource-script-name
:documentation "This slot holds the script name
\(see HUNCHENTOOT:SCRIPT-NAME) that was used to create the
resource. For objects you create yourself, you must provide a
meaningful value that can be used to access the resource."))
(:documentation "This is the base class you'll have to subclass
if you want to create your own custom DAV 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 for INITIALIZE-INSTANCE.
See the file `file-resources.lisp' for an example of a subclass
of RESOURCE."))
;;; The generic functions which /must/ be specialized
(defgeneric resource-exists (resource)
(:documentation "This function must return a true value if the
resource RESOURCE exists on the server and NIL otherwise. You
must specialize this generic function for your own classes."))
(defgeneric resource-children (resource)
(:documentation "This function must return a list of all
children of RESOURCE \(which themselves are RESOURCE objects).
You must specialize this generic function for your own
classes."))
(defgeneric resource-parent (resource)
(:documentation "This function must return a RESOURCE object
which is the parent resource of RESOURCE or NIL if there is no
parent. You must specialize this generic function for your own
classes."))
(defgeneric resource-collection-p (resource)
(:documentation "This function must return a true value if the
resource RESOURCE is a collection. You must specialize this
generic function for your own classes."))
(defgeneric resource-write-date (resource)
(:documentation "This function must return a universal time
denoting the time the resource RESOURCE was last modified. You
must specialize this generic function for your own classes."))
(defgeneric resource-length (resource)
(:documentation "This function must return an integer denoting
the length of the resource RESOURCE in octets. You must
specialize this generic function for your own classes."))
(defgeneric resource-display-name (resource)
(:documentation "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."))
(defgeneric send-content (resource stream)
(:documentation "This function is called for GET requests and
must send the complete contents of the \(non-collection) resource
RESOURCE to the \(flexi) stream STREAM."))
(defgeneric get-content (resource stream length)
(:documentation "This function is called for PUT requests and
must read LENGTH octets of data from the \(flexi) stream STREAM
and store them in a place appropriate for the resource
RESOURCE."))
(defgeneric remove-resource (resource)
(:documentation "This function must completely remove the
resource RESOURCE. It doesn't have to deal with dead properties,
and it can assume that RESOURCE doesn't have children in case
it's a collection."))
(defgeneric move-resource (source destination)
(:documentation "This function must \"move\" the \(contents of
the) resource SOURCE in such a way that it can in the future be
accessed as DESTINATION. It doesn't have to deal with dead
properties, and it can assume that SOURCE doesn't have children
in case it's a collection."))
(defgeneric copy-resource (source destination)
(:documentation "This function must \"copy\" the \(contents of
the) resource SOURCE in such a way that the copy can in the
future be accessed as DESTINATION. It doesn't have to deal with
dead properties, and it can assume that SOURCE doesn't have
children in case it's a collection."))
(defgeneric create-collection (resource)
(:documentation "This function must create a collection
resource that in the future can be accessed as RESOURCE."))
(defgeneric accept-request-p (resource-class request)
(:documentation "This must be a function which accepts a
Hunchentoot request object REQUEST and returns a generalized
boolean denoting whether REQUEST denotes a resource the DAV
server wants to handle. Usually, you'll want to look at the
script name of the request or something like that - see the class
FILE-RESOURCE for an example.
Note that you specialize this function on the resource /class/
and not on the resource.")
(:method ((resource-class standard-class) script-name)
(accept-request-p (class-name resource-class) script-name)))
;;; The generic functions which have default methods and thus don't
;;; necessarily need to be specialized
(defgeneric resource-creation-date (resource)
(:documentation "This function must return a universal time
denoting the time the resource RESOURCE was created. There's a
default method which returns RESOURCE-WRITE-DATE, but most likely
you'll want to specialize this for you own classes.")
(:method (resource)
(resource-write-date resource)))
(defgeneric resource-content-type (resource)
(:documentation "This function must return a string denoting
the MIME type of the resource RESOURCE. It will only be called
if RESOURCE 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.")
(:method (resource) "application/octet-stream"))
(defgeneric resource-content-language (resource)
(:documentation "This function should return either NIL or a
language tag as defined in section 14.13 of RFC 2068. If the
value returned by this function is not NIL, it will also be used
as the `Content-Language' header returned for GET requests.
There's a default method which always returns NIL.")
(:method (resource)))
(defgeneric resource-source (resource)
(:documentation "This function should return either NIL 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 returns NIL.")
(:method (resource)))
(defgeneric resource-etag (resource)
(:documentation "This function should return an ETag for the
resource RESOURCE or NIL. If the value returned by this function
is not NIL, it will also be used as the `ETag' 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.")
(:method (resource)
(md5-hex (format nil "~A-~A"
(get-last-modified resource)
(resource-script-name resource)))))
(defgeneric resource-type (resource)
(:documentation "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 and NIL otherwise, and in most cases you
probably don't need to specialize this function.")
(:method (resource)
(when (resource-collection-p resource)
(dav-node "resourcetype" (dav-node "collection")))))
(defgeneric resource-uri-prefix (resource)
(:documentation "This function must return a string which is
the part of a resource's HTTP 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.")
(:method (resource)
(format nil "http~:[~;s~]://~A~@[:~A~]/"
(acceptor-ssl-p *acceptor*)
(ppcre:regex-replace ":\\d+$" (acceptor-address *acceptor*) "")
(acceptor-port *acceptor*))))
(defgeneric get-dead-properties (resource)
(:documentation "This function must return all dead properties
of the resource RESOURCE as a list of XML elements structured as
XMLS nodes. There's a default method but you should definitely
specialize this for production servers.")
(:method (resource)
(retrieve-properties (resource-script-name resource))))
(defgeneric remove-dead-property (resource property)
(:documentation "This function must remove the currently stored
dead property designated by PROPERTY \(an XMLS node) of the
resource RESOURCE. There's a default method but you should
definitely specialize this for production servers.")
(:method (resource property)
(store-properties (resource-script-name resource)
(remove property (get-dead-properties resource)
:test #'property-equal))))
(defgeneric set-dead-property (resource property)
(:documentation "This function must replace the currently
stored dead property designated by PROPERTY \(an XMLS node) of
the resource RESOURCE with PROPERTY, 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.")
(:method (resource property)
(store-properties (resource-script-name resource)
(cons property
(remove property (get-dead-properties resource)
:test #'property-equal)))))
(defgeneric remove-dead-properties (resource)
(:documentation "This function must remove all dead properties
of the resource RESOURCE. There's a default method but you
should definitely specialize this for production servers.")
(:method (resource)
(remove-properties (resource-script-name resource))))
(defgeneric move-dead-properties (source destination)
(:documentation "This function must move all dead properties of
the resource SOURCE to the resource DESTINATION. There's a
default method but you should definitely specialize this for
production servers.")
(:method (source destination)
(move-properties (resource-script-name source)
(resource-script-name destination))))
(defgeneric copy-dead-properties (source destination)
(:documentation "This function must copy all dead properties of
the resource SOURCE to the resource DESTINATION. There's a
default method but you should definitely specialize this for
production servers.")
(:method (source destination)
(copy-properties (resource-script-name source)
(resource-script-name destination))))
;;; Internal functionality
(defun resource-href (resource)
"Returns a URL-encoded version of the resource's script name for use
in HREF elements in property XML."
(format nil "~:[/~;~:*~{~A~^/~}~]"
(mapcar (lambda (string)
(url-encode string +utf-8+))
(ppcre:split "/" (resource-script-name resource)))))
(defun resource-name (resource)
"Retrieves and returns the \"name part\" of the script name of
RESOURCE, i.e. the last non-empty string behind a slash. Note
that the result can be NIL. This is a bit similar to
CL:FILE-NAMESTRING."
(first (last (ppcre:split "/" (resource-script-name resource)))))
(defun get-last-modified (resource)
"This is the function that is called for the
\"getlastmodified\" property. It returns the result of
RESOURCE-WRITE-DATE as an RFC 1123 string within a DAV XML node."
(let ((node (dav-node "getlastmodified" (rfc-1123-date (resource-write-date resource)))))
(push '(("dt" . "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/") "dateTime.rfc1123")
(node-attributes node))
node))
(defun creation-date (resource)
"This is the function that is called for the \"creationdate\"
property. It returns the result of RESOURCE-CREATION-DATE as an
ISO 8601 string within a DAV XML node."
(let ((node (dav-node "creationdate" (iso-8601-date (resource-creation-date resource)))))
(push '(("dt" . "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/") "dateTime.tz")
(node-attributes node))
node))
(defun get-content-length (resource)
"This is the function that is called for the
\"getcontentlength\" property. It simply returns the result of
RESOURCE-LENGTH as a string."
(unless (resource-collection-p resource)
(format nil "~D" (resource-length resource))))
(defun get-content-type (resource)
"This is the function that is called for the \"getcontenttype\"
property. It simply returns the result of RESOURCE-CONTENT-TYPE
for non-collections and \"httpd/unix-directory\" for
collections."
(cond ((resource-collection-p resource) "httpd/unix-directory")
(t (resource-content-type resource))))
(defun remove-resource* (resource)
"Removes the resource RESOURCE and \(if necessary) its children
using REMOVE-RESOURCE. Returns a list of conses where the car is
an HTTP return code and the cdr is the corresponding resource for
exceptional situations encountered during the process."
(unless (resource-exists resource)
(return-from remove-resource*
(list (cons +http-not-found+ resource))))
(when (resource-collection-p resource)
;; try to remove the children first
(let ((child-results
(loop for child in (resource-children resource)
nconc (remove-resource* child))))
(when child-results
;; stop recursion if something went wrong deeper down in the
;; hierarchy
(return-from remove-resource* child-results))))
;; remove the dead properties first
(remove-dead-properties resource)
(handler-case
(remove-resource resource)
(error (condition)
(warn "While trying to delete ~S: ~A"
(resource-script-name resource) condition)
(list (cons +http-internal-server-error+ resource)))
(:no-error (&rest args)
(declare (ignore args))
nil)))
(defun copy-or-move-resource* (source destination movep depth)
"Copies or moves \(depending on the generalized boolean MOVEP)
the resource denoted by SOURCE to \(the resource denoted by)
DESTINATION. If DEPTH is NIL, recurses down to the children \(if
any) as well. Returns a list of conses where the car is an HTTP
return code and the cdr is the corresponding \(source) resource
for exceptional situations encountered during the process."
(unless (resource-exists source)
(return-from copy-or-move-resource*
(list (cons +http-not-found+ source))))
;; take care of dead properties
(funcall (if movep #'move-dead-properties #'copy-dead-properties)
source destination)
(let (results)
(handler-case
(funcall (if movep #'move-resource #'copy-resource)
source destination)
(error (condition)
(warn "While trying to ~:[copy~;move~] from ~S to ~S: ~A"
movep
(resource-script-name source)
(resource-script-name destination)
condition)
(push (cons +http-internal-server-error+ source) results)))
(cond ((and (resource-collection-p source)
(null depth)
;; only recurse if there weren't any errors
(null results))
(loop for source-child in (resource-children source)
for destination-child = (get-resource
;; synthesize script name for new child
(format nil "~A~A"
(resource-script-name destination)
(resource-name source-child)))
nconc (copy-or-move-resource* source-child destination-child depth movep)))
(t results))))
(defgeneric create-resource (resource-class script-name)
(:documentation "Creates and returns an object of type
RESOURCE-CLASS \(a subclass of RESOURCE) corresponding to the script
name SCRIPT-NAME \(which is already URL-decoded).")
(:method ((resource-class standard-class) script-name)
(create-resource (class-name resource-class) script-name))
(:method ((resource-class-name symbol) script-name)
(make-instance resource-class-name
:script-name script-name)))
(defun get-resource (&optional (script-name (url-decode* (script-name*))))
"Creates and returns an object of the type stored in
*RESOURCE-CLASS* corresponding to the script name SCRIPT-NAME."
(create-resource *resource-class* script-name))
(defun resource-created (resource)
"Utility function which sets up Hunchentoot's *REPLY* object
for a +HTTP-CREATED+ response corresponding to the newly-created
resource RESOURCE."
(setf (content-type*) (get-content-type resource)
(header-out :location) (resource-script-name resource)
(return-code*) +http-created+)
(let ((etag (resource-etag resource))
(content-language (resource-content-language resource)))
(when etag
(setf (header-out :etag) etag))
(when content-language
(setf (header-out :content-language) content-language)))
nil)