From f18461643285533aebdecba004ec69aacf174a50 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Thu, 16 Feb 2017 13:44:49 +0100 Subject: [PATCH] PNG generation --- project.clj | 1 + src/asciinema/endpoint/asciicasts.clj | 51 +++++++++++++++++++++++++-- src/asciinema/model/asciicast.clj | 27 +++++++++++++- src/asciinema/util/io.clj | 15 ++++++++ 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/asciinema/util/io.clj diff --git a/project.clj b/project.clj index 23a3ec6..2a6931c 100644 --- a/project.clj +++ b/project.clj @@ -19,6 +19,7 @@ [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"] [org.slf4j/slf4j-nop "1.7.21"] [org.webjars/normalize.css "3.0.2"] [duct/hikaricp-component "0.1.0"] diff --git a/src/asciinema/endpoint/asciicasts.clj b/src/asciinema/endpoint/asciicasts.clj index c1b17f2..c3d5f7f 100644 --- a/src/asciinema/endpoint/asciicasts.clj +++ b/src/asciinema/endpoint/asciicasts.clj @@ -1,15 +1,38 @@ (ns asciinema.endpoint.asciicasts (:require [asciinema.boundary [asciicast-database :as adb] - [file-store :as fstore]] + [file-store :as fstore] + [user-database :as udb]] [asciinema.model.asciicast :as asciicast] + [asciinema.util.io :refer [with-tmp-dir]] + [clojure.java.io :as io] + [clojure.java.shell :as shell] [compojure.api.sweet :refer :all] + [environ.core :refer [env]] [ring.util.http-response :as response] - [schema.core :as s])) + [schema.core :as s] + [clojure.string :as str])) (defn exception-handler [^Exception e data request] (throw e)) +(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))))) + +(def Num (s/if #(str/includes? % ".") + Double + s/Int)) + +(def Theme (apply s/enum asciicast/themes)) + (defn asciicasts-endpoint [{:keys [db file-store]}] (api {:exceptions {:handlers {:compojure.api.exception/default exception-handler}}} @@ -22,4 +45,28 @@ (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))) + + (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))) + json-store-path (asciicast/json-store-path asciicast) + png-store-path (asciicast/png-store-path asciicast png-params)] + (with-tmp-dir [dir "asciinema-png-"] + (let [json-local-path (str dir "/asciicast.json") + png-local-path (str dir "/asciicast.png")] + (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))) + (fstore/serve-file file-store png-store-path {})) (response/not-found)))))) diff --git a/src/asciinema/model/asciicast.clj b/src/asciinema/model/asciicast.clj index a9a35b3..bd2bc19 100644 --- a/src/asciinema/model/asciicast.clj +++ b/src/asciinema/model/asciicast.clj @@ -1,13 +1,38 @@ -(ns asciinema.model.asciicast) +(ns asciinema.model.asciicast + (:require[pandect.algo.sha1 :as sha1] + [clojure.string :as str])) (defn json-store-path [{:keys [id file stdout_frames]}] (cond file (str "asciicast/file/" id "/" file) stdout_frames (str "asciicast/stdout_frames/" id "/" stdout_frames))) +(def themes #{"asciinema" "tango" "solarized-dark" "solarized-light" "monokai"}) (def default-theme "asciinema") (defn theme-name [asciicast user] (or (:theme_name asciicast) (:theme_name user) default-theme)) + +(defn snapshot-at [{:keys [snapshot_at duration]}] + (or snapshot_at (/ duration 2.0))) + +(def default-png-scale 2) + +(defn png-params [asciicast user] + {:snapshot-at (snapshot-at asciicast) + :theme (theme-name asciicast user) + :scale default-png-scale}) + +(defn- png-version [asciicast params] + (let [attrs (assoc params :id (:id asciicast))] + (->> attrs + (map (fn [[k v]] (str (name k) "=" v))) + (str/join "/") + (sha1/sha1)))) + +(defn png-store-path [asciicast params] + (let [ver (png-version asciicast params) + png-filename (str ver ".png")] + (str "png/" (:id asciicast) "/" png-filename))) diff --git a/src/asciinema/util/io.clj b/src/asciinema/util/io.clj new file mode 100644 index 0000000..cdc4676 --- /dev/null +++ b/src/asciinema/util/io.clj @@ -0,0 +1,15 @@ +(ns asciinema.util.io + (:require [clojure.java.shell :as shell]) + (:import java.nio.file.Files + java.nio.file.attribute.FileAttribute)) + +(defn create-tmp-dir [prefix] + (let [dir (Files/createTempDirectory prefix (into-array FileAttribute []))] + (.toFile dir))) + +(defmacro with-tmp-dir [[sym prefix] & body] + `(let [~sym (create-tmp-dir ~prefix)] + (try + ~@body + (finally + (shell/sh "rm" "-rf" (.getPath ~sym))))))