From 137ee108c5be73176a1ffb3eaf3c15b906322cd0 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Wed, 1 Mar 2017 16:20:26 +0100 Subject: [PATCH] Refactor png generation --- resources/asciinema/system.edn | 5 +- src/asciinema/boundary/png_generator.clj | 4 + src/asciinema/component/a2png.clj | 30 +++++++ src/asciinema/endpoint/asciicasts.clj | 108 ++++++++++------------- src/asciinema/util/io.clj | 11 ++- 5 files changed, 94 insertions(+), 64 deletions(-) create mode 100644 src/asciinema/boundary/png_generator.clj create mode 100644 src/asciinema/component/a2png.clj diff --git a/resources/asciinema/system.edn b/resources/asciinema/system.edn index aded10c..3afef71 100644 --- a/resources/asciinema/system.edn +++ b/resources/asciinema/system.edn @@ -4,13 +4,14 @@ :ragtime #var duct.component.ragtime/ragtime :file-store #var asciinema.component.s3-file-store/s3-file-store :exp-set #var asciinema.component.redis-client/redis-client + :png-gen #var asciinema.component.a2png/a2png :executor #var asciinema.component.fixed-thread-executor/fixed-thread-executor} :endpoints {:asciicasts #var asciinema.endpoint.asciicasts/asciicasts-endpoint} :dependencies {:http {:app :asciicasts} :ragtime [:db] - :asciicasts [:db :file-store :exp-set :executor]} + :asciicasts [:db :file-store :exp-set :executor :png-gen]} :config {:http {:port http-port} @@ -26,6 +27,8 @@ :exp-set {:host redis-host :port redis-port} + :png-gen + {:bin-path "a2png/a2png.sh"} :executor {:threads 1 :queue-length 1}}} diff --git a/src/asciinema/boundary/png_generator.clj b/src/asciinema/boundary/png_generator.clj new file mode 100644 index 0000000..c975c29 --- /dev/null +++ b/src/asciinema/boundary/png_generator.clj @@ -0,0 +1,4 @@ +(ns asciinema.boundary.png-generator) + +(defprotocol PngGenerator + (generate [this json-is png-params])) diff --git a/src/asciinema/component/a2png.clj b/src/asciinema/component/a2png.clj new file mode 100644 index 0000000..b5cff40 --- /dev/null +++ b/src/asciinema/component/a2png.clj @@ -0,0 +1,30 @@ +(ns asciinema.component.a2png + (:require [asciinema.boundary.png-generator :as png-generator] + [asciinema.util.io :refer [cleanup-input-stream create-tmp-dir]] + [clojure.java + [io :as io] + [shell :as shell]])) + +(defn- exec-a2png [bin-path in-url out-path {:keys [snapshot-at theme scale]}] + (let [{:keys [exit] :as result} (shell/sh bin-path + "-t" theme + "-s" (str scale) + in-url + out-path + (str snapshot-at))] + (when-not (zero? exit) + (throw (ex-info "a2png error" result))))) + +(defrecord A2png [bin-path] + png-generator/PngGenerator + (generate [this json-is png-params] + (let [dir (create-tmp-dir "a2png-") + cleanup #(shell/sh "rm" "-rf" (.getPath dir)) + json-local-path (str dir "/asciicast.json") + png-local-path (str dir "/asciicast.png")] + (io/copy json-is (io/file json-local-path)) + (exec-a2png bin-path json-local-path png-local-path png-params) + (cleanup-input-stream (io/input-stream png-local-path) cleanup)))) + +(defn a2png [{:keys [bin-path]}] + (->A2png bin-path)) diff --git a/src/asciinema/endpoint/asciicasts.clj b/src/asciinema/endpoint/asciicasts.clj index 0a038b5..e7ecd38 100644 --- a/src/asciinema/endpoint/asciicasts.clj +++ b/src/asciinema/endpoint/asciicasts.clj @@ -4,46 +4,16 @@ [executor :as executor] [expiring-set :as exp-set] [file-store :as fstore] + [png-generator :as png] [user-database :as udb]] [asciinema.model.asciicast :as asciicast] - [asciinema.util.io :refer [with-tmp-dir]] - [asciinema.yada :refer [resource not-found-model]] + [asciinema.yada :refer [not-found-model resource]] [clj-time.core :as t] - [clojure.java - [io :as io] - [shell :as shell]] - [environ.core :refer [env]] [schema.core :as s] [yada.yada :as yada])) (def Theme (apply s/enum asciicast/themes)) -(def png-ttl-days 7) - -(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 - "-s" (str scale) - in-url - out-path - (str snapshot-at))] - (when-not (zero? exit) - (throw (ex-info "a2png error" result))))) - -(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)))) - (defn- service-unavailable-response [ctx] (-> (:response ctx) (assoc :status 503) @@ -69,36 +39,52 @@ filename (str "asciicast-" (:id asciicast) ".json")] (fstore/serve-file file-store ctx path (when dl {:filename filename}))))})) -(defn asciicast-png-resource [db file-store exp-set executor] +(def png-ttl-days 7) + +(defn asciicast-png-resource [db file-store exp-set executor png-gen] (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)))] - {: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 {}))))))})) + {: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)))] + {: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) + expires (-> png-ttl-days t/days t/from-now)] + (if (exp-set/contains? exp-set png-store-path) + (fstore/serve-file file-store ctx png-store-path {}) + (async-response ctx + executor + (fn [] + (let [json-store-path (asciicast/json-store-path asciicast)] + (with-open [json-is (fstore/input-stream file-store json-store-path) + png-is (png/generate png-gen json-is png-params)] + (fstore/put-file file-store png-is png-store-path))) + (exp-set/conj! exp-set png-store-path expires) + (fstore/serve-file file-store ctx png-store-path {}))))))})) -(defn asciicasts-endpoint [{:keys [db file-store exp-set executor]}] +(defn asciicasts-endpoint [{:keys [db file-store exp-set executor png-gen]}] ["" [["/a/" [[[:token ".json"] (asciicast-json-resource db file-store)] - [[:token ".png"] (asciicast-png-resource db file-store exp-set executor)]]] + [[:token ".png"] (asciicast-png-resource db file-store exp-set executor png-gen)]]] [true (yada/resource not-found-model)]]]) diff --git a/src/asciinema/util/io.clj b/src/asciinema/util/io.clj index cdc4676..6c2d1ad 100644 --- a/src/asciinema/util/io.clj +++ b/src/asciinema/util/io.clj @@ -1,7 +1,8 @@ (ns asciinema.util.io (:require [clojure.java.shell :as shell]) - (:import java.nio.file.Files - java.nio.file.attribute.FileAttribute)) + (:import java.io.FilterInputStream + java.nio.file.attribute.FileAttribute + java.nio.file.Files)) (defn create-tmp-dir [prefix] (let [dir (Files/createTempDirectory prefix (into-array FileAttribute []))] @@ -13,3 +14,9 @@ ~@body (finally (shell/sh "rm" "-rf" (.getPath ~sym)))))) + +(defn cleanup-input-stream [is cleanup] + (proxy [FilterInputStream] [is] + (close [] + (proxy-super close) + (cleanup))))