🎉 Add initial exporter (nodejs) application.

This commit is contained in:
Andrey Antukh 2020-06-29 16:07:48 +02:00 committed by Hirunatan
parent d521416329
commit c2db6d4f35
10 changed files with 1048 additions and 0 deletions

View file

@ -0,0 +1,66 @@
(ns app.browser
(:require
[lambdaisland.glogi :as log]
[promesa.core :as p]
["puppeteer-cluster" :as ppc]))
(def 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"))
(defn exec!
[browser f]
(.execute ^js browser (fn [props]
(let [page (unchecked-get props "page")]
(f page)))))
(defn emulate!
[page {:keys [viewport user-agent]
:or {user-agent USER-AGENT}}]
(let [[width height] viewport]
(.emulate page #js {:viewport #js {:width width
:height height}
:userAgent user-agent})))
(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 sleep
[page ms]
(.waitFor ^js page ms))
(defn screenshot
([page] (screenshot page nil))
([page {:keys [full-page?]
:or {full-page? true}}]
(.screenshot ^js page #js {:fullPage full-page? :omitBackground true})))
(defn set-cookie!
[page {:keys [key value domain]}]
(.setCookie ^js page #js {:name key
:value value
:domain domain}))
(defn start!
([] (start! nil))
([{:keys [concurrency concurrency-strategy]
:or {concurrency 2
concurrency-strategy :browser}}]
(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}]
(.launch ^js ppc/Cluster opts))))
(defn stop!
[instance]
(p/do!
(.idle ^js instance)
(.close ^js instance)
(log/info :msg "shutdown headless browser")
nil))

View file

@ -0,0 +1,20 @@
(ns app.config
(:require
["process" :as process]
[cljs.pprint]
[cuerdas.core :as str]))
(defn- keywordize
[s]
(-> (str/kebab s)
(str/keyword)))
(defonce env
(let [env (unchecked-get process "env")]
(persistent!
(reduce #(assoc! %1 (keywordize %2) (unchecked-get env %2))
(transient {})
(js/Object.keys env)))))
(defonce config
{:domain (:app-domain env "localhost:3449")})

View file

@ -0,0 +1,36 @@
(ns app.core
(:require
[lambdaisland.glogi :as log]
[lambdaisland.glogi.console :as glogi-console]
[promesa.core :as p]
[app.http :as http]
[app.config]
[app.browser :as bwr]))
(glogi-console/install!)
(enable-console-print!)
(defonce state (atom nil))
(defn start
[& args]
(log/info :msg "initializing")
(p/let [browser (bwr/start!)
server (http/start! {:browser browser})]
(reset! state {:http server
:browser browser})))
(def main start)
(defn stop
[done]
;; an empty line for visual feedback of restart
(js/console.log "")
(log/info :msg "stoping")
(p/do!
(when-let [instance (:browser @state)]
(bwr/stop! instance))
(when-let [instance (:http @state)]
(http/stop! instance))
(done)))

View file

@ -0,0 +1,83 @@
(ns app.http
(:require
[promesa.core :as p]
[lambdaisland.glogi :as log]
[app.browser :as bwr]
[app.http.screenshot :refer [bitmap-handler]]
[reitit.core :as r]
["koa" :as koa]
["http" :as http])
(:import
goog.Uri))
(defn query-params
"Given goog.Uri, read query parameters into Clojure map."
[^goog.Uri uri]
(let [q (.getQueryData uri)]
(->> q
(.getKeys)
(map (juxt keyword #(.get q %)))
(into {}))))
(defn- match
[router ctx]
(let [uri (.parse Uri (unchecked-get ctx "originalUrl"))]
(when-let [match (r/match-by-path router (.getPath uri))]
(let [qparams (query-params uri)
params {:path (:path-params match) :query qparams}]
(assoc match
:params params
:query-params qparams)))))
(defn- handle-response
[ctx {:keys [body headers status] :or {headers {} status 200}}]
(run! (fn [[k v]] (.set ^js ctx k v)) headers)
(set! (.-body ^js ctx) body)
(set! (.-status ^js ctx) status)
nil)
(defn- wrap-handler
[f extra]
(fn [ctx]
(let [cookies (unchecked-get ctx "cookies")
request (assoc extra :ctx ctx :cookies cookies)]
(-> (p/do! (f request))
(p/then (fn [rsp]
(when (map? rsp)
(handle-response ctx rsp))))))))
(def routes
[["/export"
["/bitmap" {:handler bitmap-handler}]]])
(defn- router-handler
[router]
(fn [{:keys [ctx] :as req}]
(let [route (match router ctx)
request (assoc req
:route route
:params (:params route))
handler (get-in route [:data :handler])]
(if (and route handler)
(handler request)
{:status 404
:body "Not found"}))))
(defn start!
[extra]
(log/info :msg "starting http server" :port 6061)
(let [router (r/router routes)
instance (doto (new koa)
(.use (-> (router-handler router)
(wrap-handler extra))))
server (.createServer http (.callback instance))]
(.listen server 6061)
(p/resolved server)))
(defn stop!
[server]
(p/create (fn [resolve]
(.close server (fn []
(log/info :msg "shutdown http server")
(resolve))))))

View file

@ -0,0 +1,43 @@
(ns app.http.screenshot
(:require
[app.browser :as bwr]
[app.config :as cfg]
[promesa.core :as p]))
(defn- load-and-screenshot
[page url cookie]
(p/do!
(bwr/emulate! page {:viewport [1920 1080]})
(bwr/set-cookie! page cookie)
(bwr/navigate! page url)
(bwr/sleep page 500)
(.evaluate page (js* "() => document.body.style.background = 'transparent'"))
(p/let [dom (.$ page "#screenshot")]
(.screenshot ^js dom #js {:omitBackground true}))))
(defn- take-screenshot
[browser {:keys [page-id object-id token]}]
(letfn [(on-browser [page]
(let [url (str "http://" (:domain cfg/config)
"/#/render-object/"
page-id "/" object-id)
cookie {:domain (:domain cfg/config)
:key "auth-token"
:value token}]
(load-and-screenshot page url cookie)))]
(bwr/exec! browser on-browser)))
(defn bitmap-handler
[{:keys [params browser cookies] :as request}]
(let [page-id (get-in params [:query :page-id])
object-id (get-in params [:query :object-id])
token (.get ^js cookies "auth-token")]
(-> (take-screenshot browser {:page-id page-id
:object-id object-id
:token token})
(p/then (fn [result]
{:status 200
:body result
:headers {"content-type" "image/png"
"content-length" (alength result)}})))))