Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add initial support for GridFS with new fns insert-file!, fetch-files…
…, destroy-file! etc.

Implementation notes:

This is the bare minimum code to get data in and out of GridFS.

The collections that back GridFS internally use an ObjectClass of
GridFSDBFile, so the regular ClojureDBObject hack can't be used to get
painless coercion. To reuse the fast conversion code in
ClojureDBObject for this purpose, I've made public the static method
ClojureDBObject/toClojureMap. The :gridfs hint for 'coerce' seemed less
messy than leaking another function into congomongo.clj.

Some reflection will probably be used in insert-file! and
write-file-to, but this seems preferable than repeating the list of
possible argument types for the wrapped overloaded methods.

There's no atomic update support in GridFS, but an update-file! fn can
be written, together with wrappers for other GridFS functionality,
such as listing all files in a bucket.

The Java GridFS implementation curiously leaves the uploadDate
attribute of files blank by default, in addition to the contentType
field. It may be desirable to fill those fields in congomongo, but I
haven't done that at this stage.
  • Loading branch information
purcell committed Jan 18, 2010
1 parent 25f81c9 commit 6bf22de
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 6 deletions.
68 changes: 66 additions & 2 deletions src/clj/somnium/congomongo.clj
Expand Up @@ -28,6 +28,7 @@
[clojure.contrib.json read write])
(:import [com.mongodb Mongo DB DBCollection DBObject]
[com.mongodb.util JSON]
[com.mongodb.gridfs GridFS]
[somnium.congomongo ClojureDBObject]))

(defunk mongo!
Expand Down Expand Up @@ -181,7 +182,70 @@
(.getCollectionNames #^DB (:db @*mongo-config*)))

(defn drop-coll!
[collection]
"Permanently deletes a collection. Use with care."
[collection]
(.drop #^DBCollection (.getCollection #^DB (:db @*mongo-config*)
#^String (named collection))))
#^String (named collection))))

;;; GridFS


(definline get-gridfs
"Returns a GridFS object for the named bucket"
[bucket]
`(GridFS. #^DB (:db @*mongo-config*) #^String (named ~bucket)))

;; The naming of :contentType is ugly, but consistent with that
;; returned by GridFSFile
(defunk insert-file!
"Insert file data into a GridFS. Data should be either a File,
InputStream or byte array.
Options include:
:filename -> defaults to nil
:contentType -> defaults to nil
:metadata -> defaults to nil"
{:arglists '(fs data {:filename nil :contentType nil :metadata nil})}
[fs data :filename nil :contentType nil :metadata nil]
(let [f (.createFile (get-gridfs fs) data)]
(if filename (.setFilename f filename))
(if contentType (.setContentType f contentType))
(if metadata (.putAll (.getMetaData f) (coerce metadata [:clojure :mongo])))
(.save f)
(coerce f [:gridfs :clojure])))

(defunk destroy-file!
"Removes file from gridfs. Takes a GridFS name and
a query map"
{:arglists '(fs where {:from :clojure})}
[fs q :from :clojure]
(.remove (get-gridfs fs)
#^DBObject (coerce q [from :mongo])))

(defunk fetch-files
"Fetches objects from a GridFS
Note that MongoDB always adds the _id and _ns
fields to objects returned from the database.
Optional arguments include
:where -> takes a query map
:from -> argument type, same options as above
:one? -> defaults to false, use fetch-one-file as a shortcut"
{:arglists
'([fs :where :from :one?])}
[fs :where {} :from :clojure :one? false]
(let [n-where (coerce where [from :mongo])
n-fs (get-gridfs fs)]
(if one?
(if-let [m (.findOne #^GridFS n-fs #^DBObject n-where)]
(coerce m [:gridfs :clojure]) nil)
(if-let [m (.find #^GridFS n-fs #^DBObject n-where)]
(coerce m [:gridfs :clojure] :many true) nil))))

(defn fetch-one-file [fs & options]
(apply fetch-files fs (concat options '[:one? true])))

(defn write-file-to
"Writes the data stored for a file to the supplied output, which
should be either an OutputStream, File, or the String path for a file."
[fs file out]
(if-let [f (.findOne (get-gridfs fs) (coerce file [:clojure :mongo]))]
(.writeTo f out)))
30 changes: 27 additions & 3 deletions src/clj/somnium/congomongo/coerce.clj
Expand Up @@ -5,13 +5,35 @@
[clojure.contrib.core :only [seqable?]])
(:import [somnium.congomongo ClojureDBObject]
[clojure.lang IPersistentMap]
[com.mongodb DBObject]
[com.mongodb.gridfs GridFSFile]
[com.mongodb.util JSON]))

(defvar *keywordize* true
"Set this to false to prevent ClojureDBObject from setting string keys to keywords")

(defunk
coerce

(defn- dbobject->clojure
"Not every DBObject returned from Mongo is a ClojureDBObject,
since we can't setObjectClass on the collections used to back GridFS;
those collections have GridFSDBFile as their object class.
This function uses ClojureDBObject to marshal such DBObjects
into Clojure structures; in practice, this applies only to GridFSFile
and its subclasses."
[#^DBObject f keywordize]
(ClojureDBObject/toClojureMap
(try (.toMap f)
;; DBObject provides .toMap, but the implementation in
;; subclass GridFSFile unhelpfully throws
;; UnsupportedOperationException
(catch UnsupportedOperationException e
(let [keys (.keySet f)]
(zipmap keys (map #(.get f %) keys)))))
keywordize))


(defunk coerce
{:arglists '([obj [:from :to] {:many false}])
:doc
"takes an object, a vector of keywords:
Expand All @@ -28,6 +50,7 @@
[:mongo :clojure] #(.toClojure #^ClojureDBObject %
#^Boolean/TYPE *keywordize*)
[:mongo :json ] #(.toString #^ClojureDBObject %)
[:gridfs :clojure] #(dbobject->map #^GridFSFile %)
[:json :clojure] #(binding [*json-keyword-keys* *keywordize*] (read-json %))
[:json :mongo ] #(JSON/parse %)
:else (throw (RuntimeException.
Expand All @@ -39,4 +62,5 @@
(defn coerce-fields
"only used for creating argument object for :only"
[fields]
(ClojureDBObject. #^IPersistentMap (zipmap fields (repeat 1))))
(ClojureDBObject. #^IPersistentMap (zipmap fields (repeat 1))))

2 changes: 1 addition & 1 deletion src/java/somnium/congomongo/ClojureDBObject.java
Expand Up @@ -91,7 +91,7 @@ public IPersistentMap toClojure(boolean keywordize) {
return toClojureMap(this, keywordize);
}

private static IPersistentMap toClojureMap(Map m, boolean keywordize){
public static IPersistentMap toClojureMap(Map m, boolean keywordize){
int msize = m.size() * 2;
Object[] ary = new Object[msize];
Set keys = m.keySet();
Expand Down
33 changes: 33 additions & 0 deletions test/congomongo_test.clj
Expand Up @@ -106,3 +106,36 @@
(add-index! :points [:x])
(is (some #(= (into {} (% "key")) {"x" 1})
(get-indexes :points)))))

(deftest gridfs-insert-and-fetch
(with-mongo
(is (empty? (fetch-files :testfs)))
(let [f (insert-file! :testfs (.getBytes "toasted")
:filename "muffin" :contentType "food/breakfast")]
(is (= "muffin" (:filename f)))
(is (= "food/breakfast" (:contentType f)))
(is (= 7 (:length f)))
(is (= nil (fetch-one-file :testfs :where {:filename "monkey"})))
(is (= f (fetch-one-file :testfs :where {:filename "muffin"})))
(is (= f (fetch-one-file :testfs :where {:contentType "food/breakfast"})))
(is (= (list f) (fetch-files :testfs))))))

(deftest gridfs-destroy
(with-mongo
(insert-file! :testfs (.getBytes "banana") :filename "lunch")
(destroy-file! :testfs {:filename "lunch"})
(is (empty? (fetch-files :testfs)))))

(deftest gridfs-insert-with-metadata
(with-mongo
(let [f (insert-file! :testfs (.getBytes "nuts")
:metadata { :calories 50, :opinion "tasty"})]
(is (= "tasty" (-> f :metadata :opinion)))
(is (= f (fetch-one-file :testfs :where { :metadata.opinion "tasty" }))))))

(deftest gridfs-write-file-to
(with-mongo
(let [f (insert-file! :testfs (.getBytes "banana"))]
(let [o (java.io.ByteArrayOutputStream.)]
(write-file-to :testfs f o)
(is (= "banana" (str o)))))))

0 comments on commit 6bf22de

Please sign in to comment.