a blog about web apps, Lisp, Rails and all the REST ...

using Lisp to store your data on Amazon S3

If you want to access your Amazon S3 account, take a look at CL-S3 from Sven Van Caekenberghe. It's simpler than wrestling with curl and the Amazon webservices authentication ;-)

What you need

CL-S3 has the following dependencies, most of which are other packages from Sven, and most are installable using ASDF-INSTALL

  • s-http-client, to speak the HTTP lingo
  • s-xml, no webservices without XML right?
  • s-utils, some utility code that CL-S3 uses
  • s-base64, al your base are belong to us!
  • KPAX, Sven's web framework (for URI-encoding functions) the latest version does not need KPAX
  • ironclad, Nathan Froyds nice cryptography package, providing MD5 functionality

Download CL-S3 from Amazon S3 itself and symlink its cl-s3.asd in your ASDF systems directory.

So fire up your favourite REPL (I'm using Lispworks Personal Edition for this example) and load it al up using ASDF:

CL-USER 3 > (setf (logical-pathname-translations "home")
              `(("**;*.*.*" ,(concatenate 'string (namestring (user-homedir-pathname)) "**/*.*"))))
               (("**;*.*.*" "/Users/tarkin/**/*.*"))

CL-USER 4 > (unless (member :asdf *features*)
              (load #p"home:Lisp;asdf;init-asdf"))
; Loading text file /Users/tarkin/lisp/asdf/init-asdf.lisp
;  Loading text file /Users/tarkin/lisp/asdf/asdf.lisp
;Pushed #P"/Users/tarkin/lisp/asdf/systems/" onto ASDF central registry

CL-USER 5 > (asdf:operate 'asdf:load-op :cl-s3)
; loading system definition from /Users/tarkin/lisp/asdf/systems/cl-s3.asd into
; Loading fasl file /Users/tarkin/Lisp/cl-s3/cl-s3-package.nfasl
; Loading fasl file /Users/tarkin/Lisp/cl-s3/cl-s3.nfasl

Giving CL-S3 the lowdown on your account

Set up your Amazon webservices access identifiers (access key id and secret access key id)

CL-S3 7 > (setf *access-key-id* "AAAAYKR2D401BBB09CCC")

CL-S3 4 > (setf *secret-access-key* "ooh_my_secret_is_very_secret")

Doing the deed

So now we should be able to query the service using CL-S3.

CL-S3 11 > (get-service)
;; CL-S3 GET http://s3.amazonaws.com/
((|ListAllMyBucketsResult| :|xmlns| "http://s3.amazonaws.com/doc/2006-03-01/") 
 (|Owner| (ID "beeec79bbda42e62b7ac74273effdabef1ddadf1b22b4c683cc91bf64d62f033") 
          (|DisplayName| "nickypeeters")) 
   (|Name| "lisp") 
   (|CreationDate| "2007-05-29T13:46:12.000Z"))))
((:X-AMZ-ID-2 . "wb2EU2Etuv8Mw6+Y9jonKPm6lf0uGMxfcspgwnKK0YRCYorteJ08FH1cmD5ZhtpU") 
 (:X-AMZ-REQUEST-ID . "07C15D88FD3EB8CD") (:DATE . "Tue, 29 May 2007 17:51:23 GMT") 
 (:CONTENT-TYPE . "application/xml") 
 (:TRANSFER-ENCODING . "chunked") 
 (:SERVER . "AmazonS3"))
#<URI http://s3.amazonaws.com:80>

Let's make a bucket to hold our objects in using put-bucket

CL-S3 15 > (put-bucket "zoetrope")
;; CL-S3 PUT http://s3.amazonaws.com/zoetrope
((:X-AMZ-ID-2 . "Dxl2qxeYXo6VvuOBscjnWEQaPZjaQlk97E+XLsL+DVRVmQMafEaRrpEnFrWyf5k9") 
 (:X-AMZ-REQUEST-ID . "BE93305AD58B1777") 
 (:DATE . "Tue, 29 May 2007 18:01:36 GMT") 
 (:LOCATION . "/zoetrope") 
 (:SERVER . "AmazonS3"))
#<URI http://s3.amazonaws.com:80/zoetrope>

Put a little text under the new bucket using put-object. You need to provide a key (e.g. filename) and the content-type of the object (e.g. text/plain). S3 returns also returns the uri of the new object.

CL-S3 16 > (put-object "zoetrope" 
            "Practical Common Lisp is a nice book on Common Lisp" 
;; CL-S3 PUT http://s3.amazonaws.com/zoetrope/practical-common-lisp
#<URI http://s3.amazonaws.com:80/zoetrope/practical-common-lisp>

So lets see if Amazon S3 really has our little text at http://s3.amazonaws.com:80/zoetrope/practical-common-lisp using an s-http-client request

CL-S3 17 > (s-http-client:do-http-request "http://s3.amazonaws.com:80/zoetrope/practical-common-lisp")
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
  <Message>Access Denied</Message><RequestId>4BA0925CDE61CC24</RequestId>

Oops ! S3 returns an AccessDenied error in XML. By default any object you put is for-your-eyes-only. So let's make an authenticated request using get-object

CL-S3 20 > (get-object "zoetrope" "practical-common-lisp")
;; CL-S3 GET http://s3.amazonaws.com/zoetrope/practical-common-lisp
"Practical Common Lisp is a nice book on Common Lisp"

Uploading a file and making it public

CL-S3 doesn't have a file-upload utility method, so let's use a quick slurp-file function. We set the object to public and read-only by using the canned access-policy with the x-amz-acl header of public-read

CL-S3 21 > (defun contents-of-file (pathname)
  (with-output-to-string (contents)
    (with-open-file (in pathname :direction :input)
      (s-utils:copy-stream in contents))))

CL-S3 22 > (put-object "zoetrope" "lispification.pdf" 
             (contents-of-file "/Users/tarkin/lispification.pdf") 
             :amz-headers '(("x-amz-acl" . "public-read")))
;; CL-S3 PUT http://s3.amazonaws.com/zoetrope/lispification.pdf
#<URI http://s3.amazonaws.com:80/zoetrope/lispification.pdf>

CL-S3 has 2 utility functions for downloading and uploading files

CL-S3 39 > (download-file "lisp" "lispification.pdf")
;; CL-S3 GET http://s3.amazonaws.com/lisp/lispification.pdf
#<STREAM::LATIN-1-FILE-STREAM /Users/tarkin/lispification.pdf>
#<URI http://s3.amazonaws.com:80/lisp/lispification.pdf>

You can see the function returns the file-stream where the file was written. You can optionally give a directory to save the file in. Uploading a file works in the same way.

CL-S3 40 > (upload-file "/Users/tarkin/Lisp/init_lispworks.lisp" "lisp" 
         :mime-type "text/plain" :acl "public-read")
;; CL-S3 PUT http://s3.amazonaws.com/lisp/init_lispworks.lisp
#<URI http://s3.amazonaws.com:80/lisp/init_lispworks.lisp>

Check out the CL-S3 API docs for more info.