Search code examples
mongodbclojureclojurescriptcompojuremonger

How can I stream GridFS files to web clients in Clojure/Monger?


I have a web app using Clojure, Clojurescript, and Monger. Documents are uploaded and stored in Mongo via GridFS. The same files can be requested for download; at the moment this is accomplished by writing the file to (the server's) disk and serving it as a static file, but this is somewhat awkward; how can I serve the file represented by the GridFS object directly in Clojure/Java? Routing is handled by Ring/Compojure.


Solution

  • It turns out that the Ring/Compojure infrastructure used in Luminus is able to return output-streams, allowing easy transmission of files without touching your drive.

    (ns my-app.routes.updown
      "File uploading and insertion into database"
      (:use compojure.core)
      (:require [my-app.db.core :as db] ;; all the db functions you see here are just wrappers for the Monger functions you'd expect
                [ring.util.response :as r]
                [ring.util.io :as io]))
    
    (defn make-file-stream
      "Takes an input stream--such as a MongoDBObject stream--and streams it"
      [file]  
      (io/piped-input-stream
       (fn [output-stream]
           (.writeTo file output-stream))))
    
    (defn download-file-by-id "Downloads the requested file, if privileges are allowed"
      [id-string]
      (let [mongo-file (db/find-file-by-id id-string)
            file-map (db/map-from-mongo-obj mongo-file)
            content-type (-> file-map :metadata :contentType)
            file-name (-> file-map :filename)]
            (-> mongo-file
                make-file-stream
                r/response
                (#(r/header % "Content-Disposition" ; to get the right default file-name
                            (str "attachment; filename=\"" file-name "\"")))
            (#(r/header % "Content-Type" content-type))))) ; final wrapper: offer the right "open with" dialogue
    
    
    ;; this is called by my main routes def like so:
    ;; (notice no extra wrappers needed, as wrapping took place in download-file-by-id)
    ;; (defroutes home-routes
    ;;     (GET "/files/:id-string" [id-string]
    ;;        (up/download-file-by-id id-string)))