diff --git a/dev/resources/dev.edn b/dev/resources/dev.edn
index 4733fa0..5f009e9 100644
--- a/dev/resources/dev.edn
+++ b/dev/resources/dev.edn
@@ -2,11 +2,7 @@
{:file-store #var asciinema.component.local-file-store/local-file-store
:exp-set #var asciinema.component.mem-expiring-set/mem-expiring-set}
:config
- {:app
- {:middleware
- {:functions {:stacktrace #var ring.middleware.stacktrace/wrap-stacktrace}
- :applied ^:replace [:not-found :webjars :ring-defaults :route-aliases :ring-logger :stacktrace]}}
- :http
+ {:http
{:port 4000}
:db
{:uri "jdbc:postgresql://localhost:15432/asciinema_development?user=vagrant"}
diff --git a/project.clj b/project.clj
index 0f74029..a4ba457 100644
--- a/project.clj
+++ b/project.clj
@@ -4,25 +4,19 @@
:min-lein-version "2.0.0"
:dependencies [[org.clojure/clojure "1.8.0"]
[com.stuartsierra/component "0.3.1"]
- [metosin/ring-http-response "0.8.0"]
[clj-time "0.13.0"]
[duct "0.8.2"]
- [compojure "1.5.1"]
- [metosin/compojure-api "1.1.10"]
+ [yada "1.2.0"]
+ [aleph "0.4.1"]
+ [bidi "2.0.12"]
[prismatic/schema "1.1.3"]
[environ "1.1.0"]
[ring "1.5.0"]
- [ring/ring-defaults "0.2.1"]
- [ring-jetty-component "0.3.1"]
- [ring-webjars "0.1.1"]
- [ring-logger-timbre "0.7.5"]
[clj-bugsnag "0.2.9"]
[clj-aws-s3 "0.3.10" :exclusions [joda-time com.fasterxml.jackson.core/jackson-core com.fasterxml.jackson.core/jackson-annotations]]
- [aleph "0.4.1"]
[pandect "0.6.1"]
[com.taoensso/carmine "2.15.1"]
[org.slf4j/slf4j-nop "1.7.21"]
- [org.webjars/normalize.css "3.0.2"]
[duct/hikaricp-component "0.1.0"]
[org.postgresql/postgresql "9.4.1211"]
[duct/ragtime-component "0.1.4"]]
diff --git a/resources/asciinema/system.edn b/resources/asciinema/system.edn
index eaebc66..aded10c 100644
--- a/resources/asciinema/system.edn
+++ b/resources/asciinema/system.edn
@@ -1,6 +1,5 @@
{:components
- {:app #var duct.component.handler/handler-component
- :http #var asciinema.component.aleph/aleph-server
+ {:http #var asciinema.component.yada-listener/yada-listener
:db #var asciinema.component.db/hikaricp
:ragtime #var duct.component.ragtime/ragtime
:file-store #var asciinema.component.s3-file-store/s3-file-store
@@ -9,50 +8,11 @@
:endpoints
{:asciicasts #var asciinema.endpoint.asciicasts/asciicasts-endpoint}
:dependencies
- {:http [:app]
- :app [:asciicasts]
+ {:http {:app :asciicasts}
:ragtime [:db]
:asciicasts [:db :file-store :exp-set :executor]}
:config
- {:app
- {:middleware
- {:functions
- {:hide-errors #var duct.middleware.errors/wrap-hide-errors
- :not-found #var duct.middleware.not-found/wrap-not-found
- :ring-defaults #var ring.middleware.defaults/wrap-defaults
- :route-aliases #var duct.middleware.route-aliases/wrap-route-aliases
- :ring-logger #var ring.logger.timbre/wrap-with-logger
- :bugsnag #var clj-bugsnag.ring/wrap-bugsnag
- :webjars #var ring.middleware.webjars/wrap-webjars}
- :applied
- [:not-found :webjars :ring-defaults :route-aliases :ring-logger :bugsnag :hide-errors]
- :arguments
- {:not-found #resource "asciinema/errors/404.html"
- :hide-errors #resource "asciinema/errors/500.html"
- :bugsnag
- {:api-key bugsnag-key
- :environment env-name
- :version git-sha
- :project-ns "asciinema"}
- :route-aliases {"/" "/index.html"}
- :ring-defaults
- {:params {:urlencoded true
- :keywordize true
- :multipart true
- :nested true}
- :cookies true
- :session {:flash true
- :cookie-attrs {:http-only true}}
- :security {:anti-forgery true
- :xss-protection {:enable? true, :mode :block}
- :frame-options :sameorigin
- :content-type-options :nosniff}
- :static {:resources "asciinema/public"}
- :responses {:not-modified-responses true
- :absolute-redirects true
- :content-types true
- :default-charset "utf-8"}}}}}
- :http
+ {:http
{:port http-port}
:db
{:uri db-uri}
diff --git a/src/asciinema/boundary/file_store.clj b/src/asciinema/boundary/file_store.clj
index c9fad8c..da2d65d 100644
--- a/src/asciinema/boundary/file_store.clj
+++ b/src/asciinema/boundary/file_store.clj
@@ -5,4 +5,4 @@
(input-stream [this path])
(move-file [this old-path new-path])
(delete-file [this path])
- (serve-file [this path opts]))
+ (serve-file [this ctx path opts]))
diff --git a/src/asciinema/component/aleph.clj b/src/asciinema/component/aleph.clj
deleted file mode 100644
index a77bfbc..0000000
--- a/src/asciinema/component/aleph.clj
+++ /dev/null
@@ -1,17 +0,0 @@
-(ns asciinema.component.aleph
- (:require [com.stuartsierra.component :as component]
- [aleph.http :refer [start-server]]))
-
-(defrecord WebServer [port server app]
- component/Lifecycle
- (start [component]
- (let [handler (:handler app)
- server (start-server handler {:port port :join? false})]
- (assoc component :server server)))
- (stop [component]
- (when server
- (.close server)
- component)))
-
-(defn aleph-server [{:keys [port app]}]
- (map->WebServer {:port port :app app}))
diff --git a/src/asciinema/component/fixed_thread_executor.clj b/src/asciinema/component/fixed_thread_executor.clj
index fb7255a..7981882 100644
--- a/src/asciinema/component/fixed_thread_executor.clj
+++ b/src/asciinema/component/fixed_thread_executor.clj
@@ -21,7 +21,7 @@
(.execute executor f)
result)
(catch RejectedExecutionException _
- {:status 503 :headers {"Retry-After" "5"} :body "
503
"})))
+ nil)))
component/Lifecycle
(start [{:keys [threads queue-length] :as component}]
diff --git a/src/asciinema/component/local_file_store.clj b/src/asciinema/component/local_file_store.clj
index 21823a0..bf82363 100644
--- a/src/asciinema/component/local_file_store.clj
+++ b/src/asciinema/component/local_file_store.clj
@@ -27,11 +27,12 @@
(let [path (str base-path path)]
(io/delete-file path)))
- (serve-file [this path {:keys [filename]}]
- (let [resp (response/ok (file-store/input-stream this path))]
+ (serve-file [this ctx path {:keys [filename]}]
+ (let [path (str base-path path)
+ response (assoc (:response ctx) :body (io/file path))]
(if filename
- (response/header resp "Content-Disposition" (str "attachment; filename=" filename))
- resp))))
+ (update response :headers assoc "content-disposition" (str "attachment; filename=" filename))
+ response))))
(defn local-file-store [{:keys [path]}]
(->LocalFileStore path))
diff --git a/src/asciinema/component/s3_file_store.clj b/src/asciinema/component/s3_file_store.clj
index 4013f49..478892d 100644
--- a/src/asciinema/component/s3_file_store.clj
+++ b/src/asciinema/component/s3_file_store.clj
@@ -50,9 +50,12 @@
(let [path (str path-prefix path)]
(s3/delete-object cred bucket path)))
- (serve-file [this path opts]
- (let [path (str path-prefix path)]
- (response/found (generate-presigned-url cred bucket path opts)))))
+ (serve-file [this ctx path opts]
+ (let [path (str path-prefix path)
+ url (generate-presigned-url cred bucket path opts)]
+ (-> (:response ctx)
+ (assoc :status 302)
+ (update :headers assoc "location" url)))))
(defn s3-file-store [{:keys [cred bucket path-prefix]}]
(->S3FileStore cred bucket path-prefix))
diff --git a/src/asciinema/component/yada_listener.clj b/src/asciinema/component/yada_listener.clj
new file mode 100644
index 0000000..d2c8810
--- /dev/null
+++ b/src/asciinema/component/yada_listener.clj
@@ -0,0 +1,21 @@
+(ns asciinema.component.yada-listener
+ (:require [com.stuartsierra.component :as component]
+ [yada.yada :as yada]))
+
+(defrecord YadaListener [port server app]
+ component/Lifecycle
+ (start [component]
+ (if server
+ component
+ (let [handler (:routes app)
+ server (yada/listener handler {:port port})]
+ (assoc component :server server))))
+ (stop [component]
+ (if server
+ (do
+ ((:close server))
+ (assoc component :server nil))
+ component)))
+
+(defn yada-listener [{:keys [port app]}]
+ (map->YadaListener {:port port :app app}))
diff --git a/src/asciinema/endpoint/asciicasts.clj b/src/asciinema/endpoint/asciicasts.clj
index ae97d96..acd43d7 100644
--- a/src/asciinema/endpoint/asciicasts.clj
+++ b/src/asciinema/endpoint/asciicasts.clj
@@ -11,16 +11,15 @@
[clojure.java
[io :as io]
[shell :as shell]]
- [clojure.string :as str]
- [compojure.api.sweet :refer :all]
[environ.core :refer [env]]
- [ring.util.http-response :as response]
- [schema.core :as s]))
+ [schema.core :as s]
+ [yada.yada :as yada]))
-(defn exception-handler [^Exception e data request]
- (throw e))
+(def Theme (apply s/enum asciicast/themes))
+
+(def png-ttl-days 7)
-(defn a2png [in-url out-path {:keys [snapshot-at theme scale]}]
+(defn- a2png [in-url out-path {:keys [snapshot-at theme scale]}]
(let [a2png-bin (:a2png-bin env "a2png/a2png.sh")
{:keys [exit] :as result} (shell/sh a2png-bin
"-t" theme
@@ -31,53 +30,74 @@
(when-not (zero? exit)
(throw (ex-info "a2png error" result)))))
-(def Num (s/if #(str/includes? % ".")
- Double
- s/Int))
+(defn- generate-png [file-store exp-set asciicast png-params png-store-path]
+ (with-tmp-dir [dir "asciinema-png-"]
+ (let [json-store-path (asciicast/json-store-path asciicast)
+ json-local-path (str dir "/asciicast.json")
+ png-local-path (str dir "/asciicast.png")
+ expires (-> png-ttl-days t/days t/from-now)]
+ (with-open [in (fstore/input-stream file-store json-store-path)]
+ (let [out (io/file json-local-path)]
+ (io/copy in out)))
+ (a2png json-local-path png-local-path png-params)
+ (fstore/put-file file-store (io/file png-local-path) png-store-path)
+ (exp-set/conj! exp-set png-store-path expires))))
-(def Theme (apply s/enum asciicast/themes))
+(defn- service-unavailable-response [ctx]
+ (-> (:response ctx)
+ (assoc :status 503)
+ (update :headers assoc "retry-after" "5")))
-(def png-ttl-days 7)
+(defn- async-response [ctx executor f]
+ (or (executor/execute executor f)
+ (service-unavailable-response ctx)))
-(defn asciicasts-endpoint [{:keys [db file-store exp-set executor]}]
- (api
- {:exceptions {:handlers {:compojure.api.exception/default exception-handler}}}
- (context
- "/a" []
- (GET "/:token.json" []
- :path-params [token :- String]
- :query-params [{dl :- s/Bool false}]
- (if-let [asciicast (adb/get-asciicast-by-token db token)]
- (let [path (asciicast/json-store-path asciicast)
- filename (str "asciicast-" (:id asciicast) ".json")]
- (fstore/serve-file file-store path (when dl {:filename filename})))
- (response/not-found)))
+(defn asciicast-json-resource [db file-store]
+ (yada/resource {:produces "application/json"
+ :parameters {:path {:token String}
+ :query {(s/optional-key :dl) s/Bool}}
+ :properties (fn [ctx]
+ (if-let [asciicast (adb/get-asciicast-by-token db (-> ctx :parameters :path :token))]
+ {:exists? true
+ ::asciicast asciicast}
+ {:exists? false}))
+ :response (fn [ctx]
+ (let [asciicast (-> ctx :properties ::asciicast)
+ dl (-> ctx :parameters :query :dl)
+ path (asciicast/json-store-path asciicast)
+ filename (str "asciicast-" (:id asciicast) ".json")]
+ (fstore/serve-file file-store ctx path (when dl {:filename filename}))))}))
- (GET "/:token.png" []
- :path-params [token :- String]
- :query-params [{time :- Num nil}
- {theme :- Theme nil}
- {scale :- (s/enum "1" "2") nil}]
- (if-let [asciicast (adb/get-asciicast-by-token db token)]
- (let [user (udb/get-user-by-id db (:user_id asciicast))
- png-params (cond-> (asciicast/png-params asciicast user)
- time (assoc :snapshot-at time)
- theme (assoc :theme theme)
- scale (assoc :scale (Integer/parseInt scale)))
- png-store-path (asciicast/png-store-path asciicast png-params)]
- (if (exp-set/contains? exp-set png-store-path)
- (fstore/serve-file file-store png-store-path {})
- (executor/execute executor (fn []
- (with-tmp-dir [dir "asciinema-png-"]
- (let [json-store-path (asciicast/json-store-path asciicast)
- json-local-path (str dir "/asciicast.json")
- png-local-path (str dir "/asciicast.png")
- expires (-> png-ttl-days t/days t/from-now)]
- (with-open [in (fstore/input-stream file-store json-store-path)]
- (let [out (io/file json-local-path)]
- (io/copy in out)))
- (a2png json-local-path png-local-path png-params)
- (fstore/put-file file-store (io/file png-local-path) png-store-path)
- (exp-set/conj! exp-set png-store-path expires)))
- (fstore/serve-file file-store png-store-path {})))))
- (response/not-found))))))
+(defn asciicast-png-resource [db file-store exp-set executor]
+ (yada/resource {:produces "image/png"
+ :parameters {:path {:token String}
+ :query {(s/optional-key :time) s/Num
+ (s/optional-key :theme) Theme
+ (s/optional-key :scale) (s/enum "1" "2")}}
+ :properties (fn [ctx]
+ (if-let [asciicast (adb/get-asciicast-by-token db (-> ctx :parameters :path :token))]
+ (let [user (udb/get-user-by-id db (:user_id asciicast))
+ {:keys [time theme scale]} (-> ctx :parameters :query)
+ png-params (cond-> (asciicast/png-params asciicast user)
+ time (assoc :snapshot-at time)
+ theme (assoc :theme theme)
+ scale (assoc :scale (Integer/parseInt scale)))]
+ {:exists? true
+ :version (asciicast/png-version asciicast png-params)
+ ::asciicast asciicast
+ ::png-params png-params})
+ {:exists? false}))
+ :response (fn [ctx]
+ (let [asciicast (-> ctx :properties ::asciicast)
+ png-params (-> ctx :properties ::png-params)
+ png-store-path (asciicast/png-store-path asciicast png-params)]
+ (if (exp-set/contains? exp-set png-store-path)
+ (fstore/serve-file file-store ctx png-store-path {})
+ (async-response ctx executor (fn []
+ (generate-png file-store exp-set asciicast png-params png-store-path)
+ (fstore/serve-file file-store ctx png-store-path {}))))))}))
+
+(defn asciicasts-endpoint [{:keys [db file-store exp-set executor]}]
+ ["" [["/a/" [[[:token ".json"] (asciicast-json-resource db file-store)]
+ [[:token ".png"] (asciicast-png-resource db file-store exp-set executor)]]]
+ [true (yada/as-resource nil)]]])
diff --git a/src/asciinema/model/asciicast.clj b/src/asciinema/model/asciicast.clj
index bd2bc19..8d28d11 100644
--- a/src/asciinema/model/asciicast.clj
+++ b/src/asciinema/model/asciicast.clj
@@ -25,7 +25,7 @@
:theme (theme-name asciicast user)
:scale default-png-scale})
-(defn- png-version [asciicast params]
+(defn png-version [asciicast params]
(let [attrs (assoc params :id (:id asciicast))]
(->> attrs
(map (fn [[k v]] (str (name k) "=" v)))