diff --git a/CHANGELOG.md b/CHANGELOG.md
index b05c34a9..bad25715 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@ is called the first time, so that `rfe/push-state` and such can be called
### `reitit-ring`
* `reitit.ring/routes` strips away `nil` routes, fixes [#394](https://github.com/metosin/reitit/issues/394)
+* `reitit.ring/create-file-handler` to serve files from classpah, fixes [#395](https://github.com/metosin/reitit/issues/395)
## 0.4.2 (2020-01-17)
diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc
index f919b40c..a9cd7d40 100644
--- a/modules/reitit-ring/src/reitit/ring.cljc
+++ b/modules/reitit-ring/src/reitit/ring.cljc
@@ -189,6 +189,48 @@
;; TODO: ring.middleware.not-modified/wrap-not-modified
;; TODO: ring.middleware.head/wrap-head
;; TODO: handle etags
+ (defn -create-file-or-resource-handler
+ [response-fn {:keys [parameter root path loader allow-symlinks? index-files paths not-found-handler]
+ :or {parameter (keyword "")
+ root "public"
+ index-files ["index.html"]
+ paths (constantly nil)
+ not-found-handler (constantly {:status 404, :body "", :headers {}})}}]
+ (let [options {:root root
+ :loader loader
+ :index-files? false
+ :allow-symlinks? allow-symlinks?}
+ path-size (count path)
+ create (fn [handler]
+ (fn
+ ([request] (handler request))
+ ([request respond _] (respond (handler request)))))
+ join-paths (fn [& paths]
+ (str/replace (str/replace (str/join "/" paths) #"([/]+)" "/") #"/$" ""))
+ response (fn [path]
+ (if-let [response (or (paths (join-paths "/" path))
+ (response-fn path options))]
+ (response/content-type response (mime-type/ext-mime-type path))))
+ path-or-index-response (fn [path uri]
+ (or (response path)
+ (loop [[file & files] index-files]
+ (if file
+ (if (response (join-paths path file))
+ (response/redirect (join-paths uri file))
+ (recur files))))))
+ handler (if path
+ (fn [request]
+ (let [uri (:uri request)]
+ (if-let [path (if (>= (count uri) path-size) (subs uri path-size))]
+ (path-or-index-response path uri))))
+ (fn [request]
+ (let [uri (:uri request)
+ path (-> request :path-params parameter)]
+ (or (path-or-index-response path uri)
+ (not-found-handler request)))))]
+ (create handler))))
+
+#?(:clj
(defn create-resource-handler
"A ring handler for serving classpath resources, configured via options:
@@ -202,42 +244,25 @@
| :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)"
([]
(create-resource-handler nil))
- ([{:keys [parameter root path loader allow-symlinks? index-files paths not-found-handler]
- :or {parameter (keyword "")
- root "public"
- index-files ["index.html"]
- paths (constantly nil)
- not-found-handler (constantly {:status 404, :body "", :headers {}})}}]
- (let [options {:root root, :loader loader, :allow-symlinks? allow-symlinks?}
- path-size (count path)
- create (fn [handler]
- (fn
- ([request] (handler request))
- ([request respond _] (respond (handler request)))))
- join-paths (fn [& paths]
- (str/replace (str/replace (str/join "/" paths) #"([/]+)" "/") #"/$" ""))
- resource-response (fn [path]
- (if-let [response (or (paths (join-paths "/" path))
- (response/resource-response path options))]
- (response/content-type response (mime-type/ext-mime-type path))))
- path-or-index-response (fn [path uri]
- (or (resource-response path)
- (loop [[file & files] index-files]
- (if file
- (if (resource-response (join-paths path file))
- (response/redirect (join-paths uri file))
- (recur files))))))
- handler (if path
- (fn [request]
- (let [uri (:uri request)]
- (if-let [path (if (>= (count uri) path-size) (subs uri path-size))]
- (path-or-index-response path uri))))
- (fn [request]
- (let [uri (:uri request)
- path (-> request :path-params parameter)]
- (or (path-or-index-response path uri)
- (not-found-handler request)))))]
- (create handler)))))
+ ([opts]
+ (-create-file-or-resource-handler response/resource-response opts))))
+
+#?(:clj
+ (defn create-file-handler
+ "A ring handler for serving file resources, configured via options:
+
+ | key | description |
+ | -------------------|-------------|
+ | :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:`
+ | :root | optional resource root, defaults to `\"public\"`
+ | :path | optional path to mount the handler to. Works only if mounted outside of a router.
+ | :loader | optional class loader to resolve the resources
+ | :index-files | optional vector of index-files to look in a resource directory, defaults to `[\"index.html\"]`
+ | :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)"
+ ([]
+ (create-file-handler nil))
+ ([opts]
+ (-create-file-or-resource-handler response/file-response opts))))
(defn create-enrich-request [inject-match? inject-router?]
(cond
diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc
index 8710f178..8cc1fb2b 100644
--- a/test/cljc/reitit/ring_test.cljc
+++ b/test/cljc/reitit/ring_test.cljc
@@ -454,158 +454,163 @@
(app request)))))))
#?(:clj
- (deftest resource-handler-test
+ (deftest file-resource-handler-test
(let [redirect (fn [uri] {:status 302, :body "", :headers {"Location" uri}})
request (fn [uri] {:uri uri, :request-method :get})]
- (testing "inside a router"
- (testing "from root"
- (let [app (ring/ring-handler
- (ring/router
- ["/*" (ring/create-resource-handler)])
- (ring/create-default-handler))]
- (testing test
- (testing "different file-types"
- (let [response (app (request "/hello.json"))]
- (is (= "application/json" (get-in response [:headers "Content-Type"])))
- (is (get-in response [:headers "Last-Modified"]))
- (is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
- (let [response (app (request "/hello.xml"))]
- (is (= "text/xml" (get-in response [:headers "Content-Type"])))
- (is (get-in response [:headers "Last-Modified"]))
- (is (= "file\n" (slurp (:body response))))))
+ (doseq [[name create] [["resource-handler" ring/create-resource-handler]
+ ["file-handler" #(ring/create-file-handler (assoc % :root "dev-resources/public"))]]]
- (testing "index-files"
- (let [response (app (request "/docs"))]
- (is (= (redirect "/docs/index.html") response)))
- (let [response (app (request "/docs/"))]
- (is (= (redirect "/docs/index.html") response))))
+ (testing (str "for " name)
+ (testing "inside a router"
- (testing "not found"
- (let [response (app (request "/not-found"))]
- (is (= 404 (:status response)))))
+ (testing "from root"
+ (let [app (ring/ring-handler
+ (ring/router
+ ["/*" (create nil)])
+ (ring/create-default-handler))]
+ (testing test
+ (testing "different file-types"
+ (let [response (app (request "/hello.json"))]
+ (is (= "application/json" (get-in response [:headers "Content-Type"])))
+ (is (get-in response [:headers "Last-Modified"]))
+ (is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
+ (let [response (app (request "/hello.xml"))]
+ (is (= "text/xml" (get-in response [:headers "Content-Type"])))
+ (is (get-in response [:headers "Last-Modified"]))
+ (is (= "file\n" (slurp (:body response))))))
- (testing "3-arity"
- (let [result (atom nil)
- respond (partial reset! result)
- raise ::not-called]
- (app (request "/hello.xml") respond raise)
- (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
- (is (get-in @result [:headers "Last-Modified"]))
- (is (= "file\n" (slurp (:body @result)))))))))
+ (testing "index-files"
+ (let [response (app (request "/docs"))]
+ (is (= (redirect "/docs/index.html") response)))
+ (let [response (app (request "/docs/"))]
+ (is (= (redirect "/docs/index.html") response))))
- (testing "from path"
- (let [app (ring/ring-handler
- (ring/router
- ["/files/*" (ring/create-resource-handler)])
- (ring/create-default-handler))
- request #(request (str "/files" %))
- redirect #(redirect (str "/files" %))]
- (testing test
- (testing "different file-types"
- (let [response (app (request "/hello.json"))]
- (is (= "application/json" (get-in response [:headers "Content-Type"])))
- (is (get-in response [:headers "Last-Modified"]))
- (is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
- (let [response (app (request "/hello.xml"))]
- (is (= "text/xml" (get-in response [:headers "Content-Type"])))
- (is (get-in response [:headers "Last-Modified"]))
- (is (= "file\n" (slurp (:body response))))))
+ (testing "not found"
+ (let [response (app (request "/not-found"))]
+ (is (= 404 (:status response)))))
- (testing "index-files"
- (let [response (app (request "/docs"))]
- (is (= (redirect "/docs/index.html") response)))
- (let [response (app (request "/docs/"))]
- (is (= (redirect "/docs/index.html") response))))
+ (testing "3-arity"
+ (let [result (atom nil)
+ respond (partial reset! result)
+ raise ::not-called]
+ (app (request "/hello.xml") respond raise)
+ (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
+ (is (get-in @result [:headers "Last-Modified"]))
+ (is (= "file\n" (slurp (:body @result)))))))))
- (testing "not found"
- (let [response (app (request "/not-found"))]
- (is (= 404 (:status response)))))
+ (testing "from path"
+ (let [app (ring/ring-handler
+ (ring/router
+ ["/files/*" (create nil)])
+ (ring/create-default-handler))
+ request #(request (str "/files" %))
+ redirect #(redirect (str "/files" %))]
+ (testing test
+ (testing "different file-types"
+ (let [response (app (request "/hello.json"))]
+ (is (= "application/json" (get-in response [:headers "Content-Type"])))
+ (is (get-in response [:headers "Last-Modified"]))
+ (is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
+ (let [response (app (request "/hello.xml"))]
+ (is (= "text/xml" (get-in response [:headers "Content-Type"])))
+ (is (get-in response [:headers "Last-Modified"]))
+ (is (= "file\n" (slurp (:body response))))))
- (testing "3-arity"
- (let [result (atom nil)
- respond (partial reset! result)
- raise ::not-called]
- (app (request "/hello.xml") respond raise)
- (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
- (is (get-in @result [:headers "Last-Modified"]))
- (is (= "file\n" (slurp (:body @result))))))))))
+ (testing "index-files"
+ (let [response (app (request "/docs"))]
+ (is (= (redirect "/docs/index.html") response)))
+ (let [response (app (request "/docs/"))]
+ (is (= (redirect "/docs/index.html") response))))
- (testing "outside a router"
+ (testing "not found"
+ (let [response (app (request "/not-found"))]
+ (is (= 404 (:status response)))))
- (testing "from root"
- (let [app (ring/ring-handler
- (ring/router [])
- (ring/routes
- (ring/create-resource-handler {:path "/"})
- (ring/create-default-handler)))]
- (testing test
- (testing "different file-types"
- (let [response (app (request "/hello.json"))]
- (is (= "application/json" (get-in response [:headers "Content-Type"])))
- (is (get-in response [:headers "Last-Modified"]))
- (is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
- (let [response (app (request "/hello.xml"))]
- (is (= "text/xml" (get-in response [:headers "Content-Type"])))
- (is (get-in response [:headers "Last-Modified"]))
- (is (= "file\n" (slurp (:body response))))))
+ (testing "3-arity"
+ (let [result (atom nil)
+ respond (partial reset! result)
+ raise ::not-called]
+ (app (request "/hello.xml") respond raise)
+ (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
+ (is (get-in @result [:headers "Last-Modified"]))
+ (is (= "file\n" (slurp (:body @result))))))))))
- (testing "index-files"
- (let [response (app (request "/docs"))]
- (is (= (redirect "/docs/index.html") response)))
- (let [response (app (request "/docs/"))]
- (is (= (redirect "/docs/index.html") response))))
+ (testing "outside a router"
- (testing "not found"
- (let [response (app (request "/not-found"))]
- (is (= 404 (:status response)))))
+ (testing "from root"
+ (let [app (ring/ring-handler
+ (ring/router [])
+ (ring/routes
+ (create {:path "/"})
+ (ring/create-default-handler)))]
+ (testing test
+ (testing "different file-types"
+ (let [response (app (request "/hello.json"))]
+ (is (= "application/json" (get-in response [:headers "Content-Type"])))
+ (is (get-in response [:headers "Last-Modified"]))
+ (is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
+ (let [response (app (request "/hello.xml"))]
+ (is (= "text/xml" (get-in response [:headers "Content-Type"])))
+ (is (get-in response [:headers "Last-Modified"]))
+ (is (= "file\n" (slurp (:body response))))))
- (testing "3-arity"
- (let [result (atom nil)
- respond (partial reset! result)
- raise ::not-called]
- (app (request "/hello.xml") respond raise)
- (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
- (is (get-in @result [:headers "Last-Modified"]))
- (is (= "file\n" (slurp (:body @result)))))))))
+ (testing "index-files"
+ (let [response (app (request "/docs"))]
+ (is (= (redirect "/docs/index.html") response)))
+ (let [response (app (request "/docs/"))]
+ (is (= (redirect "/docs/index.html") response))))
- (testing "from path"
- (let [app (ring/ring-handler
- (ring/router [])
- (ring/routes
- (ring/create-resource-handler {:path "/files"})
- (ring/create-default-handler)))
- request #(request (str "/files" %))
- redirect #(redirect (str "/files" %))]
- (testing test
- (testing "different file-types"
- (let [response (app (request "/hello.json"))]
- (is (= "application/json" (get-in response [:headers "Content-Type"])))
- (is (get-in response [:headers "Last-Modified"]))
- (is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
- (let [response (app (request "/hello.xml"))]
- (is (= "text/xml" (get-in response [:headers "Content-Type"])))
- (is (get-in response [:headers "Last-Modified"]))
- (is (= "file\n" (slurp (:body response))))))
+ (testing "not found"
+ (let [response (app (request "/not-found"))]
+ (is (= 404 (:status response)))))
- (testing "index-files"
- (let [response (app (request "/docs"))]
- (is (= (redirect "/docs/index.html") response)))
- (let [response (app (request "/docs/"))]
- (is (= (redirect "/docs/index.html") response))))
+ (testing "3-arity"
+ (let [result (atom nil)
+ respond (partial reset! result)
+ raise ::not-called]
+ (app (request "/hello.xml") respond raise)
+ (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
+ (is (get-in @result [:headers "Last-Modified"]))
+ (is (= "file\n" (slurp (:body @result)))))))))
- (testing "not found"
- (let [response (app (request "/not-found"))]
- (is (= 404 (:status response)))))
+ (testing "from path"
+ (let [app (ring/ring-handler
+ (ring/router [])
+ (ring/routes
+ (create {:path "/files"})
+ (ring/create-default-handler)))
+ request #(request (str "/files" %))
+ redirect #(redirect (str "/files" %))]
+ (testing test
+ (testing "different file-types"
+ (let [response (app (request "/hello.json"))]
+ (is (= "application/json" (get-in response [:headers "Content-Type"])))
+ (is (get-in response [:headers "Last-Modified"]))
+ (is (= "{\"hello\": \"file\"}" (slurp (:body response)))))
+ (let [response (app (request "/hello.xml"))]
+ (is (= "text/xml" (get-in response [:headers "Content-Type"])))
+ (is (get-in response [:headers "Last-Modified"]))
+ (is (= "file\n" (slurp (:body response))))))
- (testing "3-arity"
- (let [result (atom nil)
- respond (partial reset! result)
- raise ::not-called]
- (app (request "/hello.xml") respond raise)
- (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
- (is (get-in @result [:headers "Last-Modified"]))
- (is (= "file\n" (slurp (:body @result)))))))))))))
+ (testing "index-files"
+ (let [response (app (request "/docs"))]
+ (is (= (redirect "/docs/index.html") response)))
+ (let [response (app (request "/docs/"))]
+ (is (= (redirect "/docs/index.html") response))))
+
+ (testing "not found"
+ (let [response (app (request "/not-found"))]
+ (is (= 404 (:status response)))))
+
+ (testing "3-arity"
+ (let [result (atom nil)
+ respond (partial reset! result)
+ raise ::not-called]
+ (app (request "/hello.xml") respond raise)
+ (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
+ (is (get-in @result [:headers "Last-Modified"]))
+ (is (= "file\n" (slurp (:body @result)))))))))))))))
(deftest router-available-in-default-branch
(testing "1-arity"