Monday, January 20, 2014

Azure Blob Storage from Clojure

I need to write some files to Windows Azure Blob Storage. Windows Azure provides libraries in several languages and there is also a REST API.

Each version of the library comes with very good documentation. It explains how blob storage works, explains how to create an account and walks you through adding, downloading and deleting the blobs. The Java version is at http://www.windowsazure.com/en-us/documentation/articles/storage-java-how-to-use-blob-storage/

The project I am working on is in Clojure. Translating the examples is pretty straight forward.

The Java libraries are on Maven Central, so I added a dependency to my project.clj.

[com.microsoft.windowsazure/microsoft-windowsazure-api "0.4.6"]

The classes we need to import classes from the windowsazure services.core.storage package and the services.blob.client packages. The examples also use several classes from java.io. I also created a config.clj file in my src directory to hold my account name and key. So, my core.clj ns macro is:

(ns azure-blob-test.core
  (:require [azure-blob-test.config :as config])
  (:import [com.microsoft.windowsazure.services.core.storage CloudStorageAccount]
           [com.microsoft.windowsazure.services.blob.client CloudBlobClient CloudBlobContainer CloudBlockBlob CloudBlob BlobContainerPermissions BlobContainerPublicAccessType]
           [java.io File FileInputStream FileOutputStream]))

Each of the examples begin with creating a reference to a blob container. I think they are doing that so that is someone just looks at how to add a blob, they have a complete example. Really, you just create it once:

(def conn-str
  (str "DefaultEndpointsProtocol=http;"
         "AccountName=" config/account-name
         ";AccountKey=" config/account-key ";"))

(def container
  (-> conn-str
      CloudStorageAccount/parse
      .createCloudBlobClient
      (.getContainerReference "mycontainer")))

(.createIfNotExist container)

Setting the container permissions looks clunky to me in both languages. Setting public access means that people do not need an account key to access the folder. Actually, not what I want for my application, but for the sake of completeness:

(let [container-permissions (BlobContainerPermissions.)]
  (.setPublicAccess container-permissions BlobContainerPublicAccessType/CONTAINER)
  (.uploadPermissions container container-permissions))

For testing the upload, I decided to use project.clj, since I know it will be in the root folder when I am running in the REPL.

(let [blob-ref (.getBlockBlobReference container "project.clj")
      source-file (File. "project.clj")]
  (.upload blob-ref (FileInputStream. source-file) (.length source-file)))

Rather than print out a list of blobs in a container, I decided to write a function that returns a sequence of blobs. The filter checks for instances of CloudBlob to exclude virtual directories. And then I use that function to download all of the blobs. I append .bak to the filenames because I don't want to overwrite my project.clj, even if it is the same (superstitious?).

(defn blob-list [container]
  (filter #(instance? CloudBlob %) (.listBlobs container)))

(doseq [blob (blob-list container)]
  (.download blob (FileOutputStream. (str (.getName blob) ".bak"))))

And finally to clean up:

;; Delete the blob
(.delete (.getBlockBlobReference container "project.clj"))

;; Delete container
(.delete container)

No comments:

Post a Comment