diff --git a/dev-resources/public/hello.json b/dev-resources/public/hello.json
new file mode 100644
index 00000000..a1cc6c1a
--- /dev/null
+++ b/dev-resources/public/hello.json
@@ -0,0 +1 @@
+{"hello": "file"}
\ No newline at end of file
diff --git a/dev-resources/public/hello.xml b/dev-resources/public/hello.xml
new file mode 100644
index 00000000..d3653a88
--- /dev/null
+++ b/dev-resources/public/hello.xml
@@ -0,0 +1 @@
+file
diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc
index 52da7a09..be7ba179 100644
--- a/modules/reitit-ring/src/reitit/ring.cljc
+++ b/modules/reitit-ring/src/reitit/ring.cljc
@@ -1,8 +1,11 @@
(ns reitit.ring
(:require [meta-merge.core :refer [meta-merge]]
[reitit.middleware :as middleware]
+ [reitit.ring.mime :as mime]
[reitit.core :as r]
- [reitit.impl :as impl]))
+ [reitit.impl :as impl]
+ #?(:clj
+ [clojure.java.io :as io])))
(def http-methods #{:get :head :post :put :delete :connect :options :trace :patch})
(defrecord Methods [get head post put delete connect options trace patch])
@@ -64,6 +67,48 @@
(respond (error-handler request)))
(respond (not-found request)))))))
+#?(:clj
+ (defn create-resource-handler
+ "A ring handler for handling classpath resources,
+ configured via options:
+
+ | key | description |
+ | -------------|-------------|
+ | :parameter | optional name of the wildcard parameter, defaults to `:`
+ | :root | optional resource root, defaults to `public`
+ | :mime-types | optional extension->mime-type mapping, defaults to `reitit.ring.mime/default-types`
+ | :path | optional path to mount the handler to. Works only outside of a router
+ "
+ ([]
+ (create-resource-handler nil))
+ ([{:keys [parameter root mime-types path]
+ :or {parameter (keyword "")
+ root "public"
+ mime-types mime/default-mime-types}}]
+ (let [response (fn [file]
+ {:status 200
+ :body file
+ :headers {"Content-Type" (mime/ext-mime-type (.getName file) mime-types)}})]
+ (if path
+ (let [path-size (count path)]
+ (fn
+ ([req]
+ (let [uri (:uri req)]
+ (if (and (>= (count uri) path-size))
+ (some->> (str root (subs uri path-size)) io/resource io/file response))))
+ ([req respond _]
+ (let [uri (:uri req)]
+ (if (and (>= (count uri) path-size))
+ (some->> (str root (subs uri path-size)) io/resource io/file response respond))))))
+ (fn
+ ([req]
+ (or (some->> req :path-params parameter (str root "/") io/resource io/file response)
+ {:status 404}))
+ ([req respond _]
+ (respond
+ (or (some->> req :path-params parameter (str root "/") io/resource io/file response)
+ {:status 404})))))))))
+
(defn ring-handler
"Creates a ring-handler out of a ring-router.
Supports both 1 (sync) and 3 (async) arities.
diff --git a/modules/reitit-ring/src/reitit/ring/mime.cljc b/modules/reitit-ring/src/reitit/ring/mime.cljc
new file mode 100644
index 00000000..3e7b6de3
--- /dev/null
+++ b/modules/reitit-ring/src/reitit/ring/mime.cljc
@@ -0,0 +1,99 @@
+(ns reitit.ring.mime
+ (:require [clojure.string :as str]))
+
+(def default-mime-types
+ "A map of file extensions to mime-types."
+ {"7z" "application/x-7z-compressed"
+ "aac" "audio/aac"
+ "ai" "application/postscript"
+ "appcache" "text/cache-manifest"
+ "asc" "text/plain"
+ "atom" "application/atom+xml"
+ "avi" "video/x-msvideo"
+ "bin" "application/octet-stream"
+ "bmp" "image/bmp"
+ "bz2" "application/x-bzip"
+ "class" "application/octet-stream"
+ "cer" "application/pkix-cert"
+ "crl" "application/pkix-crl"
+ "crt" "application/x-x509-ca-cert"
+ "css" "text/css"
+ "csv" "text/csv"
+ "deb" "application/x-deb"
+ "dart" "application/dart"
+ "dll" "application/octet-stream"
+ "dmg" "application/octet-stream"
+ "dms" "application/octet-stream"
+ "doc" "application/msword"
+ "dvi" "application/x-dvi"
+ "edn" "application/edn"
+ "eot" "application/vnd.ms-fontobject"
+ "eps" "application/postscript"
+ "etx" "text/x-setext"
+ "exe" "application/octet-stream"
+ "flv" "video/x-flv"
+ "flac" "audio/flac"
+ "gif" "image/gif"
+ "gz" "application/gzip"
+ "htm" "text/html"
+ "html" "text/html"
+ "ico" "image/x-icon"
+ "iso" "application/x-iso9660-image"
+ "jar" "application/java-archive"
+ "jpe" "image/jpeg"
+ "jpeg" "image/jpeg"
+ "jpg" "image/jpeg"
+ "js" "text/javascript"
+ "json" "application/json"
+ "lha" "application/octet-stream"
+ "lzh" "application/octet-stream"
+ "mov" "video/quicktime"
+ "m4v" "video/mp4"
+ "mp3" "audio/mpeg"
+ "mp4" "video/mp4"
+ "mpe" "video/mpeg"
+ "mpeg" "video/mpeg"
+ "mpg" "video/mpeg"
+ "oga" "audio/ogg"
+ "ogg" "audio/ogg"
+ "ogv" "video/ogg"
+ "pbm" "image/x-portable-bitmap"
+ "pdf" "application/pdf"
+ "pgm" "image/x-portable-graymap"
+ "png" "image/png"
+ "pnm" "image/x-portable-anymap"
+ "ppm" "image/x-portable-pixmap"
+ "ppt" "application/vnd.ms-powerpoint"
+ "ps" "application/postscript"
+ "qt" "video/quicktime"
+ "rar" "application/x-rar-compressed"
+ "ras" "image/x-cmu-raster"
+ "rb" "text/plain"
+ "rd" "text/plain"
+ "rss" "application/rss+xml"
+ "rtf" "application/rtf"
+ "sgm" "text/sgml"
+ "sgml" "text/sgml"
+ "svg" "image/svg+xml"
+ "swf" "application/x-shockwave-flash"
+ "tar" "application/x-tar"
+ "tif" "image/tiff"
+ "tiff" "image/tiff"
+ "ttf" "application/x-font-ttf"
+ "txt" "text/plain"
+ "webm" "video/webm"
+ "wmv" "video/x-ms-wmv"
+ "woff" "application/font-woff"
+ "xbm" "image/x-xbitmap"
+ "xls" "application/vnd.ms-excel"
+ "xml" "text/xml"
+ "xpm" "image/x-xpixmap"
+ "xwd" "image/x-xwindowdump"
+ "zip" "application/zip"})
+
+(defn file-ext [name]
+ (if-let [i (str/last-index-of name ".")]
+ (subs name (inc i))))
+
+(defn ext-mime-type [name mime-types]
+ (-> name file-ext mime-types))
diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc
index 8ab6273d..bc7a20c8 100644
--- a/test/cljc/reitit/ring_test.cljc
+++ b/test/cljc/reitit/ring_test.cljc
@@ -264,3 +264,41 @@
(let [app (create {::middleware/transform #(interleave % (repeat (middleware "debug")))})]
(is (= {:status 200, :body [:olipa "debug" :kerran "debug" :avaruus "debug" :ok]}
(app request)))))))
+
+#?(:clj
+ (deftest resource-handler-test
+ (doseq [[test app] [["inside a router"
+ (ring/ring-handler
+ (ring/router
+ [["/ping" (constantly {:status 200, :body "pong"})]
+ ["/files/*" (ring/create-resource-handler)]])
+ (ring/create-default-handler))]
+
+ ["outside of a router"
+ (ring/ring-handler
+ (ring/router
+ ["/ping" (constantly {:status 200, :body "pong"})])
+ (ring/routes
+ (ring/create-resource-handler {:path "/files"})
+ (ring/create-default-handler)))]]]
+
+ (testing test
+ (testing "different file-types"
+ (let [response (app {:uri "/files/hello.json", :request-method :get})]
+ (is (= "application/json" (get-in response [:headers "Content-Type"])))
+ (is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
+ (let [response (app {:uri "/files/hello.xml", :request-method :get})]
+ (is (= "text/xml" (get-in response [:headers "Content-Type"])))
+ (is (= "file\n" (slurp (:body response))))))
+
+ (testing "not found"
+ (let [response (app {:uri "/files/not-found", :request-method :get})]
+ (is (= 404 (:status response)))))
+
+ (testing "3-arity"
+ (let [result (atom nil)
+ respond (partial reset! result)
+ raise ::not-called]
+ (app {:uri "/files/hello.xml", :request-method :get} respond raise)
+ (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
+ (is (= "file\n" (slurp (:body @result))))))))))