2 ;;;; Copyright (c) 2008 Zachary Beane, All Rights Reserved
4 ;;;; Redistribution and use in source and binary forms, with or without
5 ;;;; modification, are permitted provided that the following conditions
8 ;;;; * Redistributions of source code must retain the above copyright
9 ;;;; notice, this list of conditions and the following disclaimer.
11 ;;;; * Redistributions in binary form must reproduce the above
12 ;;;; copyright notice, this list of conditions and the following
13 ;;;; disclaimer in the documentation and/or other materials
14 ;;;; provided with the distribution.
16 ;;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
17 ;;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 ;;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 ;;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
20 ;;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 ;;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
22 ;;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 ;;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
24 ;;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25 ;;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 ;;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 (defparameter *canned-access-policies
*
33 '((:private .
"private")
34 (:public-read .
"public-read")
35 (:public-read-write .
"public-read-write")
36 (:authenticated-read .
"authenticated-read")))
38 (defun canned-access-policy (access-policy)
39 (let ((value (assoc access-policy
*canned-access-policies
*)))
41 (error "~S is not a supported access policy.~%Supported policies are ~S"
43 (mapcar 'first
*canned-access-policies
*)))
44 (list (cons "acl" (cdr value
)))))
46 (defun access-policy-header (access-policy public
)
47 (cond ((and access-policy public
)
48 (error "Only one of ~S and ~S should be provided"
49 :public
:access-policy
))
51 (canned-access-policy :public-read
))
53 (canned-access-policy access-policy
))))
55 (defun head (&key bucket key parameters
56 ((:credentials
*credentials
*) *credentials
*))
57 "Return three values: the HTTP status, an alist of Drakma-style HTTP
58 headers, and the HTTP phrase, with the results of a HEAD request for
59 the object specified by the optional BUCKET and KEY arguments."
61 (submit-request (make-instance 'request
65 :parameters parameters
))))
67 (values (http-headers response
)
69 (http-phrase response
))))
71 ;;; Operations on buckets
73 (defun all-buckets (&key
((:credentials
*credentials
*) *credentials
*))
74 "Return a vector of all BUCKET objects associated with *CREDENTIALS*."
75 (let ((response (submit-request (make-instance 'request
79 (defun bucket-location (bucket &key
80 ((:credentials
*credentials
*) *credentials
*))
81 "If BUCKET was created with a LocationConstraint, return its
83 (let* ((request (make-instance 'request
85 :sub-resource
"location"
87 (response (submit-request request
))
88 (location (location response
)))
89 (when (plusp (length location
))
92 (defun query-bucket (bucket &key prefix marker max-keys delimiter
93 ((:credentials
*credentials
*) *credentials
*))
94 (submit-request (make-instance 'request
102 :delimiter delimiter
))))
104 (defun continue-bucket-query (response)
106 (let ((request (successive-request response
)))
108 (submit-request request
)))))
110 (defun all-keys (bucket &key prefix
111 ((:credentials
*credentials
*) *credentials
*))
112 "Reutrn a vector of all KEY objects in BUCKET."
113 (let ((response (query-bucket bucket
:prefix prefix
))
118 (push (keys response
) results
)
119 (setf response
(continue-bucket-query response
)))
120 (let ((combined (make-array (reduce #'+ results
:key
#'length
)))
122 (dolist (keys (nreverse results
) combined
)
123 (replace combined keys
:start1 start
)
124 (incf start
(length keys
))))))
126 (defun bucket-exists-p (bucket &key
127 ((:credentials
*credentials
*) *credentials
*))
128 (let ((code (nth-value 1 (head :bucket bucket
130 (parameters-alist :max-keys
0)))))
131 (not (<= 400 code
599))))
133 (defun create-bucket (name &key
137 ((:credentials
*credentials
*) *credentials
*))
138 (let ((policy-header (access-policy-header access-policy public
)))
139 (submit-request (make-instance 'request
142 :content
(and location
143 (location-constraint-xml
145 :amz-headers policy-header
))))
147 (defun delete-bucket (bucket &key
148 ((:credentials
*credentials
*) *credentials
*))
149 (let* ((request (make-instance 'request
152 (endpoint (endpoint request
))
153 (bucket (bucket request
)))
155 (submit-request request
)
156 (setf (redirected-endpoint endpoint bucket
) nil
))))
159 ;;; Getting objects as vectors, strings, or files
161 (defun check-request-success (response)
162 (let ((code (http-code response
)))
164 (throw 'not-modified
(values nil
(http-headers response
))))
165 ((not (<= 200 code
299))
166 (setf response
(specialize-response response
))
167 (maybe-signal-error response
)))))
169 (defun make-file-writer-handler (file &key
(if-exists :supersede
))
171 (check-request-success response
)
172 (with-open-stream (input (body response
))
173 (with-open-file (output file
:direction
:output
175 :element-type
'(unsigned-byte 8))
176 (copy-n-octets (content-length response
) input output
)))
177 (setf (body response
) (probe-file file
))
180 (defun vector-writer-handler (response)
181 (check-request-success response
)
182 (let ((buffer (make-octet-vector (content-length response
))))
183 (setf (body response
)
184 (with-open-stream (input (body response
))
185 (read-sequence buffer input
)
189 (defun stream-identity-handler (response)
190 (check-request-success response
)
193 (defun make-string-writer-handler (external-format)
195 (setf response
(vector-writer-handler response
))
196 (setf (body response
)
197 (flexi-streams:octets-to-string
(body response
)
198 :external-format external-format
))
203 (defun get-object (bucket key
&key
205 unless-modified-since
210 (if-exists :supersede
)
211 (string-external-format :utf-8
)
212 ((:credentials
*credentials
*) *credentials
*))
213 (flet ((range-argument (start end
)
215 (format nil
"bytes=~D-~@[~D~]" start
(and end
(1- end
)))))
217 (and time
(http-date-string time
))))
218 (when (and end
(not start
))
220 (when (and start end
(<= end start
))
221 (error "START must be less than END."))
222 (let ((request (make-instance 'request
230 (maybe-date when-modified-since
)
232 (maybe-date unless-modified-since
)
233 :if-match when-etag-matches
234 :if-none-match unless-etag-matches
235 :range
(range-argument start end
))))
236 (handler (cond ((eql output
:vector
)
237 'vector-writer-handler
)
238 ((eql output
:string
)
239 (make-string-writer-handler string-external-format
))
240 ((eql output
:stream
)
241 'stream-identity-handler
)
242 ((or (stringp output
)
244 (make-file-writer-handler output
:if-exists if-exists
))
246 (error "Unknown ~S option ~S -- should be ~
247 :VECTOR, :STRING, :STREAM, or a pathname"
251 (let ((response (submit-request request
252 :keep-stream
(eql output
:stream
)
255 (values (body response
) (http-headers response
)))
256 (precondition-failed (c)
259 (http-headers (request-error-response c
))))))))))
261 (defun get-vector (bucket key
263 when-modified-since unless-modified-since
264 when-etag-matches unless-etag-matches
265 (if-exists :supersede
)
266 ((:credentials
*credentials
*) *credentials
*))
267 (get-object bucket key
271 :when-modified-since when-modified-since
272 :unless-modified-since unless-modified-since
273 :when-etag-matches when-etag-matches
274 :unless-etag-matches unless-etag-matches
275 :if-exists if-exists
))
277 (defun get-string (bucket key
279 (external-format :utf-8
)
280 when-modified-since unless-modified-since
281 when-etag-matches unless-etag-matches
282 (if-exists :supersede
)
283 ((:credentials
*credentials
*) *credentials
*))
284 (get-object bucket key
286 :string-external-format external-format
289 :when-modified-since when-modified-since
290 :unless-modified-since unless-modified-since
291 :when-etag-matches when-etag-matches
292 :unless-etag-matches unless-etag-matches
293 :if-exists if-exists
))
295 (defun get-file (bucket key file
297 when-modified-since unless-modified-since
298 when-etag-matches unless-etag-matches
299 (if-exists :supersede
)
300 ((:credentials
*credentials
*) *credentials
*))
301 (get-object bucket key
302 :output
(pathname file
)
305 :when-modified-since when-modified-since
306 :unless-modified-since unless-modified-since
307 :when-etag-matches when-etag-matches
308 :unless-etag-matches unless-etag-matches
309 :if-exists if-exists
))
315 (defun put-object (object bucket key
&key
319 (string-external-format :utf-8
)
325 (storage-class "STANDARD")
326 ((:credentials
*credentials
*) *credentials
*))
330 (flexi-streams:string-to-octets object
332 string-external-format
))
333 ((or vector pathname
) object
)))
335 (policy-header (access-policy-header access-policy public
)))
336 (setf storage-class
(or storage-class
"STANDARD"))
337 (submit-request (make-instance 'request
343 (append policy-header
344 (list (cons "storage-class"
348 :cache-control cache-control
349 :content-encoding content-encoding
350 :content-disposition content-disposition
351 :expires
(and expires
352 (http-date-string expires
)))
353 :content-type content-type
354 :content-length content-length
358 (defun put-vector (vector bucket key
&key
366 (content-type "binary/octet-stream")
369 ((:credentials
*credentials
*) *credentials
*))
371 (setf vector
(subseq vector
(or start
0) end
)))
372 (put-object vector bucket key
373 :access-policy access-policy
376 :cache-control cache-control
377 :content-encoding content-encoding
378 :content-disposition content-disposition
379 :content-type content-type
381 :storage-class storage-class
))
383 (defun put-string (string bucket key
&key
388 (external-format :utf-8
)
392 (content-type "text/plain")
395 ((:credentials
*credentials
*) *credentials
*))
397 (setf string
(subseq string
(or start
0) end
)))
398 (put-object string bucket key
399 :access-policy access-policy
403 :content-disposition content-disposition
404 :content-encoding content-encoding
405 :content-type content-type
406 :cache-control cache-control
407 :string-external-format external-format
408 :storage-class storage-class
))
411 (defun put-file (file bucket key
&key
419 (content-type "binary/octet-stream")
422 ((:credentials
*credentials
*) *credentials
*))
424 (setf key
(file-namestring file
)))
425 (let ((content (pathname file
)))
427 ;;; FIXME: integrate with not-in-memory file uploading
428 (setf content
(file-subset-vector file start end
)))
429 (put-object content bucket key
430 :access-policy access-policy
433 :cache-control cache-control
434 :content-disposition content-disposition
435 :content-encoding content-encoding
436 :content-type content-type
438 :storage-class storage-class
)))
440 (defun put-stream (stream bucket key
&key
448 (content-type "binary/octet-stream")
451 ((:credentials
*credentials
*) *credentials
*))
452 (let ((content (stream-subset-vector stream start end
)))
453 (put-object content bucket key
454 :access-policy access-policy
457 :cache-control cache-control
458 :content-disposition content-disposition
459 :content-encoding content-encoding
460 :content-type content-type
462 :storage-class storage-class
)))
465 ;;; Delete & copy objects
467 (defun delete-object (bucket key
&key
468 ((:credentials
*credentials
*) *credentials
*))
469 "Delete one object from BUCKET identified by KEY."
470 (submit-request (make-instance 'request
475 (defun bulk-delete-document (keys)
476 (cxml:with-xml-output
(cxml:make-octet-vector-sink
)
477 (cxml:with-element
"Delete"
479 (cxml:with-element
"Object"
480 (cxml:with-element
"Key"
481 (cxml:text key
)))))))
483 (defparameter *delete-objects-binder
*
484 (make-binder '("DeleteResult"
488 ("Key" (bind :deleted-key
)))
490 ("Key" (bind :error-key
))
491 ("Code" (bind :error-code
))
492 ("Message" (bind :error-message
))))))))
494 (defun delete-objects (bucket keys
&key
495 ((:credentials
*credentials
*) *credentials
*))
496 "Delete the objects in BUCKET identified by the sequence KEYS."
499 (subseqs (floor (length keys
) 1000)))
500 (flet ((bulk-delete (keys)
501 (unless (<= (length keys
) 1000)
502 (error "Can only delete 1000 objects per request."))
503 (let* ((content (bulk-delete-document keys
))
504 (md5 (vector-md5/b64 content
)))
506 (submit-request (make-instance 'request
508 :sub-resource
"delete"
512 (bindings (xml-bind *delete-objects-binder
*
514 (results (bvalue :results bindings
)))
515 (dolist (result results
(values deleted failed
))
516 (if (bvalue :deleted-key result
)
518 (push result failed
)))))))
519 (loop for start from
0 by
1000
520 for end
= (+ start
1000)
522 (bulk-delete (subseq keys start end
)))
523 (bulk-delete (subseq keys
(* subseqs
1000)))
524 (values deleted failed
))))
526 (defun delete-all-objects (bucket &key
527 ((:credentials
*credentials
*) *credentials
*))
528 "Delete all objects in BUCKET."
529 (delete-objects bucket
(all-keys bucket
)))
531 (defun copy-object (&key
537 unless-modified-since
538 (metadata nil metadata-supplied-p
)
542 (storage-class "STANDARD")
543 ((:credentials
*credentials
*) *credentials
*))
544 "Copy the object identified by FROM-BUCKET/FROM-KEY to
547 If TO-BUCKET is NIL, uses FROM-BUCKET as the target. If TO-KEY is NIL,
548 uses TO-KEY as the target.
550 If METADATA is provided, it should be an alist of metadata keys and
551 values to set on the new object. Otherwise, the source object's
554 Optional precondition variables are WHEN-ETAG-MATCHES,
555 UNLESS-ETAG-MATCHES, WHEN-MODIFIED-SINCE, UNLESS-MODIFIED-SINCE. The
556 etag variables use an etag as produced by the FILE-ETAG function,
557 i.e. a lowercase hex representation of the file's MD5 digest,
558 surrounded by quotes. The modified-since variables should use a
561 If PUBLIC is T, the new object is visible to all
562 users. Otherwise, a default ACL is present on the new object.
565 (error "FROM-BUCKET is required"))
567 (error "FROM-KEY is required"))
568 (setf to-bucket
(or to-bucket from-bucket
))
569 (setf to-key
(or to-key from-key
))
570 (handler-bind ((precondition-failed
572 (unless precondition-errors
573 (return-from copy-object
574 (values nil
(request-error-response condition
)))))))
576 (parameters-alist :copy-source
(format nil
"~A/~A"
577 (url-encode (name from-bucket
))
578 (url-encode (name from-key
)))
579 :storage-class storage-class
581 (if metadata-supplied-p
"REPLACE" "COPY")
582 :copy-source-if-match when-etag-matches
583 :copy-source-if-none-match unless-etag-matches
584 :copy-source-if-modified-since
585 (and when-modified-since
586 (http-date-string when-modified-since
))
587 :copy-source-if-unmodified-since
588 (and unless-modified-since
589 (http-date-string unless-modified-since
))))
590 (policy-header (access-policy-header access-policy public
)))
591 (submit-request (make-instance 'request
597 (nconc headers policy-header
))))))
600 (defun object-metadata (bucket key
&key
601 ((:credentials
*credentials
*) *credentials
*))
602 "Return the metadata headers as an alist, with keywords for the keys."
603 (let* ((prefix "X-AMZ-META-")
604 (plen (length prefix
)))
605 (flet ((metadata-symbol-p (k)
606 (and (< plen
(length (symbol-name k
)))
607 (string-equal k prefix
:end1 plen
)
608 (intern (subseq (symbol-name k
) plen
)
610 (let ((headers (head :bucket bucket
:key key
)))
611 (loop for
((k . value
)) on headers
612 for meta
= (metadata-symbol-p k
)
614 collect
(cons meta value
))))))
617 ;;; Convenience bit for storage class
619 (defun set-storage-class (bucket key storage-class
&key
620 ((:credentials
*credentials
*) *credentials
*))
621 "Set the storage class of the object identified by BUCKET and KEY to
623 (copy-object :from-bucket bucket
:from-key key
624 :storage-class storage-class
))
629 (defparameter *public-read-grant
*
630 (make-instance 'grant
632 :grantee
*all-users
*)
633 "This grant is added to or removed from an ACL to grant or revoke
634 read access for all users.")
636 (defun get-acl (&key bucket key
637 ((:credentials
*credentials
*) *credentials
*))
638 (let* ((request (make-instance 'request
642 :sub-resource
"acl"))
643 (response (submit-request request
))
644 (acl (acl response
)))
648 (defun put-acl (owner grants
&key bucket key
649 ((:credentials
*credentials
*) *credentials
*))
650 (let* ((acl (make-instance 'access-control-list
653 (request (make-instance 'request
658 :content
(acl-serialize acl
))))
659 (submit-request request
)))
662 (defun make-public (&key bucket key
663 ((:credentials
*credentials
*) *credentials
*))
664 (multiple-value-bind (owner grants
)
665 (get-acl :bucket bucket
:key key
)
667 (cons *public-read-grant
* grants
)
671 (defun make-private (&key bucket key
672 ((:credentials
*credentials
*) *credentials
*))
673 (multiple-value-bind (owner grants
)
674 (get-acl :bucket bucket
:key key
)
676 (remove *all-users
* grants
677 :test
#'acl-eqv
:key
#'grantee
))
678 (put-acl owner grants
:bucket bucket
:key key
)))
683 (defparameter *log-delivery-grants
*
684 (list (make-instance 'grant
686 :grantee
*log-delivery
*)
687 (make-instance 'grant
688 :permission
:read-acl
689 :grantee
*log-delivery
*))
690 "This list of grants is used to allow the Amazon log delivery group
691 to write logfile objects into a particular bucket.")
693 (defun enable-logging-to (bucket &key
694 ((:credentials
*credentials
*) *credentials
*))
695 "Configure the ACL of BUCKET to accept logfile objects."
696 (multiple-value-bind (owner grants
)
697 (get-acl :bucket bucket
)
698 (setf grants
(append *log-delivery-grants
* grants
))
699 (put-acl owner grants
:bucket bucket
)))
701 (defun disable-logging-to (bucket &key
702 ((:credentials
*credentials
*) *credentials
*))
703 "Configure the ACL of BUCKET to remove permissions for the log
705 (multiple-value-bind (owner grants
)
706 (get-acl :bucket bucket
)
707 (setf grants
(remove-if (lambda (grant)
708 (acl-eqv (grantee grant
) *log-delivery
*))
710 (put-acl owner grants
:bucket bucket
)))
712 (defun enable-logging (bucket target-bucket target-prefix
&key
714 ((:credentials
*credentials
*) *credentials
*))
715 "Enable logging of requests to BUCKET, putting logfile objects into
716 TARGET-BUCKET with a key prefix of TARGET-PREFIX."
717 (let* ((setup (make-instance 'logging-setup
718 :target-bucket target-bucket
719 :target-prefix target-prefix
720 :target-grants target-grants
))
721 (request (make-instance 'request
723 :sub-resource
"logging"
725 :content
(log-serialize setup
)))
729 (return (submit-request request
))
730 (invalid-logging-target (condition)
731 (when (starts-with "You must give the log-delivery group"
732 (message (request-error-response condition
)))
735 (enable-logging-to target-bucket
))))))))
738 (defparameter *empty-logging-setup
*
739 (log-serialize (make-instance 'logging-setup
))
740 "An empty logging setup; putting this into the logging setup of a
741 bucket effectively disables logging.")
743 (defun disable-logging (bucket &key
744 ((:credentials
*credentials
*) *credentials
*))
745 "Disable the creation of access logs for BUCKET."
746 (submit-request (make-instance 'request
748 :sub-resource
"logging"
750 :content
*empty-logging-setup
*)))
752 (defun logging-setup (bucket &key
753 ((:credentials
*credentials
*) *credentials
*))
755 (submit-request (make-instance 'request
757 :sub-resource
"logging")))))
758 (values (target-bucket setup
)
759 (target-prefix setup
)
760 (target-grants setup
))))
764 ;;; Creating unauthorized and authorized URLs for a resource
766 (defclass url-based-request
(request)
773 (defmethod date-string ((request url-based-request
))
774 (format nil
"~D" (expires request
)))
776 (defun resource-url (&key bucket key vhost ssl sub-resource
)
779 (format nil
"http~@[s~*~]://~A/~@[~A~]~@[?~A~]"
780 ssl bucket
(url-encode key
) sub-resource
))
782 (format nil
"http~@[s~*~]://~A.s3.amazonaws.com/~@[~A~]~@[?~A~]"
783 ssl bucket
(url-encode key
) sub-resource
))
785 (format nil
"http~@[s~*~]://s3.amazonaws.com/~@[~A/~]~@[~A~]~@[?~A~]"
786 ssl
(url-encode bucket
) (url-encode key
) sub-resource
))))
788 (defun authorized-url (&key bucket key vhost expires ssl sub-resource
789 ((:credentials
*credentials
*) *credentials
*))
790 (unless (and expires
(integerp expires
) (plusp expires
))
791 (error "~S option must be a positive integer" :expires
))
792 (let* ((request (make-instance 'url-based-request
795 :sub-resource sub-resource
797 :expires
(unix-time expires
)))
799 (alist-to-url-encoded-string
800 (list (cons "AWSAccessKeyId" (access-key *credentials
*))
801 (cons "Expires" (format nil
"~D" (expires request
)))
803 (signature request
))))))
806 (format nil
"http~@[s~*~]://~A/~@[~A~]?~@[~A&~]~A"
807 ssl bucket
(url-encode key
) sub-resource parameters
))
809 (format nil
"http~@[s~*~]://~A.s3.amazonaws.com/~@[~A~]?~@[~A&~]~A"
810 ssl bucket
(url-encode key
) sub-resource parameters
))
812 (format nil
"http~@[s~*~]://s3.amazonaws.com/~@[~A/~]~@[~A~]?~@[~A&~]~A"
813 ssl
(url-encode bucket
) (url-encode key
) sub-resource
817 ;;; Miscellaneous operations
819 (defparameter *me-cache
*
820 (make-hash-table :test
'equal
)
821 "A cache for the result of the ME function. Keys are Amazon access
825 ((:credentials
*credentials
*) *credentials
*))
826 "Return a PERSON object corresponding to the current credentials. Cached."
827 (or (gethash (access-key *credentials
*) *me-cache
*)
829 (gethash (access-key *credentials
*) *me-cache
*)
830 (let ((response (submit-request (make-instance 'request
))))
833 (defun make-post-policy (&key expires conditions
834 ((:credentials
*credentials
*) *credentials
*))
835 "Return an encoded HTTP POST policy string and policy signature as
838 (error "~S is required" :expires
))
839 (let ((policy (make-instance 'post-policy
841 :conditions conditions
)))
842 (values (policy-string64 policy
)
843 (policy-signature (secret-key *credentials
*) policy
))))