Add create-file-handler, fixes #395

This commit is contained in:
Tommi Reiman 2020-05-09 18:22:57 +03:00
parent 711c1fea8f
commit fd0d7cc46c
3 changed files with 200 additions and 169 deletions

View file

@ -30,6 +30,7 @@ is called the first time, so that `rfe/push-state` and such can be called
### `reitit-ring` ### `reitit-ring`
* `reitit.ring/routes` strips away `nil` routes, fixes [#394](https://github.com/metosin/reitit/issues/394) * `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) ## 0.4.2 (2020-01-17)

View file

@ -189,6 +189,48 @@
;; TODO: ring.middleware.not-modified/wrap-not-modified ;; TODO: ring.middleware.not-modified/wrap-not-modified
;; TODO: ring.middleware.head/wrap-head ;; TODO: ring.middleware.head/wrap-head
;; TODO: handle etags ;; 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 (defn create-resource-handler
"A ring handler for serving classpath resources, configured via options: "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)" | :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)"
([] ([]
(create-resource-handler nil)) (create-resource-handler nil))
([{:keys [parameter root path loader allow-symlinks? index-files paths not-found-handler] ([opts]
:or {parameter (keyword "") (-create-file-or-resource-handler response/resource-response opts))))
root "public"
index-files ["index.html"] #?(:clj
paths (constantly nil) (defn create-file-handler
not-found-handler (constantly {:status 404, :body "", :headers {}})}}] "A ring handler for serving file resources, configured via options:
(let [options {:root root, :loader loader, :allow-symlinks? allow-symlinks?}
path-size (count path) | key | description |
create (fn [handler] | -------------------|-------------|
(fn | :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:`
([request] (handler request)) | :root | optional resource root, defaults to `\"public\"`
([request respond _] (respond (handler request))))) | :path | optional path to mount the handler to. Works only if mounted outside of a router.
join-paths (fn [& paths] | :loader | optional class loader to resolve the resources
(str/replace (str/replace (str/join "/" paths) #"([/]+)" "/") #"/$" "")) | :index-files | optional vector of index-files to look in a resource directory, defaults to `[\"index.html\"]`
resource-response (fn [path] | :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)"
(if-let [response (or (paths (join-paths "/" path)) ([]
(response/resource-response path options))] (create-file-handler nil))
(response/content-type response (mime-type/ext-mime-type path)))) ([opts]
path-or-index-response (fn [path uri] (-create-file-or-resource-handler response/file-response opts))))
(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)))))
(defn create-enrich-request [inject-match? inject-router?] (defn create-enrich-request [inject-match? inject-router?]
(cond (cond

View file

@ -454,158 +454,163 @@
(app request))))))) (app request)))))))
#?(:clj #?(:clj
(deftest resource-handler-test (deftest file-resource-handler-test
(let [redirect (fn [uri] {:status 302, :body "", :headers {"Location" uri}}) (let [redirect (fn [uri] {:status 302, :body "", :headers {"Location" uri}})
request (fn [uri] {:uri uri, :request-method :get})] request (fn [uri] {:uri uri, :request-method :get})]
(testing "inside a router"
(testing "from root" (doseq [[name create] [["resource-handler" ring/create-resource-handler]
(let [app (ring/ring-handler ["file-handler" #(ring/create-file-handler (assoc % :root "dev-resources/public"))]]]
(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 (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
(testing "index-files" (testing (str "for " name)
(let [response (app (request "/docs"))] (testing "inside a router"
(is (= (redirect "/docs/index.html") response)))
(let [response (app (request "/docs/"))]
(is (= (redirect "/docs/index.html") response))))
(testing "not found" (testing "from root"
(let [response (app (request "/not-found"))] (let [app (ring/ring-handler
(is (= 404 (:status response))))) (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 (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
(testing "3-arity" (testing "index-files"
(let [result (atom nil) (let [response (app (request "/docs"))]
respond (partial reset! result) (is (= (redirect "/docs/index.html") response)))
raise ::not-called] (let [response (app (request "/docs/"))]
(app (request "/hello.xml") respond raise) (is (= (redirect "/docs/index.html") response))))
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
(is (get-in @result [:headers "Last-Modified"]))
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result)))))))))
(testing "from path" (testing "not found"
(let [app (ring/ring-handler (let [response (app (request "/not-found"))]
(ring/router (is (= 404 (:status response)))))
["/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 (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
(testing "index-files" (testing "3-arity"
(let [response (app (request "/docs"))] (let [result (atom nil)
(is (= (redirect "/docs/index.html") response))) respond (partial reset! result)
(let [response (app (request "/docs/"))] raise ::not-called]
(is (= (redirect "/docs/index.html") response)))) (app (request "/hello.xml") respond raise)
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
(is (get-in @result [:headers "Last-Modified"]))
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result)))))))))
(testing "not found" (testing "from path"
(let [response (app (request "/not-found"))] (let [app (ring/ring-handler
(is (= 404 (:status response))))) (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 (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
(testing "3-arity" (testing "index-files"
(let [result (atom nil) (let [response (app (request "/docs"))]
respond (partial reset! result) (is (= (redirect "/docs/index.html") response)))
raise ::not-called] (let [response (app (request "/docs/"))]
(app (request "/hello.xml") respond raise) (is (= (redirect "/docs/index.html") response))))
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
(is (get-in @result [:headers "Last-Modified"]))
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result))))))))))
(testing "outside a router" (testing "not found"
(let [response (app (request "/not-found"))]
(is (= 404 (:status response)))))
(testing "from root" (testing "3-arity"
(let [app (ring/ring-handler (let [result (atom nil)
(ring/router []) respond (partial reset! result)
(ring/routes raise ::not-called]
(ring/create-resource-handler {:path "/"}) (app (request "/hello.xml") respond raise)
(ring/create-default-handler)))] (is (= "text/xml" (get-in @result [:headers "Content-Type"])))
(testing test (is (get-in @result [:headers "Last-Modified"]))
(testing "different file-types" (is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result))))))))))
(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 (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
(testing "index-files" (testing "outside a router"
(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" (testing "from root"
(let [response (app (request "/not-found"))] (let [app (ring/ring-handler
(is (= 404 (:status response))))) (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 (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
(testing "3-arity" (testing "index-files"
(let [result (atom nil) (let [response (app (request "/docs"))]
respond (partial reset! result) (is (= (redirect "/docs/index.html") response)))
raise ::not-called] (let [response (app (request "/docs/"))]
(app (request "/hello.xml") respond raise) (is (= (redirect "/docs/index.html") response))))
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
(is (get-in @result [:headers "Last-Modified"]))
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result)))))))))
(testing "from path" (testing "not found"
(let [app (ring/ring-handler (let [response (app (request "/not-found"))]
(ring/router []) (is (= 404 (:status response)))))
(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 (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
(testing "index-files" (testing "3-arity"
(let [response (app (request "/docs"))] (let [result (atom nil)
(is (= (redirect "/docs/index.html") response))) respond (partial reset! result)
(let [response (app (request "/docs/"))] raise ::not-called]
(is (= (redirect "/docs/index.html") response)))) (app (request "/hello.xml") respond raise)
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
(is (get-in @result [:headers "Last-Modified"]))
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result)))))))))
(testing "not found" (testing "from path"
(let [response (app (request "/not-found"))] (let [app (ring/ring-handler
(is (= 404 (:status response))))) (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 (= "<xml><hello>file</hello></xml>\n" (slurp (:body response))))))
(testing "3-arity" (testing "index-files"
(let [result (atom nil) (let [response (app (request "/docs"))]
respond (partial reset! result) (is (= (redirect "/docs/index.html") response)))
raise ::not-called] (let [response (app (request "/docs/"))]
(app (request "/hello.xml") respond raise) (is (= (redirect "/docs/index.html") response))))
(is (= "text/xml" (get-in @result [:headers "Content-Type"])))
(is (get-in @result [:headers "Last-Modified"])) (testing "not found"
(is (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result))))))))))))) (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 (= "<xml><hello>file</hello></xml>\n" (slurp (:body @result)))))))))))))))
(deftest router-available-in-default-branch (deftest router-available-in-default-branch
(testing "1-arity" (testing "1-arity"