Simple File Upload


Using a library called compojure-api we can easily write a REST api to upload a file. In the code example below we have a POST call at the URI /api/upload which will conduct the file upload for us.

wrap-multipart-params is a middleware that parses multipart request bodies into parameters. It is necessary to handle file uploads from web browsers.

Ring comes with two different multipart storage engines:
ring.middleware.multipart-params.byte-array/byte-array-store and
ring.middleware.multipart-params.temp-file/temp-file-store In our example we are using the temp-file-store.

Our upload-file function below uses :tempfile and :filename keys from the file object to save the file on disk.

(ns your-clojure-ns
  (:require [ring.util.http-response :refer :all]
            [compojure.api.sweet :refer :all]
            [schema.core :as s]
            [ring.swagger.upload :as upload])


(def resource-path "/tmp/")

(defn file-path [path & [filename]]
  (java.net.URLDecoder/decode
    (str path File/separator filename)
    "utf-8"))

(defn upload-file
  "Uploads a file to the target folder."
  [path {:keys [tempfile filename]}]
  (try
    (with-open [in (new FileInputStream tempfile)
                out (new FileOutputStream (file-path path filename))]
      (let [source (.getChannel in)
            dest   (.getChannel out)]
        (.transferFrom dest source 0 (.size source))
        (.flush out)))))
        
(def service-routes
  (context "/api" []
    :tags ["api"]
 
    (POST "/upload" []
      :multipart-params [file :- upload/TempFileUpload]
      :middleware       [upload/wrap-multipart-params]
      (ok (let [{:keys [filename tempfile]} file ]
            (upload-file resource-path tempfile filename)
            {:success true}))))

(def app
    (api 
     {:swagger {:ui "/swagger-ui"
             :spec "/swagger.json"
             :data {:info {:version "1.0.0"
                           :title "Sample API"
                           :description "Sample Services"}}}}
      service-routes))

Writing The Endpoint Test


Now comes the tricky part. How exactly would you simulate a file upload in a Clojure endpoint test? Ring-Mock does not have a native function to simulate a multipart upload. Therefore we must write our own function to simulate the output from ring's wrap-multipart-params.

(defn create-temp-file
  "Simulates the output from ring's wrap-multipart-params."
  [file-path]
  (let [filename-start (inc (last-index-of file-path "/"))
        file-extension-start (last-index-of file-path ".")
        file-name (subs file-path filename-start file-extension-start)
        file-extension (subs file-path file-extension-start)
        temp-file (java.io.File/createTempFile file-name file-extension)]
    (io/copy (io/file file-path) temp-file)
    temp-file))

This function will take the file resource you want to upload and generate a temp-file for you. We can use this to create the mock file content for our endpoint test.

(deftest upload-test
  (testing "Testing POST request to /api/upload"
    (let [filecontent {:tempfile (create-temp-file "./path/to/file.txt")
                       :content-type "text/plain",
                       :filename     "file.txt"}
          response (app (-> (assoc
                         (mock/request :post "/api/upload")
                         :params {:filecontent filecontent}
                         :multipart-params {"file" filecontent})))]
      (is (= (:status response) 200)))))