Last active
August 29, 2015 14:05
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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