2 ;;;; Copyright (c) 2008, 2015 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 ((:backoff
*backoff
*) *backoff
*))
58 "Return three values: the HTTP status, an alist of Drakma-style HTTP
59 headers, and the HTTP phrase, with the results of a HEAD request for
60 the object specified by the optional BUCKET and KEY arguments."
61 (let* ((security-token (security-token *credentials
*))
63 (submit-request (make-instance 'request
69 (list (cons "security-token" security-token
)))
70 :parameters parameters
))))
72 (values (http-headers response
)
74 (http-phrase response
))))
76 ;;; Operations on buckets
78 (defun all-buckets (&key
((:credentials
*credentials
*) *credentials
*)
79 ((:backoff
*backoff
*) *backoff
*))
80 "Return a vector of all BUCKET objects associated with *CREDENTIALS*."
81 (let ((response (submit-request (make-instance 'request
85 (defun bucket-location (bucket &key
86 ((:credentials
*credentials
*) *credentials
*)
87 ((:backoff
*backoff
*) *backoff
*))
88 "If BUCKET was created with a LocationConstraint, return its
90 (let* ((request (make-instance 'request
92 :sub-resource
"location"
94 `(,(when (security-token *credentials
*)
95 (cons "x-amz-security-token"
96 (security-token *credentials
*))))
98 (response (submit-request request
))
99 (location (location response
)))
100 (when (plusp (length location
))
103 (defun bucket-region (bucket
104 &key
((:credentials
*credentials
*) *credentials
*)
105 ((:backoff
*backoff
*) *backoff
*))
106 (or (bucket-location bucket
)
109 (defun region-endpoint (region)
110 (if (string= region
"us-east-1")
111 (or *s3-endpoint
* "s3.amazonaws.com")
112 (format nil
"s3-~A.amazonaws.com" region
)))
114 (defun query-bucket (bucket &key prefix marker max-keys delimiter
115 ((:credentials
*credentials
*) *credentials
*)
116 ((:backoff
*backoff
*) *backoff
*))
117 (submit-request (make-instance 'request
125 :delimiter delimiter
))))
127 (defun continue-bucket-query (response)
129 (let ((request (successive-request response
)))
131 (submit-request request
)))))
133 (defun all-keys (bucket &key prefix
134 ((:credentials
*credentials
*) *credentials
*)
135 ((:backoff
*backoff
*) *backoff
*))
136 "Reutrn a vector of all KEY objects in BUCKET."
137 (let ((response (query-bucket bucket
:prefix prefix
))
142 (push (keys response
) results
)
143 (setf response
(continue-bucket-query response
)))
144 (let ((combined (make-array (reduce #'+ results
:key
#'length
)))
146 (dolist (keys (nreverse results
) combined
)
147 (replace combined keys
:start1 start
)
148 (incf start
(length keys
))))))
150 (defun bucket-exists-p (bucket &key
151 ((:credentials
*credentials
*) *credentials
*)
152 ((:backoff
*backoff
*) *backoff
*))
153 (let ((code (nth-value 1 (head :bucket bucket
155 (parameters-alist :max-keys
0)))))
156 (not (<= 400 code
599))))
158 (defun create-bucket (name &key
162 ((:credentials
*credentials
*) *credentials
*)
163 ((:backoff
*backoff
*) *backoff
*))
164 (let ((policy-header (access-policy-header access-policy public
)))
165 (submit-request (make-instance 'request
168 :content
(and location
169 (location-constraint-xml
171 :amz-headers policy-header
))))
173 (defun delete-bucket (bucket &key
174 ((:credentials
*credentials
*) *credentials
*)
175 ((:backoff
*backoff
*) *backoff
*))
176 (let* ((request (make-instance 'request
179 (endpoint (endpoint request
))
180 (bucket (bucket request
)))
182 (submit-request request
)
183 (setf (redirection-data endpoint bucket
) nil
))))
186 ;;; Getting objects as vectors, strings, or files
188 (defun check-request-success (response)
189 (let ((code (http-code response
)))
191 (throw 'not-modified
(values nil
(http-headers response
))))
192 ((not (<= 200 code
299))
193 (setf response
(specialize-response response
))
194 (maybe-signal-error response
)))))
196 (defun make-file-writer-handler (file &key
(if-exists :supersede
))
198 (check-request-success response
)
199 (let ((input (body response
)))
200 (with-open-file (output file
:direction
:output
202 :element-type
'(unsigned-byte 8))
203 (copy-n-octets (content-length response
) input output
)))
204 (setf (body response
) (probe-file file
))
207 (defun vector-writer-handler (response)
208 (check-request-success response
)
209 (let ((buffer (make-octet-vector (content-length response
))))
210 (setf (body response
)
211 (let ((input (body response
)))
212 (read-sequence buffer input
)
216 (defun stream-identity-handler (response)
217 (check-request-success response
)
220 (defun make-string-writer-handler (external-format)
222 (setf response
(vector-writer-handler response
))
223 (setf (body response
)
224 (flexi-streams:octets-to-string
(body response
)
225 :external-format external-format
))
230 (defun get-object (bucket key
&key
232 unless-modified-since
237 (if-exists :supersede
)
238 (string-external-format :utf-8
)
239 ((:credentials
*credentials
*) *credentials
*)
240 ((:backoff
*backoff
*) *backoff
*))
241 (flet ((range-argument (start end
)
243 (format nil
"bytes=~D-~@[~D~]" start
(and end
(1- end
)))))
245 (and time
(http-date-string time
))))
246 (when (and end
(not start
))
248 (when (and start end
(<= end start
))
249 (error "START must be less than END."))
250 (let* ((security-token (security-token *credentials
*))
251 (request (make-instance 'request
257 (list (cons "security-token" security-token
)))
260 ;; nlevine 2016-06-15 -- not only is this apparently
261 ;; unnecessary, it also sends "connection" in the
262 ;; signed headers, which results in a
263 ;; SignatureDoesNotMatch error.
264 ;; :connection (unless *use-keep-alive* "close")
266 (maybe-date when-modified-since
)
268 (maybe-date unless-modified-since
)
269 :if-match when-etag-matches
270 :if-none-match unless-etag-matches
271 :range
(range-argument start end
))))
272 (handler (cond ((eql output
:vector
)
273 'vector-writer-handler
)
274 ((eql output
:string
)
275 (make-string-writer-handler string-external-format
))
276 ((eql output
:stream
)
277 'stream-identity-handler
)
278 ((or (stringp output
)
280 (make-file-writer-handler output
:if-exists if-exists
))
282 (error "Unknown ~S option ~S -- should be ~
283 :VECTOR, :STRING, :STREAM, or a pathname"
287 (let ((response (submit-request request
288 :keep-stream
(or (eql output
:stream
)
292 (values (body response
) (http-headers response
)))
293 (precondition-failed (c)
296 (http-headers (request-error-response c
))))))))))
298 (defun get-vector (bucket key
300 when-modified-since unless-modified-since
301 when-etag-matches unless-etag-matches
302 (if-exists :supersede
)
303 ((:credentials
*credentials
*) *credentials
*)
304 ((:backoff
*backoff
*) *backoff
*))
305 (get-object bucket key
309 :when-modified-since when-modified-since
310 :unless-modified-since unless-modified-since
311 :when-etag-matches when-etag-matches
312 :unless-etag-matches unless-etag-matches
313 :if-exists if-exists
))
315 (defun get-string (bucket key
317 (external-format :utf-8
)
318 when-modified-since unless-modified-since
319 when-etag-matches unless-etag-matches
320 (if-exists :supersede
)
321 ((:credentials
*credentials
*) *credentials
*)
322 ((:backoff
*backoff
*) *backoff
*))
323 (get-object bucket key
325 :string-external-format external-format
328 :when-modified-since when-modified-since
329 :unless-modified-since unless-modified-since
330 :when-etag-matches when-etag-matches
331 :unless-etag-matches unless-etag-matches
332 :if-exists if-exists
))
334 (defun get-file (bucket key file
336 when-modified-since unless-modified-since
337 when-etag-matches unless-etag-matches
338 (if-exists :supersede
)
339 ((:credentials
*credentials
*) *credentials
*)
340 ((:backoff
*backoff
*) *backoff
*))
341 (get-object bucket key
342 :output
(pathname file
)
345 :when-modified-since when-modified-since
346 :unless-modified-since unless-modified-since
347 :when-etag-matches when-etag-matches
348 :unless-etag-matches unless-etag-matches
349 :if-exists if-exists
))
354 (defun format-tagging-header (tagging)
355 (format nil
"~{~a=~a~^&~}"
356 (mapcan #'(lambda (kv)
358 (drakma:url-encode
(car kv
) :iso-8859-1
)
359 (drakma:url-encode
(cdr kv
) :iso-8859-1
)))
362 (defun put-object (object bucket key
&key
366 (string-external-format :utf-8
)
372 (storage-class "STANDARD")
374 ((:credentials
*credentials
*) *credentials
*)
375 ((:backoff
*backoff
*) *backoff
*))
379 (flexi-streams:string-to-octets object
381 string-external-format
))
382 ((or vector pathname
) object
)))
384 (policy-header (access-policy-header access-policy public
))
385 (security-token (security-token *credentials
*)))
386 (setf storage-class
(or storage-class
"STANDARD"))
387 (submit-request (make-instance 'request
393 (append policy-header
394 (list (cons "storage-class" storage-class
))
396 (list (cons "security-token" security-token
)))
399 (cons "tagging" (format-tagging-header tagging
)))))
402 :cache-control cache-control
403 :content-encoding content-encoding
404 :content-disposition content-disposition
405 :expires
(and expires
406 (http-date-string expires
)))
407 :content-type content-type
408 :content-length content-length
412 (defun put-vector (vector bucket key
&key
420 (content-type "binary/octet-stream")
424 ((:credentials
*credentials
*) *credentials
*)
425 ((:backoff
*backoff
*) *backoff
*))
427 (setf vector
(subseq vector
(or start
0) end
)))
428 (put-object vector bucket key
429 :access-policy access-policy
432 :cache-control cache-control
433 :content-encoding content-encoding
434 :content-disposition content-disposition
435 :content-type content-type
437 :storage-class storage-class
440 (defun put-string (string bucket key
&key
445 (external-format :utf-8
)
449 (content-type "text/plain")
453 ((:credentials
*credentials
*) *credentials
*)
454 ((:backoff
*backoff
*) *backoff
*))
456 (setf string
(subseq string
(or start
0) end
)))
457 (put-object string bucket key
458 :access-policy access-policy
462 :content-disposition content-disposition
463 :content-encoding content-encoding
464 :content-type content-type
465 :cache-control cache-control
466 :string-external-format external-format
467 :storage-class storage-class
471 (defun put-file (file bucket key
&key
479 (content-type "binary/octet-stream")
483 ((:credentials
*credentials
*) *credentials
*)
484 ((:backoff
*backoff
*) *backoff
*))
486 (setf key
(file-namestring file
)))
487 (let ((content (pathname file
)))
489 ;;; FIXME: integrate with not-in-memory file uploading
490 (setf content
(file-subset-vector file start end
)))
491 (put-object content bucket key
492 :access-policy access-policy
495 :cache-control cache-control
496 :content-disposition content-disposition
497 :content-encoding content-encoding
498 :content-type content-type
500 :storage-class storage-class
503 (defun put-stream (stream bucket key
&key
511 (content-type "binary/octet-stream")
515 ((:credentials
*credentials
*) *credentials
*)
516 ((:backoff
*backoff
*) *backoff
*))
517 (let ((content (stream-subset-vector stream start end
)))
518 (put-object content bucket key
519 :access-policy access-policy
522 :cache-control cache-control
523 :content-disposition content-disposition
524 :content-encoding content-encoding
525 :content-type content-type
527 :storage-class storage-class
531 ;;; Delete & copy objects
533 (defun delete-object (bucket key
&key
534 ((:credentials
*credentials
*) *credentials
*)
535 ((:backoff
*backoff
*) *backoff
*))
536 "Delete one object from BUCKET identified by KEY."
537 (let ((security-token (security-token *credentials
*)))
538 (submit-request (make-instance 'request
544 (list (cons "security-token" security-token
)))))))
546 (defun bulk-delete-document (keys)
548 (cxml:with-xml-output
(cxml:make-octet-vector-sink
)
549 (cxml:with-element
"Delete"
552 (cxml:with-element
"Object"
553 (cxml:with-element
"Key"
554 (cxml:text
(name key
)))))
558 (defbinder delete-objects-result
563 ("Key" (bind :deleted-key
)))
565 ("Key" (bind :error-key
))
566 ("Code" (bind :error-code
))
567 ("Message" (bind :error-message
)))))))
569 (defun delete-objects (bucket keys
571 ((:credentials
*credentials
*) *credentials
*)
572 ((:backoff
*backoff
*) *backoff
*))
573 "Delete the objects in BUCKET identified by the sequence KEYS."
576 (subseqs (floor (length keys
) 1000)))
577 (flet ((bulk-delete (keys)
578 (unless (<= 1 (length keys
) 1000)
579 (error "Can only delete 1 to 1000 objects per request ~
582 (let* ((content (bulk-delete-document keys
))
583 (md5 (vector-md5/b64 content
)))
585 (submit-request (make-instance 'request
587 :sub-resource
"delete"
591 (bindings (xml-bind 'delete-objects-result
593 (results (bvalue :results bindings
)))
594 (dolist (result results
(values deleted failed
))
595 (if (bvalue :deleted-key result
)
597 (push result failed
)))))))
598 (loop for start from
0 by
1000
599 for end
= (+ start
1000)
601 (bulk-delete (subseq keys start end
)))
602 (let ((remainder (subseq keys
(* subseqs
1000))))
603 (when (plusp (length remainder
))
604 (bulk-delete (subseq keys
(* subseqs
1000)))))
605 (values deleted failed
))))
607 (defun delete-all-objects (bucket &key
608 ((:credentials
*credentials
*) *credentials
*)
609 ((:backoff
*backoff
*) *backoff
*))
610 "Delete all objects in BUCKET."
611 ;; FIXME: This should probably bucket-query and incrementally delete
612 ;; instead of fetching all keys upfront.
613 (delete-objects bucket
(all-keys bucket
)))
615 (defun copy-object (&key
621 unless-modified-since
622 (metadata nil metadata-supplied-p
)
626 (storage-class "STANDARD")
627 (tagging nil tagging-supplied-p
)
628 ((:credentials
*credentials
*) *credentials
*)
629 ((:backoff
*backoff
*) *backoff
*))
630 "Copy the object identified by FROM-BUCKET/FROM-KEY to
633 If TO-BUCKET is NIL, uses FROM-BUCKET as the target. If TO-KEY is NIL,
634 uses TO-KEY as the target.
636 If METADATA is provided, it should be an alist of metadata keys and
637 values to set on the new object. Otherwise, the source object's
640 If TAGGING is provided, it should be an alist of tag keys and values
641 to be set on the new object's tagging resource. Otherwise, the source
642 object's tagging is copied.
644 Optional precondition variables are WHEN-ETAG-MATCHES,
645 UNLESS-ETAG-MATCHES, WHEN-MODIFIED-SINCE, UNLESS-MODIFIED-SINCE. The
646 etag variables use an etag as produced by the FILE-ETAG function,
647 i.e. a lowercase hex representation of the file's MD5 digest,
648 surrounded by quotes. The modified-since variables should use a
651 If PUBLIC is T, the new object is visible to all
652 users. Otherwise, a default ACL is present on the new object.
655 (error "FROM-BUCKET is required"))
657 (error "FROM-KEY is required"))
658 (setf to-bucket
(or to-bucket from-bucket
))
659 (setf to-key
(or to-key from-key
))
660 (handler-bind ((precondition-failed
662 (unless precondition-errors
663 (return-from copy-object
664 (values nil
(request-error-response condition
)))))))
666 (parameters-alist :copy-source
(format nil
"~A/~A"
667 (url-encode (name from-bucket
))
668 (url-encode (name from-key
)))
669 :storage-class storage-class
671 (if metadata-supplied-p
"REPLACE" "COPY")
673 (if tagging-supplied-p
"REPLACE" "COPY")
674 :copy-source-if-match when-etag-matches
675 :copy-source-if-none-match unless-etag-matches
676 :copy-source-if-modified-since
677 (and when-modified-since
678 (http-date-string when-modified-since
))
679 :copy-source-if-unmodified-since
680 (and unless-modified-since
681 (http-date-string unless-modified-since
))))
682 (policy-header (access-policy-header access-policy public
))
683 (tagging-header (when tagging-supplied-p
684 (list (cons "tagging" (format-tagging-header tagging
))))))
685 (submit-request (make-instance 'request
696 (defun object-metadata (bucket key
698 ((:credentials
*credentials
*) *credentials
*)
699 ((:backoff
*backoff
*) *backoff
*))
700 "Return the metadata headers as an alist, with keywords for the keys."
701 (let* ((prefix "X-AMZ-META-")
702 (plen (length prefix
)))
703 (flet ((metadata-symbol-p (k)
704 (and (< plen
(length (symbol-name k
)))
705 (string-equal k prefix
:end1 plen
)
706 (intern (subseq (symbol-name k
) plen
)
708 (let ((headers (head :bucket bucket
:key key
)))
709 (loop for
((k . value
)) on headers
710 for meta
= (metadata-symbol-p k
)
712 collect
(cons meta value
))))))
715 ;;; Convenience bit for storage class
717 (defun set-storage-class (bucket key storage-class
719 ((:credentials
*credentials
*) *credentials
*)
720 ((:backoff
*backoff
*) *backoff
*))
721 "Set the storage class of the object identified by BUCKET and KEY to
723 (copy-object :from-bucket bucket
:from-key key
724 :storage-class storage-class
))
729 (defparameter *public-read-grant
*
730 (make-instance 'grant
732 :grantee
*all-users
*)
733 "This grant is added to or removed from an ACL to grant or revoke
734 read access for all users.")
736 (defun get-acl (&key bucket key
737 ((:credentials
*credentials
*) *credentials
*)
738 ((:backoff
*backoff
*) *backoff
*))
739 (let* ((request (make-instance 'request
743 :sub-resource
"acl"))
744 (response (submit-request request
))
745 (acl (acl response
)))
749 (defun put-acl (owner grants
&key bucket key
750 ((:credentials
*credentials
*) *credentials
*)
751 ((:backoff
*backoff
*) *backoff
*))
752 (let* ((acl (make-instance 'access-control-list
755 (request (make-instance 'request
760 :content
(acl-serialize acl
))))
761 (submit-request request
)))
764 (defun make-public (&key bucket key
765 ((:credentials
*credentials
*) *credentials
*)
766 ((:backoff
*backoff
*) *backoff
*))
767 (multiple-value-bind (owner grants
)
768 (get-acl :bucket bucket
:key key
)
770 (cons *public-read-grant
* grants
)
774 (defun make-private (&key bucket key
775 ((:credentials
*credentials
*) *credentials
*)
776 ((:backoff
*backoff
*) *backoff
*))
777 (multiple-value-bind (owner grants
)
778 (get-acl :bucket bucket
:key key
)
780 (remove *all-users
* grants
781 :test
#'acl-eqv
:key
#'grantee
))
782 (put-acl owner grants
:bucket bucket
:key key
)))
787 (defparameter *log-delivery-grants
*
788 (list (make-instance 'grant
790 :grantee
*log-delivery
*)
791 (make-instance 'grant
792 :permission
:read-acl
793 :grantee
*log-delivery
*))
794 "This list of grants is used to allow the Amazon log delivery group
795 to write logfile objects into a particular bucket.")
797 (defun enable-logging-to (bucket &key
798 ((:credentials
*credentials
*) *credentials
*)
799 ((:backoff
*backoff
*) *backoff
*))
800 "Configure the ACL of BUCKET to accept logfile objects."
801 (multiple-value-bind (owner grants
)
802 (get-acl :bucket bucket
)
803 (setf grants
(append *log-delivery-grants
* grants
))
804 (put-acl owner grants
:bucket bucket
)))
806 (defun disable-logging-to (bucket &key
807 ((:credentials
*credentials
*) *credentials
*)
808 ((:backoff
*backoff
*) *backoff
*))
809 "Configure the ACL of BUCKET to remove permissions for the log
811 (multiple-value-bind (owner grants
)
812 (get-acl :bucket bucket
)
813 (setf grants
(remove-if (lambda (grant)
814 (acl-eqv (grantee grant
) *log-delivery
*))
816 (put-acl owner grants
:bucket bucket
)))
818 (defun enable-logging (bucket target-bucket target-prefix
821 ((:credentials
*credentials
*) *credentials
*)
822 ((:backoff
*backoff
*) *backoff
*))
823 "Enable logging of requests to BUCKET, putting logfile objects into
824 TARGET-BUCKET with a key prefix of TARGET-PREFIX."
825 (let* ((setup (make-instance 'logging-setup
826 :target-bucket target-bucket
827 :target-prefix target-prefix
828 :target-grants target-grants
))
829 (request (make-instance 'request
831 :sub-resource
"logging"
833 :content
(log-serialize setup
)))
837 (return (submit-request request
))
838 (invalid-logging-target (condition)
839 (when (starts-with "You must give the log-delivery group"
840 (message (request-error-response condition
)))
843 (enable-logging-to target-bucket
))))))))
846 (defparameter *empty-logging-setup
*
847 (log-serialize (make-instance 'logging-setup
))
848 "An empty logging setup; putting this into the logging setup of a
849 bucket effectively disables logging.")
851 (defun disable-logging (bucket &key
852 ((:credentials
*credentials
*) *credentials
*)
853 ((:backoff
*backoff
*) *backoff
*))
854 "Disable the creation of access logs for BUCKET."
855 (submit-request (make-instance 'request
857 :sub-resource
"logging"
859 :content
*empty-logging-setup
*)))
861 (defun logging-setup (bucket &key
862 ((:credentials
*credentials
*) *credentials
*)
863 ((:backoff
*backoff
*) *backoff
*))
865 (submit-request (make-instance 'request
867 :sub-resource
"logging")))))
868 (values (target-bucket setup
)
869 (target-prefix setup
)
870 (target-grants setup
))))
874 ;;; Creating unauthorized and authorized URLs for a resource
876 (defclass url-based-request
(request)
883 (defmethod date-string ((request url-based-request
))
884 (format nil
"~D" (expires request
)))
886 (defun resource-url (&key bucket key vhost ssl sub-resource
)
889 (format nil
"http~@[s~*~]://~A/~@[~A~]~@[?~A~]"
890 ssl bucket
(url-encode key
) sub-resource
))
892 (format nil
"http~@[s~*~]://~A.s3.amazonaws.com/~@[~A~]~@[?~A~]"
893 ssl bucket
(url-encode key
) sub-resource
))
895 (format nil
"http~@[s~*~]://s3.amazonaws.com/~@[~A/~]~@[~A~]~@[?~A~]"
898 (url-encode key
:encode-slash nil
)
901 (defun authorized-url (&key bucket key vhost expires ssl sub-resource content-disposition content-type
902 ((:credentials
*credentials
*) *credentials
*))
903 (unless (and expires
(integerp expires
) (plusp expires
))
904 (error "~S option must be a positive integer" :expires
))
905 (let* ((region (bucket-region bucket
))
906 (region-endpoint (region-endpoint region
))
907 (endpoint (case vhost
909 (:amazon
(format nil
"~A.~A" bucket region-endpoint
))
910 (:wasabi
(format nil
"~a.s3.wasabisys.com" bucket
))
911 ((nil) region-endpoint
)))
912 (extra-parameters (append (if content-disposition
913 (list (cons "response-content-disposition" content-disposition
)))
915 (list (cons "response-content-type" content-type
)))))
916 (request (make-instance 'url-based-request
921 :sub-resource sub-resource
923 :expires
(unix-time expires
)
924 :parameters extra-parameters
)))
925 (setf (amz-headers request
) nil
)
926 (setf (parameters request
)
927 (parameters-alist "X-Amz-Algorithm" "AWS4-HMAC-SHA256"
929 (format nil
"~A/~A/~A/s3/aws4_request"
930 (access-key *credentials
*)
931 (iso8601-basic-date-string (date request
))
933 "X-Amz-Date" (iso8601-basic-timestamp-string (date request
))
934 "X-Amz-Expires" (- expires
(get-universal-time))
935 "X-Amz-SignedHeaders"
936 (format nil
"~{~A~^;~}" (signed-headers request
))))
937 (push (cons "X-Amz-Signature" (request-signature request
))
938 (parameters request
))
939 (let ((parameters (alist-to-url-encoded-string (parameters request
))))
942 (format nil
"http~@[s~*~]://~A/~@[~A~]?~@[~A&~]~A"
945 (url-encode key
:encode-slash nil
)
949 (format nil
"http~@[s~*~]://~A/~@[~A~]?~@[~A&~]~A"
952 (url-encode key
:encode-slash nil
)
956 (format nil
"http~@[s~*~]://~A/~@[~A~]?~@[~A&~]~A"
959 (url-encode key
:encode-slash nil
)
963 (format nil
"http~@[s~*~]://~A/~@[~A/~]~@[~A~]?~@[~A&~]~A"
967 (url-encode key
:encode-slash nil
)
971 ;;; Miscellaneous operations
973 (defparameter *me-cache
*
974 (make-hash-table :test
'equal
)
975 "A cache for the result of the ME function. Keys are Amazon access
979 ((:credentials
*credentials
*) *credentials
*)
980 ((:backoff
*backoff
*) *backoff
*))
981 "Return a PERSON object corresponding to the current credentials. Cached."
982 (or (gethash (access-key *credentials
*) *me-cache
*)
984 (gethash (access-key *credentials
*) *me-cache
*)
985 (let ((response (submit-request (make-instance 'request
))))
988 (defun make-post-policy (&key expires conditions
989 ((:credentials
*credentials
*) *credentials
*))
990 "Return an encoded HTTP POST policy string and policy signature as
993 (error "~S is required" :expires
))
994 (let ((policy (make-instance 'post-policy
996 :conditions conditions
)))
997 (values (policy-string64 policy
)
998 (policy-signature (secret-key *credentials
*) policy
))))
1002 (defbinder get-tagging-result
1008 ("Value" (bind :value
)))))))
1010 (defun get-tagging (&key bucket key
1011 ((:credentials
*credentials
*) *credentials
*)
1012 ((:backoff
*backoff
*) *backoff
*))
1013 "Returns the current contents of the object's tagging resource as an alist."
1014 (let* ((request (make-instance 'request
1018 :sub-resource
"tagging"))
1019 (response (submit-request request
))
1020 (tagging (xml-bind 'get-tagging-result
(body response
))))
1021 (mapcar #'(lambda (tag)
1022 (cons (bvalue :key tag
)
1023 (bvalue :value tag
)))
1024 (bvalue :tag-set tagging
))))
1026 (defun put-tagging (tag-set &key bucket key
1027 ((:credentials
*credentials
*) *credentials
*)
1028 ((:backoff
*backoff
*) *backoff
*))
1029 "Sets the tag set, given as an alist, to the object's tagging resource."
1030 (let* ((content (with-xml-output
1031 (with-element "Tagging"
1032 (with-element "TagSet"
1033 (dolist (tag tag-set
)
1035 (with-element "Key" (cxml:text
(car tag
)))
1036 (with-element "Value" (cxml:text
(cdr tag
)))))))))
1037 (request (make-instance 'request
1041 :sub-resource
"tagging"
1043 (submit-request request
)))
1045 (defun delete-tagging (&key bucket key
1046 ((:credentials
*credentials
*) *credentials
*)
1047 ((:backoff
*backoff
*) *backoff
*))
1048 "Deletes the object's tagging resource."
1049 (let* ((request (make-instance 'request
1053 :sub-resource
"tagging")))
1054 (submit-request request
)))