mirror of
https://github.com/penpot/penpot.git
synced 2025-05-29 23:46:12 +02:00
♻️ Refactor exporter state initialization.
This commit is contained in:
parent
4825efb582
commit
abb244c940
12 changed files with 308 additions and 287 deletions
|
@ -1,11 +1,13 @@
|
||||||
{:dependencies
|
{:dependencies
|
||||||
[[funcool/promesa "6.0.0"]
|
[[com.cognitect/transit-cljs "0.8.269"]
|
||||||
[danlentz/clj-uuid "0.1.9"]
|
[danlentz/clj-uuid "0.1.9"]
|
||||||
|
[frankiesardo/linked "1.3.0"]
|
||||||
[funcool/cuerdas "2021.05.02-0"]
|
[funcool/cuerdas "2021.05.02-0"]
|
||||||
|
[funcool/promesa "6.0.0"]
|
||||||
|
[integrant/integrant "0.8.0"]
|
||||||
[lambdaisland/glogi "1.0.106"]
|
[lambdaisland/glogi "1.0.106"]
|
||||||
[metosin/reitit-core "0.5.13"]
|
[lambdaisland/uri "1.4.54"]
|
||||||
[com.cognitect/transit-cljs "0.8.269"]
|
[metosin/reitit-core "0.5.13"]]
|
||||||
[frankiesardo/linked "1.3.0"]]
|
|
||||||
|
|
||||||
:source-paths ["src" "vendor" "../common"]
|
:source-paths ["src" "vendor" "../common"]
|
||||||
:jvm-opts ["-Xmx512m" "-Xms50m" "-XX:+UseSerialGC"]
|
:jvm-opts ["-Xmx512m" "-Xms50m" "-XX:+UseSerialGC"]
|
||||||
|
|
|
@ -6,10 +6,13 @@
|
||||||
|
|
||||||
(ns app.browser
|
(ns app.browser
|
||||||
(:require
|
(:require
|
||||||
|
[app.config :as cf]
|
||||||
[lambdaisland.glogi :as log]
|
[lambdaisland.glogi :as log]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
["puppeteer-cluster" :as ppc]))
|
["puppeteer-cluster" :as ppc]))
|
||||||
|
|
||||||
|
;; --- BROWSER API
|
||||||
|
|
||||||
(def USER-AGENT
|
(def USER-AGENT
|
||||||
(str "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
(str "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||||
"(KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"))
|
"(KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"))
|
||||||
|
@ -74,24 +77,38 @@
|
||||||
:value value
|
:value value
|
||||||
:domain domain}))
|
:domain domain}))
|
||||||
|
|
||||||
(defn start!
|
;; --- BROWSER STATE
|
||||||
([] (start! nil))
|
|
||||||
([{:keys [concurrency concurrency-strategy]
|
|
||||||
:or {concurrency 10
|
|
||||||
concurrency-strategy :incognito}}]
|
|
||||||
(let [ccst (case concurrency-strategy
|
|
||||||
:browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster)
|
|
||||||
:incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster)
|
|
||||||
:page (.-CONCURRENCY_PAGE ^js ppc/Cluster))
|
|
||||||
opts #js {:concurrency ccst
|
|
||||||
:maxConcurrency concurrency
|
|
||||||
:puppeteerOptions #js {:args #js ["--no-sandbox"]}}]
|
|
||||||
(.launch ^js ppc/Cluster opts))))
|
|
||||||
|
|
||||||
(defn stop!
|
(def instance (atom nil))
|
||||||
[instance]
|
|
||||||
(p/do!
|
(defn- create-browser
|
||||||
(.idle ^js instance)
|
[concurrency strategy]
|
||||||
(.close ^js instance)
|
(let [strategy (case strategy
|
||||||
(log/info :msg "shutdown headless browser")
|
:browser (.-CONCURRENCY_BROWSER ^js ppc/Cluster)
|
||||||
nil))
|
:incognito (.-CONCURRENCY_CONTEXT ^js ppc/Cluster)
|
||||||
|
:page (.-CONCURRENCY_PAGE ^js ppc/Cluster))
|
||||||
|
opts #js {:concurrency strategy
|
||||||
|
:maxConcurrency concurrency
|
||||||
|
:puppeteerOptions #js {:args #js ["--no-sandbox"]}}]
|
||||||
|
(.launch ^js ppc/Cluster opts)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn init
|
||||||
|
[]
|
||||||
|
(let [concurrency (cf/get :browser-concurrency)
|
||||||
|
strategy (cf/get :browser-strategy)]
|
||||||
|
(-> (create-browser concurrency strategy)
|
||||||
|
(p/then #(reset! instance %))
|
||||||
|
(p/catch (fn [error]
|
||||||
|
(log/error :msg "failed to initialize browser")
|
||||||
|
(js/console.error error))))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn stop
|
||||||
|
[]
|
||||||
|
(if-let [instance @instance]
|
||||||
|
(p/do!
|
||||||
|
(.idle ^js instance)
|
||||||
|
(.close ^js instance)
|
||||||
|
(log/info :msg "shutdown headless browser"))
|
||||||
|
(p/resolved nil)))
|
||||||
|
|
|
@ -5,22 +5,54 @@
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.config
|
(ns app.config
|
||||||
|
(:refer-clojure :exclude [get])
|
||||||
(:require
|
(:require
|
||||||
["process" :as process]
|
["process" :as process]
|
||||||
[cljs.pprint]
|
[cljs.pprint]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[cljs.spec.alpha :as s]
|
||||||
|
[cljs.core :as c]
|
||||||
|
[lambdaisland.uri :as u]))
|
||||||
|
|
||||||
(defn- keywordize
|
(def defaults
|
||||||
[s]
|
{:public-uri "http://localhost:3449"
|
||||||
(-> (str/kebab s)
|
:http-server-port 6061
|
||||||
(str/keyword)))
|
:browser-concurrency 5
|
||||||
|
:browser-strategy :incognito})
|
||||||
|
|
||||||
(defonce env
|
(s/def ::public-uri ::us/string)
|
||||||
(let [env (unchecked-get process "env")]
|
(s/def ::http-server-port ::us/integer)
|
||||||
(persistent!
|
(s/def ::browser-concurrency ::us/integer)
|
||||||
(reduce #(assoc! %1 (keywordize %2) (unchecked-get env %2))
|
(s/def ::browser-strategy ::us/keyword)
|
||||||
(transient {})
|
|
||||||
(js/Object.keys env)))))
|
|
||||||
|
|
||||||
(defonce config
|
(s/def ::config
|
||||||
{:public-uri (:penpot-public-uri env "http://localhost:3449")})
|
(s/keys :opt-un [::public-uri
|
||||||
|
::http-server-port
|
||||||
|
::browser-concurrency
|
||||||
|
::browser-strategy]))
|
||||||
|
(defn- read-env
|
||||||
|
[prefix]
|
||||||
|
(let [env (unchecked-get process "env")
|
||||||
|
kwd (fn [s] (-> (str/kebab s) (str/keyword)))
|
||||||
|
prefix (str prefix "-")
|
||||||
|
len (count prefix)]
|
||||||
|
(reduce (fn [res key]
|
||||||
|
(cond-> res
|
||||||
|
(str/starts-with? key prefix)
|
||||||
|
(assoc (kwd (subs key len))
|
||||||
|
(unchecked-get env key))))
|
||||||
|
{}
|
||||||
|
(js/Object.keys env))))
|
||||||
|
|
||||||
|
(def config
|
||||||
|
(atom (->> (read-env "penpot")
|
||||||
|
(merge defaults)
|
||||||
|
(us/conform ::config))))
|
||||||
|
|
||||||
|
(defn get
|
||||||
|
"A configuration getter."
|
||||||
|
([key]
|
||||||
|
(c/get @config key))
|
||||||
|
([key default]
|
||||||
|
(c/get @config key default)))
|
||||||
|
|
|
@ -21,10 +21,9 @@
|
||||||
(defn start
|
(defn start
|
||||||
[& args]
|
[& args]
|
||||||
(log/info :msg "initializing")
|
(log/info :msg "initializing")
|
||||||
(p/let [browser (bwr/start!)
|
(p/do!
|
||||||
server (http/start! {:browser browser})]
|
(bwr/init)
|
||||||
(reset! state {:http server
|
(http/init)))
|
||||||
:browser browser})))
|
|
||||||
|
|
||||||
(def main start)
|
(def main start)
|
||||||
|
|
||||||
|
@ -35,8 +34,6 @@
|
||||||
|
|
||||||
(log/info :msg "stoping")
|
(log/info :msg "stoping")
|
||||||
(p/do!
|
(p/do!
|
||||||
(when-let [instance (:browser @state)]
|
(bwr/stop)
|
||||||
(bwr/stop! instance))
|
(http/stop)
|
||||||
(when-let [instance (:http @state)]
|
|
||||||
(http/stop! instance))
|
|
||||||
(done)))
|
(done)))
|
||||||
|
|
|
@ -6,29 +6,33 @@
|
||||||
|
|
||||||
(ns app.http
|
(ns app.http
|
||||||
(:require
|
(:require
|
||||||
|
[app.config :as cf]
|
||||||
[app.http.export :refer [export-handler]]
|
[app.http.export :refer [export-handler]]
|
||||||
[app.http.thumbnail :refer [thumbnail-handler]]
|
|
||||||
[app.http.impl :as impl]
|
[app.http.impl :as impl]
|
||||||
[lambdaisland.glogi :as log]
|
[lambdaisland.glogi :as log]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[reitit.core :as r]))
|
[reitit.core :as r]))
|
||||||
|
|
||||||
(def routes
|
(def routes
|
||||||
[["/export/thumbnail" {:handler thumbnail-handler}]
|
[["/export" {:handler export-handler}]])
|
||||||
["/export" {:handler export-handler}]])
|
|
||||||
|
|
||||||
(defn start!
|
(def instance (atom nil))
|
||||||
[extra]
|
|
||||||
(log/info :msg "starting http server" :port 6061)
|
(defn init
|
||||||
|
[]
|
||||||
(let [router (r/router routes)
|
(let [router (r/router routes)
|
||||||
handler (impl/handler router extra)
|
handler (impl/handler router)
|
||||||
server (impl/server handler)]
|
server (impl/server handler)
|
||||||
(.listen server 6061)
|
port (cf/get :http-server-port 6061)]
|
||||||
(p/resolved server)))
|
(.listen server port)
|
||||||
|
(log/info :msg "starting http server" :port port)
|
||||||
|
(reset! instance server)))
|
||||||
|
|
||||||
(defn stop!
|
(defn stop
|
||||||
[server]
|
[]
|
||||||
(p/create (fn [resolve]
|
(if-let [server @instance]
|
||||||
(.close server (fn []
|
(p/create (fn [resolve]
|
||||||
(log/info :msg "shutdown http server")
|
(.close server (fn []
|
||||||
(resolve))))))
|
(log/info :msg "shutdown http server")
|
||||||
|
(resolve)))))
|
||||||
|
(p/resolved nil)))
|
||||||
|
|
|
@ -6,15 +6,15 @@
|
||||||
|
|
||||||
(ns app.http.export
|
(ns app.http.export
|
||||||
(:require
|
(:require
|
||||||
[app.http.export-bitmap :as bitmap]
|
[app.common.exceptions :as exc :include-macros true]
|
||||||
[app.http.export-svg :as svg]
|
[app.common.spec :as us]
|
||||||
|
[app.renderer.bitmap :as rb]
|
||||||
|
[app.renderer.svg :as rs]
|
||||||
[app.zipfile :as zip]
|
[app.zipfile :as zip]
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[lambdaisland.glogi :as log]
|
[lambdaisland.glogi :as log]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]))
|
||||||
[app.common.exceptions :as exc :include-macros true]
|
|
||||||
[app.common.spec :as us]))
|
|
||||||
|
|
||||||
(s/def ::name ::us/string)
|
(s/def ::name ::us/string)
|
||||||
(s/def ::page-id ::us/uuid)
|
(s/def ::page-id ::us/uuid)
|
||||||
|
@ -38,42 +38,44 @@
|
||||||
(declare attach-filename)
|
(declare attach-filename)
|
||||||
|
|
||||||
(defn export-handler
|
(defn export-handler
|
||||||
[{:keys [params browser cookies] :as request}]
|
[{:keys [params cookies] :as request}]
|
||||||
(let [{:keys [exports page-id file-id object-id name]} (us/conform ::handler-params params)
|
(let [{:keys [exports page-id file-id object-id name]} (us/conform ::handler-params params)
|
||||||
token (.get ^js cookies "auth-token")]
|
token (.get ^js cookies "auth-token")]
|
||||||
(case (count exports)
|
(case (count exports)
|
||||||
0 (exc/raise :type :validation :code :missing-exports)
|
0 (exc/raise :type :validation
|
||||||
1 (handle-single-export
|
:code :missing-exports)
|
||||||
request
|
|
||||||
(assoc (first exports)
|
1 (-> (first exports)
|
||||||
:name name
|
(assoc :name name)
|
||||||
:token token
|
(assoc :token token)
|
||||||
:file-id file-id
|
(assoc :file-id file-id)
|
||||||
:page-id page-id
|
(assoc :page-id page-id)
|
||||||
:object-id object-id))
|
(assoc :object-id object-id)
|
||||||
(handle-multiple-export
|
(handle-single-export))
|
||||||
request
|
|
||||||
(map (fn [item]
|
(->> exports
|
||||||
(assoc item
|
(map (fn [item]
|
||||||
:name name
|
(-> item
|
||||||
:token token
|
(assoc :name name)
|
||||||
:file-id file-id
|
(assoc :token token)
|
||||||
:page-id page-id
|
(assoc :file-id file-id)
|
||||||
:object-id object-id)) exports)))))
|
(assoc :page-id page-id)
|
||||||
|
(assoc :object-id object-id))))
|
||||||
|
(handle-multiple-export)))))
|
||||||
|
|
||||||
(defn- handle-single-export
|
(defn- handle-single-export
|
||||||
[{:keys [browser]} params]
|
[params]
|
||||||
(p/let [result (perform-export browser params)]
|
(p/let [result (perform-export params)]
|
||||||
{:status 200
|
{:status 200
|
||||||
:body (:content result)
|
:body (:content result)
|
||||||
:headers {"content-type" (:mime-type result)
|
:headers {"content-type" (:mime-type result)
|
||||||
"content-length" (:length result)}}))
|
"content-length" (:length result)}}))
|
||||||
|
|
||||||
(defn- handle-multiple-export
|
(defn- handle-multiple-export
|
||||||
[{:keys [browser]} exports]
|
[exports]
|
||||||
(let [proms (->> exports
|
(let [proms (->> exports
|
||||||
(attach-filename)
|
(attach-filename)
|
||||||
(map (partial perform-export browser)))]
|
(map perform-export))]
|
||||||
(-> (p/all proms)
|
(-> (p/all proms)
|
||||||
(p/then (fn [results]
|
(p/then (fn [results]
|
||||||
(reduce #(zip/add! %1 (:filename %2) (:content %2)) (zip/create) results)))
|
(reduce #(zip/add! %1 (:filename %2) (:content %2)) (zip/create) results)))
|
||||||
|
@ -83,11 +85,11 @@
|
||||||
:body (.generateNodeStream ^js fzip)})))))
|
:body (.generateNodeStream ^js fzip)})))))
|
||||||
|
|
||||||
(defn- perform-export
|
(defn- perform-export
|
||||||
[browser params]
|
[params]
|
||||||
(case (:type params)
|
(case (:type params)
|
||||||
:png (bitmap/export browser params)
|
:png (rb/render params)
|
||||||
:jpeg (bitmap/export browser params)
|
:jpeg (rb/render params)
|
||||||
:svg (svg/export browser params)))
|
:svg (rs/render params)))
|
||||||
|
|
||||||
(defn- find-filename-candidate
|
(defn- find-filename-candidate
|
||||||
[params used]
|
[params used]
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) UXBOX Labs SL
|
|
||||||
|
|
||||||
(ns app.http.export-bitmap
|
|
||||||
(:require
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[app.browser :as bwr]
|
|
||||||
[app.config :as cfg]
|
|
||||||
[lambdaisland.glogi :as log]
|
|
||||||
[cljs.spec.alpha :as s]
|
|
||||||
[promesa.core :as p]
|
|
||||||
[app.common.exceptions :as exc :include-macros true]
|
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.pages :as cp]
|
|
||||||
[app.common.spec :as us])
|
|
||||||
(:import
|
|
||||||
goog.Uri))
|
|
||||||
|
|
||||||
(defn screenshot-object
|
|
||||||
[browser {:keys [file-id page-id object-id token scale type]}]
|
|
||||||
(letfn [(handle [page]
|
|
||||||
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
|
||||||
uri (doto (Uri. (:public-uri cfg/config))
|
|
||||||
(.setPath "/")
|
|
||||||
(.setFragment path))
|
|
||||||
cookie {:domain (str (.getDomain uri)
|
|
||||||
":"
|
|
||||||
(.getPort uri))
|
|
||||||
:key "auth-token"
|
|
||||||
:value token}]
|
|
||||||
(log/info :uri (.toString uri))
|
|
||||||
(screenshot page (.toString uri) cookie)))
|
|
||||||
|
|
||||||
(screenshot [page uri cookie]
|
|
||||||
(p/do!
|
|
||||||
(bwr/emulate! page {:viewport [1920 1080]
|
|
||||||
:scale scale})
|
|
||||||
(bwr/set-cookie! page cookie)
|
|
||||||
(bwr/navigate! page uri)
|
|
||||||
(bwr/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
|
||||||
(p/let [dom (bwr/select page "#screenshot")]
|
|
||||||
(case type
|
|
||||||
:png (bwr/screenshot dom {:omit-background? true :type type})
|
|
||||||
:jpeg (bwr/screenshot dom {:omit-background? false :type type})))))]
|
|
||||||
|
|
||||||
(bwr/exec! browser handle)))
|
|
||||||
|
|
||||||
(s/def ::name ::us/string)
|
|
||||||
(s/def ::suffix ::us/string)
|
|
||||||
(s/def ::type #{:jpeg :png})
|
|
||||||
(s/def ::page-id ::us/uuid)
|
|
||||||
(s/def ::file-id ::us/uuid)
|
|
||||||
(s/def ::object-id ::us/uuid)
|
|
||||||
(s/def ::scale ::us/number)
|
|
||||||
(s/def ::token ::us/string)
|
|
||||||
(s/def ::filename ::us/string)
|
|
||||||
|
|
||||||
(s/def ::export-params
|
|
||||||
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id]
|
|
||||||
:opt-un [::filename]))
|
|
||||||
|
|
||||||
(defn export
|
|
||||||
[browser params]
|
|
||||||
(us/assert ::export-params params)
|
|
||||||
(p/let [content (screenshot-object browser params)]
|
|
||||||
{:content content
|
|
||||||
:filename (or (:filename params)
|
|
||||||
(str (:name params)
|
|
||||||
(:suffix params "")
|
|
||||||
(case (:type params)
|
|
||||||
:png ".png"
|
|
||||||
:jpeg ".jpg")))
|
|
||||||
:length (alength content)
|
|
||||||
:mime-type (case (:type params)
|
|
||||||
:png "image/png"
|
|
||||||
:jpeg "image/jpeg")}))
|
|
||||||
|
|
|
@ -13,25 +13,15 @@
|
||||||
[app.util.transit :as t]
|
[app.util.transit :as t]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[lambdaisland.glogi :as log]
|
[lambdaisland.glogi :as log]
|
||||||
|
[lambdaisland.uri :as u]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[reitit.core :as r])
|
[reitit.core :as r]))
|
||||||
(:import
|
|
||||||
goog.Uri))
|
|
||||||
|
|
||||||
(defn- query-params
|
|
||||||
"Given goog.Uri, read query parameters into Clojure map."
|
|
||||||
[^Uri uri]
|
|
||||||
(let [^js q (.getQueryData uri)]
|
|
||||||
(->> q
|
|
||||||
(.getKeys)
|
|
||||||
(map (juxt keyword #(.get q %)))
|
|
||||||
(into {}))))
|
|
||||||
|
|
||||||
(defn- match
|
(defn- match
|
||||||
[router ctx]
|
[router ctx]
|
||||||
(let [uri (.parse Uri (unchecked-get ctx "originalUrl"))]
|
(let [uri (u/uri (unchecked-get ctx "originalUrl"))]
|
||||||
(when-let [match (r/match-by-path router (.getPath ^js uri))]
|
(when-let [match (r/match-by-path router (:path uri))]
|
||||||
(assoc match :query-params (query-params uri)))))
|
(assoc match :query-params (u/query-string->map (:query uri))))))
|
||||||
|
|
||||||
(defn- handle-error
|
(defn- handle-error
|
||||||
[error request]
|
[error request]
|
||||||
|
@ -48,17 +38,21 @@
|
||||||
:headers {"content-type" "text/html"}
|
:headers {"content-type" "text/html"}
|
||||||
:body (str "<pre style='font-size:16px'>" (:explain data) "</pre>\n")}))
|
:body (str "<pre style='font-size:16px'>" (:explain data) "</pre>\n")}))
|
||||||
|
|
||||||
|
(and (= :internal type)
|
||||||
|
(= :browser-not-ready code))
|
||||||
|
{:status 503
|
||||||
|
:headers {"x-error" (t/encode data)}
|
||||||
|
:body ""}
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do
|
(do
|
||||||
(log/error :msg "Unexpected error"
|
(log/error :msg "Unexpected error"
|
||||||
:error error)
|
:error error)
|
||||||
(js/console.error error)
|
(js/console.error error)
|
||||||
{:status 500
|
{:status 500
|
||||||
:headers {"x-metadata" (t/encode {:type :unexpected
|
:headers {"x-error" (t/encode data)}
|
||||||
:message (ex-message error)})}
|
|
||||||
:body ""}))))
|
:body ""}))))
|
||||||
|
|
||||||
|
|
||||||
(defn- handle-response
|
(defn- handle-response
|
||||||
[ctx {:keys [body headers status] :or {headers {} status 200}}]
|
[ctx {:keys [body headers status] :or {headers {} status 200}}]
|
||||||
(run! (fn [[k v]] (.set ^js ctx k v)) headers)
|
(run! (fn [[k v]] (.set ^js ctx k v)) headers)
|
||||||
|
@ -89,17 +83,16 @@
|
||||||
(t/decode))))))))
|
(t/decode))))))))
|
||||||
|
|
||||||
(defn- wrap-handler
|
(defn- wrap-handler
|
||||||
[f extra]
|
[f]
|
||||||
(fn [ctx]
|
(fn [ctx]
|
||||||
(p/let [cookies (unchecked-get ctx "cookies")
|
(p/let [cookies (unchecked-get ctx "cookies")
|
||||||
headers (parse-headers ctx)
|
headers (parse-headers ctx)
|
||||||
body (parse-body ctx)
|
body (parse-body ctx)
|
||||||
request (assoc extra
|
request {:method (str/lower (unchecked-get ctx "method"))
|
||||||
:method (str/lower (unchecked-get ctx "method"))
|
:body body
|
||||||
:body body
|
:ctx ctx
|
||||||
:ctx ctx
|
:headers headers
|
||||||
:headers headers
|
:cookies cookies}]
|
||||||
:cookies cookies)]
|
|
||||||
(-> (p/do! (f request))
|
(-> (p/do! (f request))
|
||||||
(p/then (fn [rsp]
|
(p/then (fn [rsp]
|
||||||
(when (map? rsp)
|
(when (map? rsp)
|
||||||
|
@ -131,10 +124,10 @@
|
||||||
(.createServer http @handler))
|
(.createServer http @handler))
|
||||||
|
|
||||||
(defn handler
|
(defn handler
|
||||||
[router extra]
|
[router]
|
||||||
(let [instance (doto (new koa)
|
(let [instance (doto (new koa)
|
||||||
(.use (-> (router-handler router)
|
(.use (-> (router-handler router)
|
||||||
(wrap-handler extra))))]
|
(wrap-handler))))]
|
||||||
(specify! instance
|
(specify! instance
|
||||||
cljs.core/IDeref
|
cljs.core/IDeref
|
||||||
(-deref [_]
|
(-deref [_]
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) UXBOX Labs SL
|
|
||||||
|
|
||||||
(ns app.http.thumbnail
|
|
||||||
(:require
|
|
||||||
[app.common.exceptions :as exc :include-macros true]
|
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.http.export-bitmap :as bitmap]
|
|
||||||
[cljs.spec.alpha :as s]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[lambdaisland.glogi :as log]
|
|
||||||
[promesa.core :as p]))
|
|
||||||
|
|
||||||
(s/def ::page-id ::us/uuid)
|
|
||||||
(s/def ::file-id ::us/uuid)
|
|
||||||
(s/def ::object-id ::us/uuid)
|
|
||||||
(s/def ::scale ::us/number)
|
|
||||||
|
|
||||||
(s/def ::handler-params
|
|
||||||
(s/keys :req-un [::page-id ::file-id ::object-id]))
|
|
||||||
|
|
||||||
(declare handle-single-export)
|
|
||||||
(declare handle-multiple-export)
|
|
||||||
(declare perform-export)
|
|
||||||
(declare attach-filename)
|
|
||||||
|
|
||||||
(defn thumbnail-handler
|
|
||||||
[{:keys [params browser cookies] :as request}]
|
|
||||||
(let [{:keys [page-id file-id object-id]} (us/conform ::handler-params params)
|
|
||||||
params {:token (.get ^js cookies "auth-token")
|
|
||||||
:file-id file-id
|
|
||||||
:page-id page-id
|
|
||||||
:object-id object-id
|
|
||||||
:scale 0.3
|
|
||||||
:type :jpeg}]
|
|
||||||
(p/let [content (bitmap/screenshot-object browser params)]
|
|
||||||
{:status 200
|
|
||||||
:body content
|
|
||||||
:headers {"content-type" "image/jpeg"
|
|
||||||
"content-length" (alength content)}})))
|
|
91
exporter/src/app/renderer/bitmap.cljs
Normal file
91
exporter/src/app/renderer/bitmap.cljs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.renderer.bitmap
|
||||||
|
"A bitmap renderer."
|
||||||
|
(:require
|
||||||
|
[app.browser :as bw]
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex :include-macros true]
|
||||||
|
[app.common.pages :as cp]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.config :as cf]
|
||||||
|
[cljs.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[lambdaisland.uri :as u]
|
||||||
|
[lambdaisland.glogi :as log]
|
||||||
|
[promesa.core :as p]))
|
||||||
|
|
||||||
|
(defn create-cookie
|
||||||
|
[uri token]
|
||||||
|
(let [domain (str (:host uri)
|
||||||
|
(when (:port uri)
|
||||||
|
(str ":" (:port uri))))]
|
||||||
|
{:domain domain
|
||||||
|
:key "auth-token"
|
||||||
|
:value token}))
|
||||||
|
|
||||||
|
(defn screenshot-object
|
||||||
|
[browser {:keys [file-id page-id object-id token scale type]}]
|
||||||
|
(letfn [(handle [page]
|
||||||
|
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
||||||
|
uri (-> (u/uri (cf/get :public-uri))
|
||||||
|
(assoc :path "/")
|
||||||
|
(assoc :fragment path))
|
||||||
|
cookie (create-cookie uri token)]
|
||||||
|
(screenshot page (str uri) cookie)))
|
||||||
|
|
||||||
|
(screenshot [page uri cookie]
|
||||||
|
(log/info :uri uri)
|
||||||
|
(p/do!
|
||||||
|
(bw/emulate! page {:viewport [1920 1080]
|
||||||
|
:scale scale})
|
||||||
|
(bw/set-cookie! page cookie)
|
||||||
|
(bw/navigate! page uri)
|
||||||
|
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
||||||
|
(p/let [dom (bw/select page "#screenshot")]
|
||||||
|
(case type
|
||||||
|
:png (bw/screenshot dom {:omit-background? true :type type})
|
||||||
|
:jpeg (bw/screenshot dom {:omit-background? false :type type})))))]
|
||||||
|
|
||||||
|
(bw/exec! browser handle)))
|
||||||
|
|
||||||
|
(s/def ::name ::us/string)
|
||||||
|
(s/def ::suffix ::us/string)
|
||||||
|
(s/def ::type #{:jpeg :png})
|
||||||
|
(s/def ::page-id ::us/uuid)
|
||||||
|
(s/def ::file-id ::us/uuid)
|
||||||
|
(s/def ::object-id ::us/uuid)
|
||||||
|
(s/def ::scale ::us/number)
|
||||||
|
(s/def ::token ::us/string)
|
||||||
|
(s/def ::filename ::us/string)
|
||||||
|
|
||||||
|
(s/def ::render-params
|
||||||
|
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id]
|
||||||
|
:opt-un [::filename]))
|
||||||
|
|
||||||
|
(defn render
|
||||||
|
[params]
|
||||||
|
(us/assert ::render-params params)
|
||||||
|
(let [browser @bw/instance]
|
||||||
|
(when-not browser
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :browser-not-ready
|
||||||
|
:hint "browser cluster is not initialized yet"))
|
||||||
|
|
||||||
|
(p/let [content (screenshot-object browser params)]
|
||||||
|
{:content content
|
||||||
|
:filename (or (:filename params)
|
||||||
|
(str (:name params)
|
||||||
|
(:suffix params "")
|
||||||
|
(case (:type params)
|
||||||
|
:png ".png"
|
||||||
|
:jpeg ".jpg")))
|
||||||
|
:length (alength content)
|
||||||
|
:mime-type (case (:type params)
|
||||||
|
:png "image/png"
|
||||||
|
:jpeg "image/jpeg")})))
|
||||||
|
|
|
@ -4,24 +4,24 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.http.export-svg
|
(ns app.renderer.svg
|
||||||
(:require
|
(:require
|
||||||
["path" :as path]
|
["path" :as path]
|
||||||
["xml-js" :as xml]
|
["xml-js" :as xml]
|
||||||
[app.browser :as bwr]
|
[app.browser :as bw]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as exc :include-macros true]
|
[app.common.exceptions :as ex :include-macros true]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cfg]
|
[app.config :as cf]
|
||||||
[app.util.shell :as sh]
|
[app.util.shell :as sh]
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
[clojure.walk :as walk]
|
[clojure.walk :as walk]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[lambdaisland.glogi :as log]
|
[lambdaisland.glogi :as log]
|
||||||
[promesa.core :as p])
|
[lambdaisland.uri :as u]
|
||||||
(:import
|
[app.renderer.bitmap :refer [create-cookie]]
|
||||||
goog.Uri))
|
[promesa.core :as p]))
|
||||||
|
|
||||||
(log/set-level "app.http.export-svg" :trace)
|
(log/set-level "app.http.export-svg" :trace)
|
||||||
|
|
||||||
|
@ -67,7 +67,6 @@
|
||||||
(nil? d)
|
(nil? d)
|
||||||
(str/empty? d)))))
|
(str/empty? d)))))
|
||||||
|
|
||||||
|
|
||||||
(defn flatten-toplevel-svg-elements
|
(defn flatten-toplevel-svg-elements
|
||||||
"Flattens XML data structure if two nested top-side SVG elements found."
|
"Flattens XML data structure if two nested top-side SVG elements found."
|
||||||
[item]
|
[item]
|
||||||
|
@ -165,7 +164,9 @@
|
||||||
;; objects.
|
;; objects.
|
||||||
(let [vbox (-> (get-in result ["attributes" "viewBox"])
|
(let [vbox (-> (get-in result ["attributes" "viewBox"])
|
||||||
(parse-viewbox))
|
(parse-viewbox))
|
||||||
transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y (/ width (:width vbox)) (/ height (:height vbox)))]
|
transform (str/fmt "translate(%s, %s) scale(%s, %s)" x y
|
||||||
|
(/ width (:width vbox))
|
||||||
|
(/ height (:height vbox)))]
|
||||||
(-> result
|
(-> result
|
||||||
(assoc "name" "g")
|
(assoc "name" "g")
|
||||||
(assoc "attributes" {})
|
(assoc "attributes" {})
|
||||||
|
@ -212,8 +213,8 @@
|
||||||
(extract-single-node [node]
|
(extract-single-node [node]
|
||||||
(log/trace :fn :extract-single-node)
|
(log/trace :fn :extract-single-node)
|
||||||
|
|
||||||
(p/let [attrs (bwr/eval! node extract-element-attrs)
|
(p/let [attrs (bw/eval! node extract-element-attrs)
|
||||||
shot (bwr/screenshot node {:omit-background? true :type "png"})]
|
shot (bw/screenshot node {:omit-background? true :type "png"})]
|
||||||
{:id (unchecked-get attrs "id")
|
{:id (unchecked-get attrs "id")
|
||||||
:x (unchecked-get attrs "x")
|
:x (unchecked-get attrs "x")
|
||||||
:y (unchecked-get attrs "y")
|
:y (unchecked-get attrs "y")
|
||||||
|
@ -235,12 +236,12 @@
|
||||||
|
|
||||||
(process-text-nodes [page]
|
(process-text-nodes [page]
|
||||||
(log/trace :fn :process-text-nodes)
|
(log/trace :fn :process-text-nodes)
|
||||||
(-> (bwr/select-all page "#screenshot foreignObject")
|
(-> (bw/select-all page "#screenshot foreignObject")
|
||||||
(p/then (fn [nodes] (p/all (map process-text-node nodes))))))
|
(p/then (fn [nodes] (p/all (map process-text-node nodes))))))
|
||||||
|
|
||||||
(extract-svg [page]
|
(extract-svg [page]
|
||||||
(p/let [dom (bwr/select page "#screenshot")
|
(p/let [dom (bw/select page "#screenshot")
|
||||||
xmldata (bwr/eval! dom (fn [elem] (.-outerHTML ^js elem)))
|
xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem)))
|
||||||
nodes (process-text-nodes page)
|
nodes (process-text-nodes page)
|
||||||
nodes (d/index-by :id nodes)
|
nodes (d/index-by :id nodes)
|
||||||
result (replace-text-nodes xmldata nodes)]
|
result (replace-text-nodes xmldata nodes)]
|
||||||
|
@ -253,30 +254,28 @@
|
||||||
|
|
||||||
(render-in-page [page {:keys [uri cookie] :as rctx}]
|
(render-in-page [page {:keys [uri cookie] :as rctx}]
|
||||||
(p/do!
|
(p/do!
|
||||||
(bwr/emulate! page {:viewport [1920 1080]
|
(bw/emulate! page {:viewport [1920 1080]
|
||||||
:scale 4})
|
:scale 4})
|
||||||
(bwr/set-cookie! page cookie)
|
(bw/set-cookie! page cookie)
|
||||||
(bwr/navigate! page uri)
|
(bw/navigate! page uri)
|
||||||
;; (bwr/wait-for page "#screenshot foreignObject" {:visible true})
|
;; (bw/wait-for page "#screenshot foreignObject" {:visible true})
|
||||||
(bwr/sleep page 2000)
|
(bw/sleep page 2000)
|
||||||
;; (bwr/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
;; (bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
||||||
page))
|
page))
|
||||||
|
|
||||||
(handle [rctx page]
|
(handle [rctx page]
|
||||||
(p/let [page (render-in-page page rctx)]
|
(p/let [page (render-in-page page rctx)]
|
||||||
(extract-svg page)))]
|
(extract-svg page)))]
|
||||||
|
|
||||||
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
||||||
uri (doto (Uri. (:public-uri cfg/config))
|
uri (-> (u/uri (cf/get :public-uri))
|
||||||
(.setPath "/")
|
(assoc :path "/")
|
||||||
(.setFragment path))
|
(assoc :fragment path))
|
||||||
rctx {:cookie {:domain (str (.getDomain uri) ":" (.getPort uri))
|
cookie (create-cookie uri token)
|
||||||
:key "auth-token"
|
rctx {:cookie cookie
|
||||||
:value token}
|
:uri (str uri)}]
|
||||||
:uri (.toString uri)}]
|
(log/info :uri (:uri rctx))
|
||||||
|
(bw/exec! browser (partial handle rctx)))))
|
||||||
(log/info :uri (.toString uri))
|
|
||||||
(bwr/exec! browser (partial handle rctx)))))
|
|
||||||
|
|
||||||
(s/def ::name ::us/string)
|
(s/def ::name ::us/string)
|
||||||
(s/def ::suffix ::us/string)
|
(s/def ::suffix ::us/string)
|
||||||
|
@ -288,18 +287,25 @@
|
||||||
(s/def ::token ::us/string)
|
(s/def ::token ::us/string)
|
||||||
(s/def ::filename ::us/string)
|
(s/def ::filename ::us/string)
|
||||||
|
|
||||||
(s/def ::export-params
|
(s/def ::render-params
|
||||||
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token]
|
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token]
|
||||||
:opt-un [::filename]))
|
:opt-un [::filename]))
|
||||||
|
|
||||||
(defn export
|
(defn render
|
||||||
[browser params]
|
[params]
|
||||||
(us/assert ::export-params params)
|
(us/assert ::render-params params)
|
||||||
(p/let [content (render-object browser params)]
|
(let [browser @bw/instance]
|
||||||
{:content content
|
(when-not browser
|
||||||
:filename (or (:filename params)
|
(ex/raise :type :internal
|
||||||
(str (:name params)
|
:code :browser-not-ready
|
||||||
(:suffix params "")
|
:hint "browser cluster is not initialized yet"))
|
||||||
".svg"))
|
|
||||||
:length (alength content)
|
|
||||||
:mime-type "image/svg+xml"}))
|
(p/let [content (render-object browser params)]
|
||||||
|
{:content content
|
||||||
|
:filename (or (:filename params)
|
||||||
|
(str (:name params)
|
||||||
|
(:suffix params "")
|
||||||
|
".svg"))
|
||||||
|
:length (alength content)
|
||||||
|
:mime-type "image/svg+xml"})))
|
|
@ -25,5 +25,5 @@
|
||||||
|
|
||||||
(defn encode
|
(defn encode
|
||||||
[data]
|
[data]
|
||||||
(let [w (t/writer :json {:handlers +write-handlers+})]
|
(let [w (t/writer :json-verbose {:handlers +write-handlers+})]
|
||||||
(t/write w data)))
|
(t/write w data)))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue