mirror of
https://github.com/penpot/penpot.git
synced 2025-05-29 18:16:12 +02:00
♻️ Refactor exporter
- Migrate from puppeteer to playwright - Fix many lifecycle and resource usage issues - Add redis integration - Enable multiple exportation - Enable asynchronos exportation (with progress reporting)
This commit is contained in:
parent
f0a9889f33
commit
4a9e38a221
21 changed files with 1366 additions and 1017 deletions
|
@ -7,7 +7,7 @@
|
|||
(ns app.browser
|
||||
(:require
|
||||
["generic-pool" :as gp]
|
||||
["puppeteer-core" :as pp]
|
||||
["playwright" :as pw]
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.uuid :as uuid]
|
||||
|
@ -20,78 +20,72 @@
|
|||
;; --- BROWSER API
|
||||
|
||||
(def default-timeout 30000)
|
||||
(def default-viewport {:width 1920 :height 1080 :scale 1})
|
||||
(def default-viewport-width 1920)
|
||||
(def default-viewport-height 1080)
|
||||
(def default-user-agent
|
||||
(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/99.0.3729.169 Safari/537.36"))
|
||||
|
||||
(defn set-cookie!
|
||||
[page {:keys [key value domain]}]
|
||||
(.setCookie ^js page #js {:name key
|
||||
:value value
|
||||
:domain domain}))
|
||||
(defn create-cookies
|
||||
[uri {:keys [name token] :or {name "auth-token"}}]
|
||||
(let [domain (str (:host uri)
|
||||
(when (:port uri)
|
||||
(str ":" (:port uri))))]
|
||||
#js [#js {:domain domain
|
||||
:path "/"
|
||||
:name name
|
||||
:value token}]))
|
||||
|
||||
(defn configure-page!
|
||||
[page {:keys [timeout cookie user-agent viewport]}]
|
||||
(let [timeout (or timeout default-timeout)
|
||||
user-agent (or user-agent default-user-agent)
|
||||
viewport (merge default-viewport viewport)]
|
||||
|
||||
(p/do!
|
||||
(.setViewport ^js page #js {:width (:width viewport)
|
||||
:height (:height viewport)
|
||||
:deviceScaleFactor (:scale viewport)})
|
||||
(.setUserAgent ^js page user-agent)
|
||||
(.setDefaultTimeout ^js page timeout)
|
||||
(when cookie
|
||||
(set-cookie! page cookie)))))
|
||||
|
||||
(defn navigate!
|
||||
([page url] (navigate! page url nil))
|
||||
([page url {:keys [wait-until]
|
||||
:or {wait-until "networkidle2"}}]
|
||||
(.goto ^js page url #js {:waitUntil wait-until})))
|
||||
(defn nav!
|
||||
([page url] (nav! page url nil))
|
||||
([page url {:keys [wait-until timeout] :or {wait-until "networkidle" timeout 20000}}]
|
||||
(.goto ^js page (str url) #js {:waitUntil wait-until :timeout timeout})))
|
||||
|
||||
(defn sleep
|
||||
[page ms]
|
||||
(.waitForTimeout ^js page ms))
|
||||
|
||||
(defn wait-for
|
||||
([page selector] (wait-for page selector nil))
|
||||
([page selector {:keys [visible timeout] :or {visible false timeout 10000}}]
|
||||
(.waitForSelector ^js page selector #js {:visible visible})))
|
||||
([locator] (wait-for locator nil))
|
||||
([locator {:keys [state timeout] :or {state "visible" timeout 10000}}]
|
||||
(.waitFor ^js locator #js {:state state :timeout timeout})))
|
||||
|
||||
(defn screenshot
|
||||
([frame] (screenshot frame nil))
|
||||
([frame {:keys [full-page? omit-background? type]
|
||||
:or {type "png"
|
||||
full-page? false
|
||||
omit-background? false}}]
|
||||
([frame] (screenshot frame {}))
|
||||
([frame {:keys [full-page? omit-background? type quality]
|
||||
:or {type "png" full-page? false omit-background? false quality 95}}]
|
||||
(let [options (-> (obj/new)
|
||||
(obj/set! "type" (name type))
|
||||
(obj/set! "omitBackground" omit-background?)
|
||||
(cond-> full-page? (-> (obj/set! "fullPage" true)
|
||||
(obj/set! "clip" nil))))]
|
||||
(cond-> (= "jpeg" type) (obj/set! "quality" quality))
|
||||
(cond-> full-page? (-> (obj/set! "fullPage" true)
|
||||
(obj/set! "clip" nil))))]
|
||||
(.screenshot ^js frame options))))
|
||||
|
||||
(defn emulate-media!
|
||||
[page {:keys [media]}]
|
||||
(.emulateMedia ^js page #js {:media media})
|
||||
page)
|
||||
|
||||
(defn pdf
|
||||
([page] (pdf page nil))
|
||||
([page {:keys [viewport save-path]}]
|
||||
(p/let [viewport (d/merge default-viewport viewport)]
|
||||
(.emulateMediaType ^js page "screen")
|
||||
(.pdf ^js page #js {:path save-path
|
||||
:width (:width viewport)
|
||||
:height (:height viewport)
|
||||
:scale (:scale viewport)
|
||||
:printBackground true
|
||||
:preferCSSPageSize true}))))
|
||||
([page] (pdf page {}))
|
||||
([page {:keys [width height scale save-path]
|
||||
:or {width default-viewport-width
|
||||
height default-viewport-height
|
||||
scale 1}}]
|
||||
(.pdf ^js page #js {:path save-path
|
||||
:width width
|
||||
:height height
|
||||
:scale scale
|
||||
:printBackground true
|
||||
:preferCSSPageSize true})))
|
||||
(defn eval!
|
||||
[frame f]
|
||||
(.evaluate ^js frame f))
|
||||
|
||||
(defn select
|
||||
[frame selector]
|
||||
(.$ ^js frame selector))
|
||||
(.locator ^js frame selector))
|
||||
|
||||
(defn select-all
|
||||
[frame selector]
|
||||
|
@ -103,23 +97,14 @@
|
|||
(defonce pool (atom nil))
|
||||
(defonce pool-browser-id (atom 1))
|
||||
|
||||
(def default-chrome-args
|
||||
#js ["--no-sandbox"
|
||||
"--font-render-hinting=none"
|
||||
"--disable-setuid-sandbox"
|
||||
"--disable-accelerated-2d-canvas"
|
||||
"--disable-gpu"])
|
||||
|
||||
(def browser-pool-factory
|
||||
(letfn [(create []
|
||||
(let [path (cf/get :browser-executable-path "/usr/bin/google-chrome")]
|
||||
(-> (pp/launch #js {:executablePath path :args default-chrome-args})
|
||||
(p/then (fn [browser]
|
||||
(let [id (deref pool-browser-id)]
|
||||
(l/info :origin "factory" :action "create" :browser-id id)
|
||||
(unchecked-set browser "__id" id)
|
||||
(swap! pool-browser-id inc)
|
||||
browser))))))
|
||||
(p/let [browser (.launch pw/chromium)
|
||||
id (swap! pool-browser-id inc)]
|
||||
(l/info :origin "factory" :action "create" :browser-id id)
|
||||
(unchecked-set browser "__id" id)
|
||||
browser))
|
||||
|
||||
(destroy [obj]
|
||||
(let [id (unchecked-get obj "__id")]
|
||||
(l/info :origin "factory" :action "destroy" :browser-id id)
|
||||
|
@ -137,14 +122,13 @@
|
|||
(defn init
|
||||
[]
|
||||
(l/info :msg "initializing browser pool")
|
||||
(let [opts #js {:max (cf/get :browser-pool-max 3)
|
||||
:min (cf/get :browser-pool-min 0)
|
||||
(let [opts #js {:max (cf/get :exporter-browser-pool-max 5)
|
||||
:min (cf/get :exporter-browser-pool-min 0)
|
||||
:testOnBorrow true
|
||||
:evictionRunIntervalMillis 5000
|
||||
:numTestsPerEvictionRun 5
|
||||
:acquireTimeoutMillis 120000 ; 2min
|
||||
:idleTimeoutMillis 10000}]
|
||||
|
||||
(reset! pool (gp/createPool browser-pool-factory opts))
|
||||
(p/resolved nil)))
|
||||
|
||||
|
@ -152,73 +136,40 @@
|
|||
[]
|
||||
(when-let [pool (deref pool)]
|
||||
(l/info :msg "finalizing browser pool")
|
||||
(-> (.drain ^js pool)
|
||||
(p/then (fn [] (.clear ^js pool))))))
|
||||
(p/do!
|
||||
(.drain ^js pool)
|
||||
(.clear ^js pool))))
|
||||
|
||||
(defn- ex-ignore
|
||||
[p]
|
||||
(p/handle p (constantly nil)))
|
||||
|
||||
(defn exec!
|
||||
[callback]
|
||||
(letfn [(release-browser [pool browser]
|
||||
(let [id (unchecked-get browser "__id")]
|
||||
(-> (p/do! (.release ^js pool browser))
|
||||
(p/handle (fn [res err]
|
||||
(l/trace :action "exec:release-browser" :browser-id id)
|
||||
(when err (js/console.log err))
|
||||
(if err
|
||||
(p/rejected err)
|
||||
(p/resolved res)))))))
|
||||
[config handle]
|
||||
(letfn [(handle-browser [browser]
|
||||
(p/let [id (unchecked-get browser "__id")
|
||||
context (.newContext ^js browser config)]
|
||||
(l/trace :hint "exec:handle:start" :browser-id id)
|
||||
(p/let [page (.newPage ^js context)
|
||||
result (handle page)]
|
||||
(.close ^js context)
|
||||
(l/trace :hint "exec:handle:end" :browser-id id)
|
||||
result)))
|
||||
|
||||
(destroy-browser [pool browser]
|
||||
(let [id (unchecked-get browser "__id")]
|
||||
(-> (p/do! (.destroy ^js pool browser))
|
||||
(p/handle (fn [res err]
|
||||
(l/trace :action "exec:destroy-browser" :browser-id id)
|
||||
(when err (js/console.log err))
|
||||
(if err
|
||||
(p/rejected err)
|
||||
(p/resolved res)))))))
|
||||
|
||||
(handle-error [pool browser obj err]
|
||||
(let [id (unchecked-get browser "__id")]
|
||||
(if err
|
||||
(do
|
||||
(l/trace :action "exec:handle-error" :browser-id id)
|
||||
(-> (p/do! (destroy-browser pool browser))
|
||||
(p/handle #(p/rejected err))))
|
||||
(p/resolved obj))))
|
||||
|
||||
(on-result [pool browser context result]
|
||||
(let [id (unchecked-get browser "__id")]
|
||||
(l/trace :action "exec:on-result" :browser-id id)
|
||||
(-> (p/do! (.close ^js context))
|
||||
(p/handle (fn [_ err]
|
||||
(if err
|
||||
(destroy-browser pool browser)
|
||||
(release-browser pool browser))))
|
||||
(p/handle #(p/resolved result)))))
|
||||
|
||||
(on-page [pool browser context page]
|
||||
(let [id (unchecked-get browser "__id")]
|
||||
(l/trace :action "exec:on-page" :browser-id id)
|
||||
(-> (p/do! (callback page))
|
||||
(p/handle (partial handle-error pool browser))
|
||||
(p/then (partial on-result pool browser context)))))
|
||||
|
||||
(on-context [pool browser ctx]
|
||||
(let [id (unchecked-get browser "__id")]
|
||||
(l/trace :action "exec:on-context" :browser-id id)
|
||||
(-> (p/do! (.newPage ^js ctx))
|
||||
(p/handle (partial handle-error pool browser))
|
||||
(p/then (partial on-page pool browser ctx)))))
|
||||
|
||||
(on-acquire [pool browser err]
|
||||
(let [id (unchecked-get browser "__id")]
|
||||
(l/trace :action "exec:on-acquire" :browser-id id)
|
||||
(if err
|
||||
(js/console.log err)
|
||||
(-> (p/do! (.createIncognitoBrowserContext ^js browser))
|
||||
(p/handle (partial handle-error pool browser))
|
||||
(p/then (partial on-context pool browser))))))]
|
||||
(on-acquire [pool browser]
|
||||
(-> (handle-browser browser)
|
||||
(p/then (fn [result]
|
||||
(.release ^js pool browser)
|
||||
result))
|
||||
(p/catch (fn [cause]
|
||||
(p/do!
|
||||
(ex-ignore (.destroy ^js pool browser))
|
||||
(p/rejected cause))))))
|
||||
]
|
||||
|
||||
(when-let [pool (deref pool)]
|
||||
(-> (p/do! (.acquire ^js pool))
|
||||
(p/handle (partial on-acquire pool))))))
|
||||
(p/then (partial on-acquire pool))
|
||||
(p/catch (fn [cause]
|
||||
(js/console.log "KKK" cause)
|
||||
(p/rejected cause)))))))
|
||||
|
|
|
@ -13,38 +13,41 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.common.version :as v]
|
||||
[app.common.uri :as u]
|
||||
[cljs.core :as c]
|
||||
[cljs.pprint]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lambdaisland.uri :as u]))
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def defaults
|
||||
{:public-uri "http://localhost:3449"
|
||||
:tenant "dev"
|
||||
:host "devenv"
|
||||
:http-server-port 6061
|
||||
:browser-concurrency 5
|
||||
:browser-strategy :incognito})
|
||||
:http-server-host "localhost"
|
||||
:redis-uri "redis://redis/0"
|
||||
:exporter-domain-whitelist #{"localhost2:3449"}})
|
||||
|
||||
(s/def ::browser-concurrency ::us/integer)
|
||||
(s/def ::browser-executable-path ::us/string)
|
||||
(s/def ::browser-strategy ::us/keyword)
|
||||
(s/def ::http-server-port ::us/integer)
|
||||
(s/def ::public-uri ::us/string)
|
||||
(s/def ::sentry-dsn ::us/string)
|
||||
(s/def ::http-server-host ::us/string)
|
||||
(s/def ::public-uri ::us/uri)
|
||||
(s/def ::tenant ::us/string)
|
||||
(s/def ::host ::us/string)
|
||||
|
||||
(s/def ::exporter-domain-whitelist ::us/set-of-str)
|
||||
(s/def ::exporter-browser-pool-max ::us/integer)
|
||||
(s/def ::exporter-browser-pool-min ::us/integer)
|
||||
|
||||
(s/def ::config
|
||||
(s/keys :opt-un [::public-uri
|
||||
::sentry-dsn
|
||||
::host
|
||||
::tenant
|
||||
::http-server-port
|
||||
::browser-concurrency
|
||||
::browser-strategy
|
||||
::browser-executable-path]))
|
||||
::http-server-host
|
||||
::exporter-browser-pool-max
|
||||
::exporter-browser-pool-min
|
||||
::exporter-domain-whitelist]))
|
||||
|
||||
(defn- read-env
|
||||
[prefix]
|
||||
(let [env (unchecked-get process "env")
|
||||
|
@ -62,10 +65,14 @@
|
|||
|
||||
(defn- prepare-config
|
||||
[]
|
||||
(let [env (read-env "penpot")
|
||||
env (d/without-nils env)
|
||||
data (merge defaults env)]
|
||||
(us/conform ::config data)))
|
||||
(try
|
||||
(let [env (read-env "penpot")
|
||||
env (d/without-nils env)
|
||||
data (merge defaults env)]
|
||||
(us/conform ::config data))
|
||||
(catch :default cause
|
||||
(js/console.log (us/pretty-explain (ex-data cause)))
|
||||
(throw cause))))
|
||||
|
||||
(def config
|
||||
(atom (prepare-config)))
|
||||
|
|
|
@ -6,24 +6,23 @@
|
|||
|
||||
(ns app.core
|
||||
(:require
|
||||
["process" :as proc]
|
||||
[app.browser :as bwr]
|
||||
[app.redis :as redis]
|
||||
[app.common.logging :as l]
|
||||
[app.config]
|
||||
[app.http :as http]
|
||||
[app.sentry :as sentry]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(enable-console-print!)
|
||||
(l/initialize!)
|
||||
(sentry/init!)
|
||||
|
||||
(defonce state (atom nil))
|
||||
|
||||
(defn start
|
||||
[& args]
|
||||
(l/info :msg "initializing")
|
||||
(p/do!
|
||||
(bwr/init)
|
||||
(redis/init)
|
||||
(http/init)))
|
||||
|
||||
(def main start)
|
||||
|
@ -36,5 +35,9 @@
|
|||
(l/info :msg "stoping")
|
||||
(p/do!
|
||||
(bwr/stop)
|
||||
(redis/stop)
|
||||
(http/stop)
|
||||
(done)))
|
||||
|
||||
(proc/on "uncaughtException" (fn [cause]
|
||||
(js/console.error cause)))
|
||||
|
|
98
exporter/src/app/handlers.cljs
Normal file
98
exporter/src/app/handlers.cljs
Normal file
|
@ -0,0 +1,98 @@
|
|||
;; 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.handlers
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.handlers.export-frames :as export-frames]
|
||||
[app.handlers.export-shapes :as export-shapes]
|
||||
[app.handlers.resources :as resources]
|
||||
[app.util.transit :as t]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[reitit.core :as r]))
|
||||
|
||||
(l/set-level! :info)
|
||||
|
||||
(defn on-error
|
||||
[error exchange]
|
||||
(let [{:keys [type message code] :as data} (ex-data error)]
|
||||
(cond
|
||||
(or (= :validation type)
|
||||
(= :assertion type))
|
||||
(let [explain (us/pretty-explain data)
|
||||
data (-> data
|
||||
(assoc :explain explain)
|
||||
(dissoc ::s/problems ::s/value ::s/spec))]
|
||||
(-> exchange
|
||||
(assoc :response/status 400)
|
||||
(assoc :response/body (t/encode data))
|
||||
(assoc :response/headers {"content-type" "application/transit+json"})))
|
||||
|
||||
(= :not-found type)
|
||||
(-> exchange
|
||||
(assoc :response/status 404)
|
||||
(assoc :response/body (t/encode data))
|
||||
(assoc :response/headers {"content-type" "application/transit+json"}))
|
||||
|
||||
(and (= :internal type)
|
||||
(= :browser-not-ready code))
|
||||
(-> exchange
|
||||
(assoc :response/status 503)
|
||||
(assoc :response/body (t/encode data))
|
||||
(assoc :response/headers {"content-type" "application/transit+json"}))
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/error :msg "Unexpected error" :cause error)
|
||||
(-> exchange
|
||||
(assoc :response/status 500)
|
||||
(assoc :response/body (t/encode data))
|
||||
(assoc :response/headers {"content-type" "application/transit+json"}))))))
|
||||
|
||||
(defmulti command-spec :cmd)
|
||||
|
||||
(s/def ::id ::us/string)
|
||||
(s/def ::uri ::us/uri)
|
||||
(s/def ::wait ::us/boolean)
|
||||
(s/def ::cmd ::us/keyword)
|
||||
|
||||
(defmethod command-spec :export-shapes [_] ::export-shapes/params)
|
||||
(defmethod command-spec :export-frames [_] ::export-frames/params)
|
||||
(defmethod command-spec :get-resource [_] (s/keys :req-un [::id]))
|
||||
|
||||
(s/def ::params
|
||||
(s/and (s/keys :req-un [::cmd]
|
||||
:opt-un [::wait ::uri])
|
||||
(s/multi-spec command-spec :cmd)))
|
||||
|
||||
(defn validate-uri!
|
||||
[uri]
|
||||
(let [white-list (cf/get :exporter-domain-whitelist #{})
|
||||
default (cf/get :public-uri)]
|
||||
(when-not (or (contains? white-list (u/get-domain uri))
|
||||
(= (u/get-domain default) (u/get-domain uri)))
|
||||
(ex/raise :type :validation
|
||||
:code :domain-not-allowed
|
||||
:hint "looks like the uri provided is not part of the white list"))))
|
||||
|
||||
(defn handler
|
||||
[{:keys [:request/params] :as exchange}]
|
||||
(let [{:keys [cmd uri] :as params} (us/conform ::params params)]
|
||||
(some-> uri validate-uri!)
|
||||
(case cmd
|
||||
:get-resource (resources/handler exchange)
|
||||
:export-shapes (export-shapes/handler exchange params)
|
||||
:export-frames (export-frames/handler exchange params)
|
||||
(ex/raise :type :internal
|
||||
:code :method-not-implemented
|
||||
:hint (dm/fmt "method % not implemented" cmd)))))
|
152
exporter/src/app/handlers/export_frames.cljs
Normal file
152
exporter/src/app/handlers/export_frames.cljs
Normal file
|
@ -0,0 +1,152 @@
|
|||
;; 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.handlers.export-frames
|
||||
(:require
|
||||
["path" :as path]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as exc :include-macros true]
|
||||
[app.common.spec :as us]
|
||||
[app.handlers.resources :as rsc]
|
||||
[app.redis :as redis]
|
||||
[app.renderer.pdf :as rp]
|
||||
[app.util.shell :as sh]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(declare ^:private handle-export)
|
||||
(declare ^:private create-pdf)
|
||||
(declare ^:private export-frame)
|
||||
(declare ^:private join-pdf)
|
||||
(declare ^:private move-file)
|
||||
(declare ^:private clean-tmp)
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::frame-id ::us/uuid)
|
||||
(s/def ::uri ::us/uri)
|
||||
|
||||
(s/def ::export
|
||||
(s/keys :req-un [::file-id ::page-id ::frame-id ::name]))
|
||||
|
||||
(s/def ::exports
|
||||
(s/every ::export :kind vector? :min-count 1))
|
||||
|
||||
(s/def ::params
|
||||
(s/keys :req-un [::exports]
|
||||
:opt-un [::uri ::name]))
|
||||
|
||||
(defn handler
|
||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports uri] :as params}]
|
||||
(let [xform (map #(assoc % :token auth-token :uri uri))
|
||||
exports (sequence xform exports)]
|
||||
(handle-export exchange (assoc params :exports exports))))
|
||||
|
||||
(defn handle-export
|
||||
[exchange {:keys [exports wait uri name] :as params}]
|
||||
(let [topic (-> exports first :file-id str)
|
||||
resource (rsc/create :pdf (or name (-> exports first :name)))
|
||||
|
||||
on-progress (fn [progress]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:status "running"
|
||||
:progress progress}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
on-complete (fn [resource]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:size (:size resource)
|
||||
:status "ended"}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
on-error (fn [cause]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:status "error"
|
||||
:cause (ex-message cause)}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
proc (create-pdf :resource resource
|
||||
:items exports
|
||||
:on-progress on-progress
|
||||
:on-complete on-complete
|
||||
:on-error on-error)]
|
||||
(if wait
|
||||
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
|
||||
(assoc exchange :response/body (dissoc resource :path)))))
|
||||
|
||||
(defn create-pdf
|
||||
[& {:keys [resource items on-progress on-complete on-error]
|
||||
:or {on-progress identity
|
||||
on-complete identity
|
||||
on-error identity}}]
|
||||
(p/let [progress (atom 0)
|
||||
tmpdir (sh/create-tmpdir! "pdfexport")
|
||||
file-id (-> items first :file-id)
|
||||
items (into [] (map #(partial export-frame tmpdir %)) items)
|
||||
xform (map (fn [export-fn]
|
||||
#(p/finally
|
||||
(export-fn)
|
||||
(fn [result _]
|
||||
(on-progress {:total (count items)
|
||||
:done (swap! progress inc)
|
||||
:name (:name result)})))))]
|
||||
(-> (reduce (fn [res export-fn]
|
||||
(p/let [res res
|
||||
out (export-fn)]
|
||||
(cons (:path out) res)))
|
||||
(p/resolved nil)
|
||||
(into '() xform items))
|
||||
(p/then (partial join-pdf tmpdir file-id))
|
||||
(p/then (partial move-file resource))
|
||||
(p/then (partial clean-tmp tmpdir))
|
||||
(p/then (fn [resource]
|
||||
(-> (sh/stat (:path resource))
|
||||
(p/then #(merge resource %)))))
|
||||
(p/finally (fn [result cause]
|
||||
(if cause
|
||||
(on-error cause)
|
||||
(on-complete result)))))))
|
||||
|
||||
(defn- export-frame
|
||||
[tmpdir {:keys [file-id page-id frame-id token uri] :as params}]
|
||||
(let [file-name (dm/fmt "%.pdf" frame-id)
|
||||
save-path (path/join tmpdir file-name)]
|
||||
(-> (rp/render {:name (dm/str frame-id)
|
||||
:uri uri
|
||||
:suffix ""
|
||||
:token token
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id frame-id
|
||||
:scale 1
|
||||
:save-path save-path})
|
||||
(p/then (fn [_]
|
||||
{:name file-name
|
||||
:path save-path})))))
|
||||
|
||||
(defn- join-pdf
|
||||
[tmpdir file-id paths]
|
||||
(let [output-path (path/join tmpdir (str file-id ".pdf"))
|
||||
paths-str (str/join " " paths)]
|
||||
(-> (sh/run-cmd! (str "pdfunite " paths-str " " output-path))
|
||||
(p/then (constantly output-path)))))
|
||||
|
||||
(defn- move-file
|
||||
[{:keys [path] :as resource} output-path]
|
||||
(p/do
|
||||
(sh/move! output-path path)
|
||||
resource))
|
||||
|
||||
(defn- clean-tmp
|
||||
[tdpath data]
|
||||
(p/do!
|
||||
(sh/rmdir! tdpath)
|
||||
data))
|
170
exporter/src/app/handlers/export_shapes.cljs
Normal file
170
exporter/src/app/handlers/export_shapes.cljs
Normal file
|
@ -0,0 +1,170 @@
|
|||
;; 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.handlers.export-shapes
|
||||
(:require
|
||||
[app.common.exceptions :as exc :include-macros true]
|
||||
[app.common.spec :as us]
|
||||
[app.redis :as redis]
|
||||
[app.handlers.resources :as rsc]
|
||||
[app.renderer.bitmap :as rb]
|
||||
[app.renderer.pdf :as rp]
|
||||
[app.renderer.svg :as rs]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(declare ^:private handle-exports)
|
||||
(declare ^:private handle-single-export)
|
||||
(declare ^:private handle-multiple-export)
|
||||
(declare ^:private run-export)
|
||||
(declare ^:private assign-file-name)
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(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 ::suffix ::us/string)
|
||||
(s/def ::type ::us/keyword)
|
||||
(s/def ::suffix string?)
|
||||
(s/def ::scale number?)
|
||||
(s/def ::uri ::us/uri)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::wait ::us/boolean)
|
||||
|
||||
(s/def ::export
|
||||
(s/keys :req-un [::page-id ::file-id ::object-id ::type ::suffix ::scale ::name]))
|
||||
|
||||
(s/def ::exports
|
||||
(s/coll-of ::export :kind vector? :min-count 1))
|
||||
|
||||
(s/def ::params
|
||||
(s/keys :req-un [::exports ::profile-id]
|
||||
:opt-un [::uri ::wait ::name]))
|
||||
|
||||
(defn handler
|
||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}]
|
||||
(let [xform (comp
|
||||
(map #(assoc % :token auth-token))
|
||||
(assign-file-name))
|
||||
exports (into [] xform exports)]
|
||||
(if (= 1 (count exports))
|
||||
(handle-single-export exchange (assoc params :export (first exports)))
|
||||
(handle-multiple-export exchange (assoc params :exports exports)))))
|
||||
|
||||
(defn- handle-single-export
|
||||
[exchange {:keys [export wait uri profile-id name] :as params}]
|
||||
(let [topic (str profile-id)
|
||||
resource (rsc/create (:type export) (or name (:name export)))
|
||||
|
||||
on-progress (fn [progress]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:status "running"
|
||||
:progress progress}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
on-complete (fn [resource]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:size (:size resource)
|
||||
:name (:name resource)
|
||||
:status "ended"}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
on-error (fn [cause]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:status "error"
|
||||
:cause (ex-message cause)}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
proc (rsc/create-simple :task #(run-export export)
|
||||
:resource resource
|
||||
:on-progress on-progress
|
||||
:on-error on-error
|
||||
:on-complete on-complete)]
|
||||
(if wait
|
||||
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
|
||||
(assoc exchange :response/body (dissoc resource :path)))))
|
||||
|
||||
(defn- handle-multiple-export
|
||||
[exchange {:keys [exports wait uri profile-id name] :as params}]
|
||||
(let [tasks (map #(fn [] (run-export %)) exports)
|
||||
topic (str profile-id)
|
||||
resource (rsc/create :zip (or name (-> exports first :name)))
|
||||
|
||||
on-progress (fn [progress]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:status "running"
|
||||
:progress progress}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
on-complete (fn [resource]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:size (:size resource)
|
||||
:status "ended"}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
on-error (fn [cause]
|
||||
(let [data {:type :export-update
|
||||
:resource-id (:id resource)
|
||||
:name (:name resource)
|
||||
:status "error"
|
||||
:cause (ex-message cause)}]
|
||||
(redis/pub! topic data)))
|
||||
|
||||
proc (rsc/create-zip :resource resource
|
||||
:tasks tasks
|
||||
:on-progress on-progress
|
||||
:on-complete on-complete
|
||||
:on-error on-error)]
|
||||
(if wait
|
||||
(p/then proc #(assoc exchange :response/body (dissoc % :path)))
|
||||
(assoc exchange :response/body (dissoc resource :path)))))
|
||||
|
||||
(defn- run-export
|
||||
[{:keys [type] :as params}]
|
||||
(p/let [res (case type
|
||||
:png (rb/render params)
|
||||
:jpeg (rb/render params)
|
||||
:svg (rs/render params)
|
||||
:pdf (rp/render params))]
|
||||
(assoc res :type type)))
|
||||
|
||||
(defn- assign-file-name
|
||||
"A transducer that assocs a candidate filename and avoid duplicates."
|
||||
[]
|
||||
(letfn [(find-candidate [params used]
|
||||
(loop [index 0]
|
||||
(let [candidate (str (:name params)
|
||||
(:suffix params "")
|
||||
(when (pos? index)
|
||||
(str "-" (inc index)))
|
||||
(case (:type params)
|
||||
:png ".png"
|
||||
:jpeg ".jpg"
|
||||
:svg ".svg"
|
||||
:pdf ".pdf"))]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc index))
|
||||
candidate))))]
|
||||
(fn [rf]
|
||||
(let [used (volatile! #{})]
|
||||
(fn
|
||||
([] (rf))
|
||||
([result] (rf result))
|
||||
([result params]
|
||||
(let [candidate (find-candidate params @used)
|
||||
params (assoc params :filename candidate)]
|
||||
(vswap! used conj candidate)
|
||||
(rf result params))))))))
|
130
exporter/src/app/handlers/resources.cljs
Normal file
130
exporter/src/app/handlers/resources.cljs
Normal file
|
@ -0,0 +1,130 @@
|
|||
;; 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.handlers.resources
|
||||
"Temporal resouces management."
|
||||
(:require
|
||||
["archiver" :as arc]
|
||||
["fs" :as fs]
|
||||
["os" :as os]
|
||||
["path" :as path]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.shell :as sh]
|
||||
[cljs.core :as c]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn- get-path
|
||||
[type id]
|
||||
(path/join (os/tmpdir) (dm/str "exporter." (d/name type) "." id)))
|
||||
|
||||
(defn- get-mtype
|
||||
[type]
|
||||
(case (d/name type)
|
||||
"zip" "application/zip"
|
||||
"jpeg" "image/jpeg"
|
||||
"png" "image/png"
|
||||
"pdf" "application/pdf"))
|
||||
|
||||
(defn create
|
||||
"Generates ephimeral resource object."
|
||||
[type name]
|
||||
(let [task-id (uuid/next)]
|
||||
{:path (get-path type task-id)
|
||||
:mtype (get-mtype type)
|
||||
:name name
|
||||
:id (dm/str (c/name type) "." task-id)}))
|
||||
|
||||
(defn- write-as-zip!
|
||||
[{:keys [id path]} items on-progress]
|
||||
(let [^js zip (arc/create "zip")
|
||||
^js out (fs/createWriteStream path)
|
||||
append! (fn [{:keys [data name] :as result}]
|
||||
(.append zip data #js {:name name}))
|
||||
progress (atom 0)]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(.on zip "error" #(reject %))
|
||||
(.on zip "end" resolve)
|
||||
(.on zip "entry" (fn [data]
|
||||
(let [name (unchecked-get data "name")
|
||||
num (swap! progress inc)]
|
||||
#_(when (= 2 num)
|
||||
(.abort ^js zip)
|
||||
(reject (js/Error. "unable to create zip file")))
|
||||
(on-progress
|
||||
{:total (count items)
|
||||
:done num}))))
|
||||
(.pipe zip out)
|
||||
(-> (reduce (fn [res export-fn]
|
||||
(p/then res (fn [_] (-> (export-fn) (p/then append!)))))
|
||||
(p/resolved 1)
|
||||
items)
|
||||
(p/then #(.finalize zip))
|
||||
(p/catch reject))))))
|
||||
|
||||
(defn create-simple
|
||||
[& {:keys [task resource on-progress on-complete on-error]
|
||||
:or {on-progress identity
|
||||
on-complete identity
|
||||
on-error identity}
|
||||
:as params}]
|
||||
(let [path (:path resource)]
|
||||
(-> (task)
|
||||
(p/then (fn [{:keys [data name]}]
|
||||
(on-progress {:total 1 :done 1 :name name})
|
||||
(.writeFile fs/promises path data)))
|
||||
(p/then #(sh/stat path))
|
||||
(p/then #(merge resource %))
|
||||
(p/finally (fn [result cause]
|
||||
(if cause
|
||||
(on-error cause)
|
||||
(on-complete result)))))))
|
||||
|
||||
(defn create-zip
|
||||
"Creates a resource with multiple files merget into a single zip file."
|
||||
[& {:keys [resource tasks on-error on-progress on-complete]
|
||||
:or {on-error identity
|
||||
on-progress identity
|
||||
on-complete identity}}]
|
||||
(let [{:keys [path id] :as resource} resource]
|
||||
(-> (write-as-zip! resource tasks on-progress)
|
||||
(p/then #(sh/stat path))
|
||||
(p/then #(merge resource %))
|
||||
(p/finally (fn [result cause]
|
||||
(if cause
|
||||
(on-error cause)
|
||||
(on-complete result)))))))
|
||||
|
||||
(defn- lookup
|
||||
[id]
|
||||
(p/let [[type task-id] (str/split id "." 2)
|
||||
path (get-path type task-id)
|
||||
mtype (get-mtype type)
|
||||
stat (sh/stat path)]
|
||||
|
||||
(when-not stat
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
{:stream (fs/createReadStream path)
|
||||
:headers {"content-type" mtype
|
||||
"content-length" (:size stat)}}))
|
||||
|
||||
(defn handler
|
||||
[{:keys [:request/params response] :as exchange}]
|
||||
(when-not (contains? params :id)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-id))
|
||||
|
||||
(-> (lookup (get params :id))
|
||||
(p/then (fn [{:keys [stream headers] :as resource}]
|
||||
(-> exchange
|
||||
(assoc :response/status 200)
|
||||
(assoc :response/body stream)
|
||||
(assoc :response/headers headers))))))
|
|
@ -6,67 +6,162 @@
|
|||
|
||||
(ns app.http
|
||||
(:require
|
||||
["cookies" :as Cookies]
|
||||
["http" :as http]
|
||||
["inflation" :as inflate]
|
||||
["raw-body" :as raw-body]
|
||||
["stream" :as stream]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]
|
||||
[app.http.export :refer [export-handler]]
|
||||
[app.http.export-frames :refer [export-frames-handler]]
|
||||
[app.http.impl :as impl]
|
||||
[app.sentry :as sentry]
|
||||
[app.util.transit :as t]
|
||||
[app.handlers :as handlers]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[reitit.core :as r]))
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(l/set-level! :info)
|
||||
|
||||
(def routes
|
||||
[["/export-frames" {:handler export-frames-handler}]
|
||||
["/export" {:handler export-handler}]])
|
||||
(defprotocol IStreamableResponseBody
|
||||
(write-body! [_ response]))
|
||||
|
||||
(extend-protocol IStreamableResponseBody
|
||||
string
|
||||
(write-body! [data response]
|
||||
(.write ^js response data)
|
||||
(.end ^js response))
|
||||
|
||||
js/Buffer
|
||||
(write-body! [data response]
|
||||
(.write ^js response data)
|
||||
(.end ^js response))
|
||||
|
||||
stream/Stream
|
||||
(write-body! [data response]
|
||||
(.pipe ^js data response)
|
||||
(.on ^js data "error" (fn [cause]
|
||||
(js/console.error cause)
|
||||
(.end response)))))
|
||||
|
||||
(defn- handle-response
|
||||
[{:keys [:response/body
|
||||
:response/headers
|
||||
:response/status
|
||||
response]
|
||||
:as exchange}]
|
||||
(let [status (or status 200)
|
||||
headers (clj->js headers)
|
||||
body (or body "")]
|
||||
(.writeHead ^js response status headers)
|
||||
(write-body! body response)))
|
||||
|
||||
(defn- parse-headers
|
||||
[req]
|
||||
(let [orig (unchecked-get req "headers")]
|
||||
(persistent!
|
||||
(reduce #(assoc! %1 (str/lower %2) (unchecked-get orig %2))
|
||||
(transient {})
|
||||
(js/Object.keys orig)))))
|
||||
|
||||
(defn- wrap-body-params
|
||||
[handler]
|
||||
(let [opts #js {:limit "2mb" :encoding "utf8"}]
|
||||
(fn [{:keys [:request/method :request/headers request] :as exchange}]
|
||||
(let [ctype (get headers "content-type")]
|
||||
(if (= method "post")
|
||||
(-> (raw-body (inflate request) opts)
|
||||
(p/then (fn [data]
|
||||
(cond-> data
|
||||
(= ctype "application/transit+json")
|
||||
(t/decode-str))))
|
||||
(p/then (fn [data]
|
||||
(handler (assoc exchange :request/body-params data)))))
|
||||
(handler exchange))))))
|
||||
|
||||
(defn- wrap-params
|
||||
[handler]
|
||||
(fn [{:keys [:request/body-params :request/query-params] :as exchange}]
|
||||
(handler (assoc exchange :request/params (merge query-params body-params)))))
|
||||
|
||||
(defn- wrap-response-format
|
||||
[handler]
|
||||
(fn [exchange]
|
||||
(p/then
|
||||
(handler exchange)
|
||||
(fn [{:keys [:response/body :response/status] :as exchange}]
|
||||
(cond
|
||||
(map? body)
|
||||
(let [data (t/encode-str body {:type :json-verbose})]
|
||||
(-> exchange
|
||||
(assoc :response/body data)
|
||||
(assoc :response/status 200)
|
||||
(update :response/headers assoc "content-type" "application/transit+json")
|
||||
(update :response/headers assoc "content-length" (count data))))
|
||||
|
||||
(and (nil? body)
|
||||
(= 200 status))
|
||||
(-> exchange
|
||||
(assoc :response/body "")
|
||||
(assoc :response/status 204)
|
||||
(assoc :response/headers {"content-length" 0}))
|
||||
|
||||
:else
|
||||
exchange)))))
|
||||
|
||||
(defn- wrap-query-params
|
||||
[handler]
|
||||
(fn [{:keys [:request/uri] :as exchange}]
|
||||
(handler (assoc exchange :request/query-params (u/query-string->map (:query uri))))))
|
||||
|
||||
(defn- wrap-error
|
||||
[handler on-error]
|
||||
(fn [exchange]
|
||||
(-> (p/do (handler exchange))
|
||||
(p/catch (fn [cause] (on-error cause exchange))))))
|
||||
|
||||
(defn- wrap-auth
|
||||
[handler cookie-name]
|
||||
(fn [{:keys [:request/cookies] :as exchange}]
|
||||
(let [token (.get ^js cookies cookie-name)]
|
||||
(handler (cond-> exchange token (assoc :request/auth-token token))))))
|
||||
|
||||
(defn- create-adapter
|
||||
[handler]
|
||||
(fn [req res]
|
||||
(let [cookies (Cookies. req res)
|
||||
headers (parse-headers req)
|
||||
uri (u/uri (unchecked-get req "url"))
|
||||
exchange {:request/method (str/lower (unchecked-get req "method"))
|
||||
:request/path (:path uri)
|
||||
:request/uri uri
|
||||
:request/headers headers
|
||||
:request/cookies cookies
|
||||
:request req
|
||||
:response res}]
|
||||
(-> (p/do (handler exchange))
|
||||
(p/then handle-response)))))
|
||||
|
||||
(defn- create-server
|
||||
[handler]
|
||||
(.createServer ^js http (create-adapter handler)))
|
||||
|
||||
(def instance (atom nil))
|
||||
|
||||
(defn- on-error
|
||||
[error request]
|
||||
(let [{:keys [type message code] :as data} (ex-data error)]
|
||||
(sentry/capture-exception error {::sentry/request request
|
||||
:ex-data data})
|
||||
|
||||
(cond
|
||||
(= :validation type)
|
||||
(let [header (get-in request [:headers "accept"])]
|
||||
(if (and (str/starts-with? header "text/html")
|
||||
(= :spec-validation (:code data)))
|
||||
{:status 400
|
||||
:headers {"content-type" "text/html"}
|
||||
:body (str "<pre style='font-size:16px'>" (:explain data) "</pre>\n")}
|
||||
{:status 400
|
||||
:headers {"content-type" "text/html"}
|
||||
: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
|
||||
(do
|
||||
(l/error :msg "Unexpected error" :error error)
|
||||
(js/console.error error)
|
||||
{:status 500
|
||||
:headers {"x-error" (t/encode data)}
|
||||
:body ""}))))
|
||||
|
||||
(defn init
|
||||
[]
|
||||
(let [router (r/router routes)
|
||||
handler (impl/router-handler router)
|
||||
server (impl/server handler on-error)
|
||||
(let [handler (-> handlers/handler
|
||||
(wrap-auth "auth-token")
|
||||
(wrap-response-format)
|
||||
(wrap-params)
|
||||
(wrap-query-params)
|
||||
(wrap-body-params)
|
||||
(wrap-error handlers/on-error))
|
||||
server (create-server handler)
|
||||
port (cf/get :http-server-port 6061)]
|
||||
(.listen server port)
|
||||
(l/info :msg "welcome to penpot"
|
||||
:module "exporter"
|
||||
:version (:full @cf/version))
|
||||
:module "exporter"
|
||||
:version (:full @cf/version))
|
||||
(l/info :msg "starting http server" :port port)
|
||||
(reset! instance server)))
|
||||
|
||||
|
|
|
@ -1,125 +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
|
||||
(:require
|
||||
[app.common.exceptions :as exc :include-macros true]
|
||||
[app.common.spec :as us]
|
||||
[app.renderer.bitmap :as rb]
|
||||
[app.renderer.pdf :as rp]
|
||||
[app.renderer.svg :as rs]
|
||||
[app.zipfile :as zip]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(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 ::suffix ::us/string)
|
||||
(s/def ::type ::us/keyword)
|
||||
(s/def ::suffix string?)
|
||||
(s/def ::scale number?)
|
||||
|
||||
(s/def ::export (s/keys :req-un [::type ::suffix ::scale]))
|
||||
(s/def ::exports (s/coll-of ::export :kind vector?))
|
||||
|
||||
(s/def ::handler-params
|
||||
(s/keys :req-un [::page-id ::file-id ::object-id ::name ::exports]))
|
||||
|
||||
(declare handle-single-export)
|
||||
(declare handle-multiple-export)
|
||||
(declare perform-export)
|
||||
(declare attach-filename)
|
||||
|
||||
(defn export-handler
|
||||
[{:keys [params cookies] :as request}]
|
||||
(let [{:keys [exports page-id file-id object-id name]} (us/conform ::handler-params params)
|
||||
token (.get ^js cookies "auth-token")]
|
||||
(case (count exports)
|
||||
0 (exc/raise :type :validation
|
||||
:code :missing-exports)
|
||||
|
||||
1 (-> (first exports)
|
||||
(assoc :name name)
|
||||
(assoc :token token)
|
||||
(assoc :file-id file-id)
|
||||
(assoc :page-id page-id)
|
||||
(assoc :object-id object-id)
|
||||
(handle-single-export))
|
||||
|
||||
(->> exports
|
||||
(map (fn [item]
|
||||
(-> item
|
||||
(assoc :name name)
|
||||
(assoc :token token)
|
||||
(assoc :file-id file-id)
|
||||
(assoc :page-id page-id)
|
||||
(assoc :object-id object-id))))
|
||||
(handle-multiple-export)))))
|
||||
|
||||
(defn- handle-single-export
|
||||
[params]
|
||||
(p/let [result (perform-export params)]
|
||||
{:status 200
|
||||
:body (:content result)
|
||||
:headers {"content-type" (:mime-type result)
|
||||
"content-length" (:length result)}}))
|
||||
|
||||
(defn- handle-multiple-export
|
||||
[exports]
|
||||
(let [proms (->> exports
|
||||
(attach-filename)
|
||||
(map perform-export))]
|
||||
(-> (p/all proms)
|
||||
(p/then (fn [results]
|
||||
(reduce #(zip/add! %1 (:filename %2) (:content %2)) (zip/create) results)))
|
||||
(p/then (fn [fzip]
|
||||
(.generateAsync ^js fzip #js {:type "uint8array"})))
|
||||
(p/then (fn [data]
|
||||
{:status 200
|
||||
:headers {"content-type" "application/zip"}
|
||||
:body data})))))
|
||||
|
||||
(defn- perform-export
|
||||
[params]
|
||||
(case (:type params)
|
||||
:png (rb/render params)
|
||||
:jpeg (rb/render params)
|
||||
:svg (rs/render params)
|
||||
:pdf (rp/render params)))
|
||||
|
||||
(defn- find-filename-candidate
|
||||
[params used]
|
||||
(loop [index 0]
|
||||
(let [candidate (str (:name params)
|
||||
(:suffix params "")
|
||||
(when (pos? index)
|
||||
(str "-" (inc index)))
|
||||
(case (:type params)
|
||||
:png ".png"
|
||||
:jpeg ".jpg"
|
||||
:svg ".svg"
|
||||
:pdf ".pdf"))]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc index))
|
||||
candidate))))
|
||||
|
||||
(defn- attach-filename
|
||||
[exports]
|
||||
(loop [exports (seq exports)
|
||||
used #{}
|
||||
result []]
|
||||
(if (nil? exports)
|
||||
result
|
||||
(let [export (first exports)
|
||||
candidate (find-filename-candidate export used)
|
||||
export (assoc export :filename candidate)]
|
||||
(recur (next exports)
|
||||
(conj used candidate)
|
||||
(conj result export))))))
|
|
@ -1,73 +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-frames
|
||||
(:require
|
||||
["path" :as path]
|
||||
[app.common.exceptions :as exc :include-macros true]
|
||||
[app.common.spec :as us]
|
||||
[app.renderer.pdf :as rp]
|
||||
[app.util.shell :as sh]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::frame-id ::us/uuid)
|
||||
(s/def ::frame-ids (s/coll-of ::frame-id :kind vector?))
|
||||
|
||||
(s/def ::handler-params
|
||||
(s/keys :req-un [::file-id ::page-id ::frame-ids]))
|
||||
|
||||
(defn- export-frame
|
||||
[tdpath file-id page-id token frame-id spaths]
|
||||
(p/let [spath (path/join tdpath (str frame-id ".pdf"))
|
||||
result (rp/render {:name (str frame-id)
|
||||
:suffix ""
|
||||
:token token
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id frame-id
|
||||
:scale 1
|
||||
:save-path spath})]
|
||||
(conj spaths spath)))
|
||||
|
||||
(defn- join-files
|
||||
[tdpath file-id paths]
|
||||
(let [output-path (path/join tdpath (str file-id ".pdf"))
|
||||
paths-str (str/join " " paths)]
|
||||
(-> (sh/run-cmd! (str "pdfunite " paths-str " " output-path))
|
||||
(p/then (constantly output-path)))))
|
||||
|
||||
(defn- clean-tmp-data
|
||||
[tdpath data]
|
||||
(p/do!
|
||||
(sh/rmdir! tdpath)
|
||||
data))
|
||||
|
||||
(defn export-frames-handler
|
||||
[{:keys [params cookies] :as request}]
|
||||
(let [{:keys [name file-id page-id frame-ids]} (us/conform ::handler-params params)
|
||||
token (.get ^js cookies "auth-token")]
|
||||
(if (seq frame-ids)
|
||||
(p/let [tdpath (sh/create-tmpdir! "pdfexport-")
|
||||
data (-> (reduce (fn [promise frame-id]
|
||||
(p/then promise (partial export-frame tdpath file-id page-id token frame-id)))
|
||||
(p/future [])
|
||||
(reverse frame-ids))
|
||||
(p/then (partial join-files tdpath file-id))
|
||||
(p/then sh/read-file)
|
||||
(p/then (partial clean-tmp-data tdpath)))]
|
||||
{:status 200
|
||||
:body data
|
||||
:headers {"content-type" "application/pdf"
|
||||
"content-length" (.-length data)}})
|
||||
{:status 204
|
||||
:body ""
|
||||
:headers {"content-type" "text/plain"}})))
|
||||
|
|
@ -1,95 +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.impl
|
||||
(:require
|
||||
["http" :as http]
|
||||
["cookies" :as Cookies]
|
||||
["inflation" :as inflate]
|
||||
["raw-body" :as raw-body]
|
||||
[app.util.transit :as t]
|
||||
[cuerdas.core :as str]
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.core :as p]
|
||||
[reitit.core :as r]))
|
||||
|
||||
(def methods-with-body
|
||||
#{"POST" "PUT" "DELETE"})
|
||||
|
||||
(defn- match
|
||||
[router {:keys [path query] :as request}]
|
||||
(when-let [match (r/match-by-path router path)]
|
||||
(assoc match :query-params (u/query-string->map query))))
|
||||
|
||||
(defn- handle-response
|
||||
[req res]
|
||||
(fn [{:keys [body headers status] :or {headers {} status 200}}]
|
||||
(.writeHead ^js res status (clj->js headers))
|
||||
(.end ^js res body)))
|
||||
|
||||
(defn- parse-headers
|
||||
[req]
|
||||
(let [orig (unchecked-get req "headers")]
|
||||
(persistent!
|
||||
(reduce #(assoc! %1 %2 (unchecked-get orig %2))
|
||||
(transient {})
|
||||
(js/Object.keys orig)))))
|
||||
|
||||
(defn- parse-body
|
||||
[req]
|
||||
(let [headers (unchecked-get req "headers")
|
||||
method (unchecked-get req "method")
|
||||
ctype (unchecked-get headers "content-type")
|
||||
opts #js {:limit "5mb" :encoding "utf8"}]
|
||||
(when (contains? methods-with-body method)
|
||||
(-> (raw-body (inflate req) opts)
|
||||
(p/then (fn [data]
|
||||
(cond-> data
|
||||
(= ctype "application/transit+json")
|
||||
(t/decode))))))))
|
||||
|
||||
(defn- handler-adapter
|
||||
[handler on-error]
|
||||
(fn [req res]
|
||||
(let [cookies (new Cookies req res)
|
||||
headers (parse-headers req)
|
||||
uri (u/uri (unchecked-get req "url"))
|
||||
request {:method (str/lower (unchecked-get req "method"))
|
||||
:path (:path uri)
|
||||
:query (:query uri)
|
||||
:url uri
|
||||
:headers headers
|
||||
:cookies cookies
|
||||
:internal-request req
|
||||
:internal-response res}]
|
||||
(-> (parse-body req)
|
||||
(p/then (fn [body]
|
||||
(let [request (assoc request :body body)]
|
||||
(handler request))))
|
||||
(p/catch (fn [error] (on-error error request)))
|
||||
(p/then (handle-response req res))))))
|
||||
|
||||
(defn router-handler
|
||||
[router]
|
||||
(fn [{:keys [body] :as request}]
|
||||
(let [route (match router request)
|
||||
params (merge {}
|
||||
(:query-params route)
|
||||
(:path-params route)
|
||||
(when (map? body) body))
|
||||
request (assoc request
|
||||
:route route
|
||||
:params params)
|
||||
|
||||
handler (get-in route [:data :handler])]
|
||||
(if (and route handler)
|
||||
(handler request)
|
||||
{:status 404
|
||||
:body "Not found"}))))
|
||||
|
||||
(defn server
|
||||
[handler on-error]
|
||||
(.createServer ^js http (handler-adapter handler on-error)))
|
54
exporter/src/app/redis.cljs
Normal file
54
exporter/src/app/redis.cljs
Normal file
|
@ -0,0 +1,54 @@
|
|||
;; 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.redis
|
||||
(:require
|
||||
["ioredis" :as redis]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]))
|
||||
|
||||
(l/set-level! :trace)
|
||||
|
||||
(def client (atom nil))
|
||||
|
||||
(defn- create-client
|
||||
[uri]
|
||||
(let [^js client (new redis uri)]
|
||||
(.on client "connect"
|
||||
(fn [] (l/info :hint "redis connection established" :uri uri)))
|
||||
(.on client "error"
|
||||
(fn [cause] (l/error :hint "error on redis connection" :cause cause)))
|
||||
(.on client "close"
|
||||
(fn [] (l/warn :hint "connection closed")))
|
||||
(.on client "reconnect"
|
||||
(fn [ms] (l/warn :hint "reconnecting to redis" :ms ms)))
|
||||
(.on client "end"
|
||||
(fn [ms] (l/warn :hint "client ended, no more connections will be attempted")))
|
||||
client))
|
||||
|
||||
(defn init
|
||||
[]
|
||||
(swap! client (fn [prev]
|
||||
(when prev (.disconnect ^js prev))
|
||||
(create-client (cf/get :redis-uri)))))
|
||||
|
||||
|
||||
(defn stop
|
||||
[]
|
||||
(swap! client (fn [client]
|
||||
(when client (.quit ^js client))
|
||||
nil)))
|
||||
|
||||
(def ^:private tenant (cf/get :tenant))
|
||||
|
||||
(defn pub!
|
||||
[topic payload]
|
||||
(let [payload (if (map? payload) (t/encode-str payload) payload)
|
||||
topic (dm/str tenant "." topic)]
|
||||
(when-let [client @client]
|
||||
(.publish ^js client topic payload))))
|
|
@ -16,46 +16,34 @@
|
|||
[app.config :as cf]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lambdaisland.uri :as u]
|
||||
[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
|
||||
[{: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]
|
||||
(l/info :uri uri)
|
||||
(let [viewport {:width 1920
|
||||
:height 1080
|
||||
:scale scale}
|
||||
options {:viewport viewport
|
||||
:cookie cookie}]
|
||||
(p/do!
|
||||
(bw/configure-page! page options)
|
||||
(bw/navigate! page uri)
|
||||
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
||||
(bw/wait-for page "#screenshot")
|
||||
(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! handle)))
|
||||
[{:keys [file-id page-id object-id token scale type uri]}]
|
||||
(p/let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
||||
uri (-> (or uri (cf/get :public-uri))
|
||||
(assoc :path "/")
|
||||
(assoc :fragment path))]
|
||||
(bw/exec!
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:viewport #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:locale "en-US"
|
||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||
:deviceScaleFactor scale
|
||||
:userAgent bw/default-user-agent}
|
||||
(fn [page]
|
||||
(l/info :uri uri)
|
||||
(p/do!
|
||||
(bw/nav! page (str uri))
|
||||
(p/let [node (bw/select page "#screenshot")]
|
||||
(bw/wait-for node)
|
||||
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
||||
(bw/sleep page 2000) ; the good old fix with sleep
|
||||
(case type
|
||||
:png (bw/screenshot node {:omit-background? true :type type})
|
||||
:jpeg (bw/screenshot node {:omit-background? false :type type}))))))))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
|
@ -65,25 +53,32 @@
|
|||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::scale ::us/number)
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::filename ::us/string)
|
||||
(s/def ::origin ::us/string)
|
||||
(s/def ::uri ::us/uri)
|
||||
|
||||
(s/def ::render-params
|
||||
(s/def ::params
|
||||
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id]
|
||||
:opt-un [::filename]))
|
||||
:opt-un [::origin ::uri]))
|
||||
|
||||
(defn render
|
||||
[params]
|
||||
(us/assert ::render-params params)
|
||||
(p/let [content (screenshot-object 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")}))
|
||||
(us/verify ::params params)
|
||||
(when (and (:origin params)
|
||||
(not (contains? (cf/get :origin-white-list) (:origin params))))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-origin
|
||||
:hint "invalid origin"
|
||||
:origin (:origin params)))
|
||||
|
||||
(p/let [content (screenshot-object params)]
|
||||
{:data content
|
||||
:name (str (:name params)
|
||||
(:suffix params "")
|
||||
(case (:type params)
|
||||
:png ".png"
|
||||
:jpeg ".jpg"))
|
||||
:size (alength content)
|
||||
:mtype (case (:type params)
|
||||
:png "image/png"
|
||||
:jpeg "image/jpeg")}))
|
||||
|
||||
|
|
|
@ -13,46 +13,33 @@
|
|||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[cljs.spec.alpha :as s]
|
||||
[lambdaisland.uri :as u]
|
||||
[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 pdf-from-object
|
||||
[{:keys [file-id page-id object-id token scale type save-path]}]
|
||||
(letfn [(handle [page]
|
||||
(let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(assoc :path "/")
|
||||
(assoc :query "essential=t")
|
||||
(assoc :fragment path))
|
||||
|
||||
cookie (create-cookie uri token)]
|
||||
(pdf-from page (str uri) cookie)))
|
||||
|
||||
(pdf-from [page uri cookie]
|
||||
(l/info :uri uri)
|
||||
(p/let [options {:cookie cookie}]
|
||||
(bw/configure-page! page options)
|
||||
(bw/navigate! page uri)
|
||||
(bw/wait-for page "#screenshot")
|
||||
;; taking png screenshot before pdf, helps to make the
|
||||
;; pdf rendering works as expected.
|
||||
(p/let [dom (bw/select page "#screenshot")]
|
||||
(bw/screenshot dom {:full-page? true}))
|
||||
|
||||
(if save-path
|
||||
(bw/pdf page {:save-path save-path})
|
||||
(bw/pdf page))))]
|
||||
|
||||
(bw/exec! handle)))
|
||||
[{:keys [file-id page-id object-id token scale type save-path uri] :as params}]
|
||||
(p/let [path (str "/render-object/" file-id "/" page-id "/" object-id)
|
||||
uri (-> (or uri (cf/get :public-uri))
|
||||
(assoc :path "/")
|
||||
(assoc :fragment path))]
|
||||
(bw/exec!
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:viewport #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:locale "en-US"
|
||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||
:deviceScaleFactor scale
|
||||
:userAgent bw/default-user-agent}
|
||||
(fn [page]
|
||||
(l/info :uri uri)
|
||||
(p/do!
|
||||
(bw/nav! page uri)
|
||||
(p/let [dom (bw/select page "#screenshot")]
|
||||
(bw/wait-for dom)
|
||||
(bw/screenshot dom {:full-page? true})
|
||||
(if save-path
|
||||
(bw/pdf page {:save-path save-path})
|
||||
(bw/pdf page))))))))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
|
@ -61,22 +48,21 @@
|
|||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::scale ::us/number)
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::filename ::us/string)
|
||||
(s/def ::save-path ::us/string)
|
||||
(s/def ::uri ::us/uri)
|
||||
|
||||
(s/def ::render-params
|
||||
(s/keys :req-un [::name ::suffix ::object-id ::page-id ::scale ::token ::file-id]
|
||||
:opt-un [::filename ::save-path]))
|
||||
:opt-un [::save-path ::uri]))
|
||||
|
||||
(defn render
|
||||
[params]
|
||||
(us/assert ::render-params params)
|
||||
(p/let [content (pdf-from-object params)]
|
||||
{:content content
|
||||
:filename (or (:filename params)
|
||||
(str (:name params)
|
||||
(:suffix params "")
|
||||
".pdf"))
|
||||
:length (alength content)
|
||||
:mime-type "application/pdf"}))
|
||||
{:data content
|
||||
:name (str (:name params)
|
||||
(:suffix params "")
|
||||
".pdf")
|
||||
:size (alength content)
|
||||
:mtype "application/pdf"}))
|
||||
|
||||
|
|
|
@ -15,12 +15,10 @@
|
|||
[app.common.pages :as cp]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.renderer.bitmap :refer [create-cookie]]
|
||||
[app.util.shell :as sh]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.walk :as walk]
|
||||
[cuerdas.core :as str]
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(l/set-level! :trace)
|
||||
|
@ -114,7 +112,7 @@
|
|||
|
||||
|
||||
(defn- render-object
|
||||
[{:keys [page-id file-id object-id token scale suffix type]}]
|
||||
[{:keys [page-id file-id object-id token scale suffix type uri]}]
|
||||
(letfn [(convert-to-ppm [pngpath]
|
||||
(l/trace :fn :convert-to-ppm)
|
||||
(let [basepath (path/dirname pngpath)
|
||||
|
@ -306,7 +304,7 @@
|
|||
(-> (bw/select-all page "#screenshot foreignObject")
|
||||
(p/then (fn [nodes] (p/all (map (partial process-text-node page) nodes))))))
|
||||
|
||||
(extract-svg [page]
|
||||
(extract [page]
|
||||
(p/let [dom (bw/select page "#screenshot")
|
||||
xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem)))
|
||||
nodes (process-text-nodes page)
|
||||
|
@ -322,35 +320,32 @@
|
|||
;; (cljs.pprint/pprint (xml->clj result))
|
||||
;; (println "-------")
|
||||
result))
|
||||
]
|
||||
|
||||
(render-in-page [page {:keys [uri cookie] :as rctx}]
|
||||
(let [viewport {:width 1920
|
||||
:height 1080
|
||||
:scale 4}
|
||||
options {:viewport viewport
|
||||
:timeout 15000
|
||||
:cookie cookie}]
|
||||
(p/do!
|
||||
(bw/configure-page! page options)
|
||||
(bw/navigate! page uri)
|
||||
(bw/wait-for page "#screenshot")
|
||||
(bw/sleep page 2000)
|
||||
;; (bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
||||
page)))
|
||||
|
||||
(handle [rctx page]
|
||||
(p/let [page (render-in-page page rctx)]
|
||||
(extract-svg page)))]
|
||||
|
||||
(let [path (str "/render-object/" file-id "/" page-id "/" object-id "?render-texts=true")
|
||||
uri (-> (u/uri (cf/get :public-uri))
|
||||
(p/let [path (str "/render-object/" file-id "/" page-id "/" object-id "?render-texts=true")
|
||||
uri (-> (or uri (cf/get :public-uri))
|
||||
(assoc :path "/")
|
||||
(assoc :fragment path))
|
||||
cookie (create-cookie uri token)
|
||||
rctx {:cookie cookie
|
||||
:uri (str uri)}]
|
||||
(l/info :uri (:uri rctx))
|
||||
(bw/exec! (partial handle rctx)))))
|
||||
(assoc :fragment path))]
|
||||
|
||||
(bw/exec!
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:viewport #js {:width bw/default-viewport-width
|
||||
:height bw/default-viewport-height}
|
||||
:locale "en-US"
|
||||
:storageState #js {:cookies (bw/create-cookies uri {:token token})}
|
||||
:deviceScaleFactor scale
|
||||
:userAgent bw/default-user-agent}
|
||||
(fn [page]
|
||||
(l/info :uri uri)
|
||||
(p/do!
|
||||
(bw/nav! page uri)
|
||||
(p/let [dom (bw/select page "#screenshot")]
|
||||
(js/console.log "FFFF" dom)
|
||||
(bw/wait-for dom)
|
||||
(bw/sleep page 2000))
|
||||
|
||||
(extract page)))))))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::suffix ::us/string)
|
||||
|
@ -360,21 +355,20 @@
|
|||
(s/def ::object-id ::us/uuid)
|
||||
(s/def ::scale ::us/number)
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::filename ::us/string)
|
||||
(s/def ::uri ::us/uri)
|
||||
|
||||
(s/def ::render-params
|
||||
(s/def ::params
|
||||
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token]
|
||||
:opt-un [::filename]))
|
||||
:opt-un [::uri]))
|
||||
|
||||
(defn render
|
||||
[params]
|
||||
(us/assert ::render-params params)
|
||||
(us/assert ::params params)
|
||||
(p/let [content (render-object params)]
|
||||
{:content content
|
||||
:filename (or (:filename params)
|
||||
(str (:name params)
|
||||
(:suffix params "")
|
||||
".svg"))
|
||||
:length (alength content)
|
||||
:mime-type "image/svg+xml"}))
|
||||
{:data content
|
||||
:name (str (:name params)
|
||||
(:suffix params "")
|
||||
".svg")
|
||||
:size (alength content)
|
||||
:mtype "image/svg+xml"}))
|
||||
|
||||
|
|
|
@ -1,44 +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.sentry
|
||||
(:require
|
||||
["@sentry/node" :as sentry]
|
||||
["@sentry/tracing" :as sentry-t]
|
||||
[app.common.data :as d]
|
||||
[app.config :as cf]))
|
||||
|
||||
(defn init!
|
||||
[]
|
||||
(when-let [dsn (cf/get :sentry-dsn)]
|
||||
(sentry/init
|
||||
#js {:dsn dsn
|
||||
:release (str "frontend@" (:base @cf/version))
|
||||
:serverName (cf/get :host)
|
||||
:environment (cf/get :tenant)
|
||||
:autoSessionTracking false
|
||||
:attachStacktrace false
|
||||
:maxBreadcrumbs 20
|
||||
:tracesSampleRate 1.0})))
|
||||
|
||||
(def parse-request (unchecked-get sentry/Handlers "parseRequest"))
|
||||
|
||||
(defn capture-exception
|
||||
[error {:keys [::request ::tags] :as context}]
|
||||
(let [context (-> (dissoc context ::request ::tags)
|
||||
(d/without-nils))]
|
||||
(sentry/withScope
|
||||
(fn [scope]
|
||||
(.addEventProcessor ^js scope (fn [event]
|
||||
(let [node-request (:internal-request request)]
|
||||
(parse-request event node-request))))
|
||||
(doseq [[k v] tags]
|
||||
(.setTag ^js scope (if (keyword? k) (name k) (str k)) (str v)))
|
||||
|
||||
(doseq [[k v] context]
|
||||
(.setContext ^js scope (if (keyword? k) (name k) (str k)) (clj->js v)))
|
||||
|
||||
(sentry/captureException error)))))
|
|
@ -7,7 +7,7 @@
|
|||
(ns app.util.shell
|
||||
"Shell & FS utilities."
|
||||
(:require
|
||||
["child_process" :as chp]
|
||||
["child_process" :as proc]
|
||||
["fs" :as fs]
|
||||
["os" :as os]
|
||||
["path" :as path]
|
||||
|
@ -18,51 +18,44 @@
|
|||
|
||||
(defn create-tmpdir!
|
||||
[prefix]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(fs/mkdtemp (path/join (os/tmpdir) prefix)
|
||||
(fn [err dir]
|
||||
(if err
|
||||
(reject err)
|
||||
(resolve dir)))))))
|
||||
(-> (.mkdtemp fs/promises prefix)
|
||||
(p/then (fn [result]
|
||||
(path/join (os/tmpdir) result)))))
|
||||
|
||||
|
||||
(defn move!
|
||||
[origin-path dest-path]
|
||||
(.rename fs/promises origin-path dest-path))
|
||||
|
||||
(defn stat
|
||||
[path]
|
||||
(-> (.stat fs/promises path)
|
||||
(p/then (fn [data]
|
||||
{:created-at (inst-ms (.-ctime ^js data))
|
||||
:size (.-size data)}))
|
||||
(p/catch (constantly nil))))
|
||||
|
||||
(defn rmdir!
|
||||
[path]
|
||||
(.rm fs/promises path #js {:recursive true}))
|
||||
|
||||
(defn write-file!
|
||||
[fpath content]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(fs/writeFile fpath content (fn [err]
|
||||
(if err
|
||||
(reject err)
|
||||
(resolve nil)))))))
|
||||
(.writeFile fs/promises fpath content))
|
||||
|
||||
(defn read-file
|
||||
[fpath]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(fs/readFile fpath (fn [err content]
|
||||
(if err
|
||||
(reject err)
|
||||
(resolve content)))))))
|
||||
(.readFile fs/promises fpath))
|
||||
|
||||
(defn run-cmd!
|
||||
[cmd]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(l/trace :fn :run-cmd :cmd cmd)
|
||||
(chp/exec cmd #js {:encoding "buffer"}
|
||||
(fn [error stdout stderr]
|
||||
;; (l/trace :fn :run-cmd :stdout stdout)
|
||||
(if error
|
||||
(reject error)
|
||||
(resolve stdout)))))))
|
||||
|
||||
(defn rmdir!
|
||||
[path]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(fs/rmdir path #js {:recursive true}
|
||||
(fn [err]
|
||||
(if err
|
||||
(reject err)
|
||||
(resolve nil)))))))
|
||||
|
||||
(proc/exec cmd #js {:encoding "buffer"}
|
||||
(fn [error stdout stderr]
|
||||
;; (l/trace :fn :run-cmd :stdout stdout)
|
||||
(if error
|
||||
(reject error)
|
||||
(resolve stdout)))))))
|
||||
|
||||
|
|
|
@ -1,19 +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.zipfile
|
||||
(:require
|
||||
["jszip" :as jszip]))
|
||||
|
||||
(defn create
|
||||
[]
|
||||
(new jszip))
|
||||
|
||||
(defn add!
|
||||
[zfile name data]
|
||||
(.file ^js zfile name data)
|
||||
zfile)
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue