Skip to content

Instantly share code, notes, and snippets.

@daveliepmann
Last active August 29, 2015 14:05
Show Gist options
  • Save daveliepmann/c30f3238699929d0dd69 to your computer and use it in GitHub Desktop.
Save daveliepmann/c30f3238699929d0dd69 to your computer and use it in GitHub Desktop.
Draft of minimal demonstration of uploading more than one file at a time to AWS S3. Complete finished repo at https://github.com/daveliepmann/aws-s3-upload-clj
(ns aws-s3-upload-clj.demo
(:use [hiccup page]
[ring.util.codec])
(:import (javax.crypto Mac)
(javax.crypto.spec SecretKeySpec))
(:require [compojure.core :refer [GET defroutes]]
[clj-time.core :as time]
[clj-time.format :as time-f]
[clojure.data.json :as json]
[compojure.route :as route]
[noir.util.middleware :refer [app-handler]]))
(def s3-bucket "insert-your-bucket-name-here") ;; FIXME
;; you'll need to set these on your own machine, obvs:
(def aws-access-key (System/getenv "AWS_ACCESS_KEY")) ;; FIXME
(def aws-secret-key (System/getenv "AWS_SECRET_KEY")) ;; FIXME
(defn expiration-date
"Given today's date as `yyyymmdd` and integer `d`, returns an expiry
date based on today plus that many days, formatted as date-time."
[yyyymmdd d]
(time-f/unparse (time-f/formatters :date-time)
(time/plus (time-f/parse
(time-f/formatters :basic-date)
yyyymmdd) (time/days d))))
(defn policy
"Given today's date as `yyyymmdd`, returns a base-64-encoded policy
document for AWS POST uploads to S3."
[yyyymmdd]
(ring.util.codec/base64-encode
(.getBytes (json/write-str { "expiration" (expiration-date yyyymmdd 1),
"conditions" [{"bucket" s3-bucket}
{"acl" "public-read"}
["starts-with", "$Content-Type", ""],
["starts-with", "$key" ""],
{"success_action_status" "201"}]})
"UTF-8")))
(defn hmac-sha1 [key string]
"Returns signature of `string` with a given `key` using SHA-1 HMAC."
(ring.util.codec/base64-encode
(.doFinal (doto (javax.crypto.Mac/getInstance "HmacSHA1")
(.init (javax.crypto.spec.SecretKeySpec. (.getBytes key) "HmacSHA1")))
(.getBytes string "UTF-8"))))
(defn upload-page
"Display form to allow user to upload multiple files to AWS S3."
[& [params]]
(html5
[:meta {:charset "UTF-8"}]
[:title "Batch uploads to AWS S3 using Clojure"]
[:body.upload
(let [policy (policy (time-f/unparse (time-f/formatters :basic-date) (time/now)))]
[:main
[:form#s3_upload
[:input#files {:type "file" :name "file" :multiple "multiple"}]]
[:button#upload_trigger
{:onclick "javascript:awsuplclj.upload_files();"}
"Upload files to AWS S3"]
[:form#s3_fields.hidden
{:action (str "http://" s3-bucket ".s3.amazonaws.com/")
:method "POST" :enctype "multipart/form-data"}
[:input {:type "hidden" :name "key"
:value "${filename}"}]
[:input {:type "hidden" :name "success_action_status"
:value "201"}]
[:input {:type "hidden" :name "acl"
:value "public-read"}]
[:input {:type "hidden" :name "policy"
:value policy}]
[:input {:type "hidden" :name "AWSAccessKeyId"
:value aws-access-key}]
[:input {:type "hidden" :name "signature"
:value (hmac-sha1 aws-secret-key policy)}]]])
(include-js "/js/cljs.js")]))
(defroutes aws-routes
(GET "/" [] (upload-page))
(route/resources "/")
(route/not-found "Not Found"))
(def app
(app-handler [aws-routes]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ns awsuplclj
(:require [ajax.core :as ajax :refer [POST]]
[dommy.core :as dommy]
[cljs.core.async :as async :refer [chan put!]]
[goog.events :as events]
[cljs-time.core :as t]
[cljs-time.format :as tf])
(:require-macros [cljs.core.async.macros :refer [go]])
(:use-macros [dommy.macros :only [node sel sel1]]))
(def upload-queue (atom []))
(defn listen
"Listen for events of `type` on element `el`.
Return asynchronous channel with event. Cribbed with love from
swannodette's cljs 101 core.async tutorial:
http://swannodette.github.io/2013/11/07/clojurescript-101/"
[el type]
(let [out (chan)]
(events/listen el type
(fn [e] (put! out e)))
out))
(defn enqueue-file!
"Add `file` to the upload queue."
[file]
(swap! upload-queue conj {:file file
:formdata (doto (js/FormData. (sel1 :form#s3_fields))
(.append "Content-Type" (.-type file))
(.append "file" file (str (.-name file))))}))
(defn upload-to-s3!
"Upload file-containing `form-data` to Amazon Web Services' Simple
Storage Service (AWS S3) using credentials in
`s3-template-form`. Report completion on asynchronous channel `ch`."
[form-data s3-template-form ch]
(let [req (js/XMLHttpRequest.)]
;; Status 4 means 'DONE', per https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest
(aset req "onreadystatechange" #(when (= (.-readyState req) 4)
(go (>! ch req))))
(.open req "POST" (dommy/attr s3-template-form :action))
(.send req form-data)))
(defn listen-for-files!
"Initialize a listener to detect the selection of files and add them
to the `upload-queue`."
[]
(go (while true
(doseq [file (array-seq (.-files (.-target (<! (listen (sel1 :input#files) "change")))))]
(enqueue-file! file)))))
(defn upload-files []
(go (let [c (chan 1)]
(doseq [f @upload-queue]
(upload-to-s3! (:formdata f) (sel1 :form#s3_fields) c)
(when (= 201 (.-status (<! c))) ;; Successfully uploaded this file
)))))
(aset js/document "onreadystatechange" #(when (= "complete" (. js/document -readyState))
(listen-for-files!)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment