mirror of
https://github.com/penpot/penpot.git
synced 2026-09-09 13:39:02 +00:00
Merge pull request #3001 from penpot/niwinz-experiments-2
♻️ Refactor concurrency model (start using JDK19 virtual threads on RPC and WebSockets)
This commit is contained in:
commit
afb09919ed
@ -2,6 +2,7 @@
|
|||||||
{promesa.core/let clojure.core/let
|
{promesa.core/let clojure.core/let
|
||||||
promesa.core/->> clojure.core/->>
|
promesa.core/->> clojure.core/->>
|
||||||
promesa.core/-> clojure.core/->
|
promesa.core/-> clojure.core/->
|
||||||
|
promesa.exec.csp/go-loop clojure.core/loop
|
||||||
rumext.v2/defc clojure.core/defn
|
rumext.v2/defc clojure.core/defn
|
||||||
rumext.v2/fnc clojure.core/fn
|
rumext.v2/fnc clojure.core/fn
|
||||||
app.common.data/export clojure.core/def
|
app.common.data/export clojure.core/def
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
{:deps
|
{:mvn/repos
|
||||||
|
{"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
|
||||||
|
|
||||||
|
:deps
|
||||||
{penpot/common {:local/root "../common"}
|
{penpot/common {:local/root "../common"}
|
||||||
org.clojure/clojure {:mvn/version "1.11.1"}
|
org.clojure/clojure {:mvn/version "1.11.1"}
|
||||||
org.clojure/core.async {:mvn/version "1.6.673"}
|
org.clojure/core.async {:mvn/version "1.6.673"}
|
||||||
@ -19,14 +22,16 @@
|
|||||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||||
|
|
||||||
funcool/yetti
|
funcool/yetti
|
||||||
{:git/tag "v9.12"
|
{:git/tag "v9.15"
|
||||||
:git/sha "51646d8"
|
:git/sha "aa9b967"
|
||||||
:git/url "https://github.com/funcool/yetti.git"
|
:git/url "https://github.com/funcool/yetti.git"
|
||||||
:exclusions [org.slf4j/slf4j-api]}
|
:exclusions [org.slf4j/slf4j-api]}
|
||||||
|
|
||||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
|
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
|
||||||
metosin/reitit-core {:mvn/version "0.5.18"}
|
metosin/reitit-core {:mvn/version "0.5.18"}
|
||||||
org.postgresql/postgresql {:mvn/version "42.5.2"}
|
|
||||||
|
org.postgresql/postgresql {:mvn/version "42.6.0-SNAPSHOT"}
|
||||||
|
|
||||||
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
|
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
|
||||||
|
|
||||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||||
@ -34,7 +39,7 @@
|
|||||||
buddy/buddy-hashers {:mvn/version "1.8.158"}
|
buddy/buddy-hashers {:mvn/version "1.8.158"}
|
||||||
buddy/buddy-sign {:mvn/version "3.4.333"}
|
buddy/buddy-sign {:mvn/version "3.4.333"}
|
||||||
|
|
||||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.2"}
|
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.5"}
|
||||||
|
|
||||||
org.jsoup/jsoup {:mvn/version "1.15.3"}
|
org.jsoup/jsoup {:mvn/version "1.15.3"}
|
||||||
org.im4java/im4java
|
org.im4java/im4java
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
;; Example climit.edn file
|
;; Example climit.edn file
|
||||||
;; Required: concurrency
|
;; Required: permits
|
||||||
;; Optional: queue-size, ommited means Integer/MAX_VALUE
|
;; Optional: queue, ommited means Integer/MAX_VALUE
|
||||||
{:update-file {:concurrency 1 :queue-size 3}
|
;; Optional: timeout, ommited means no timeout
|
||||||
:auth {:concurrency 128}
|
;; Note: queue and timeout are excluding
|
||||||
:process-font {:concurrency 4 :queue-size 32}
|
{:update-file-by-id {:permits 1 :queue 3}
|
||||||
:process-image {:concurrency 8 :queue-size 32}
|
:update-file {:permits 20}
|
||||||
:push-audit-events
|
|
||||||
{:concurrency 1 :queue-size 3}}
|
:derive-password {:permits 8}
|
||||||
|
:process-font {:permits 4 :queue 32}
|
||||||
|
:process-image {:permits 8 :queue 32}
|
||||||
|
|
||||||
|
:submit-audit-events-by-profile
|
||||||
|
{:permits 1 :queue 3}}
|
||||||
|
|||||||
@ -3,12 +3,12 @@
|
|||||||
<Appenders>
|
<Appenders>
|
||||||
<Console name="console" target="SYSTEM_OUT">
|
<Console name="console" target="SYSTEM_OUT">
|
||||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
||||||
alwaysWriteExceptions="false" />
|
alwaysWriteExceptions="true" />
|
||||||
</Console>
|
</Console>
|
||||||
|
|
||||||
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">
|
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">
|
||||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
||||||
alwaysWriteExceptions="false" />
|
alwaysWriteExceptions="true" />
|
||||||
<Policies>
|
<Policies>
|
||||||
<SizeBasedTriggeringPolicy size="50M"/>
|
<SizeBasedTriggeringPolicy size="50M"/>
|
||||||
</Policies>
|
</Policies>
|
||||||
|
|||||||
@ -3,8 +3,9 @@
|
|||||||
{:default
|
{:default
|
||||||
[[:default :window "200000/h"]]
|
[[:default :window "200000/h"]]
|
||||||
|
|
||||||
#{:command/get-teams}
|
;; #{:command/get-teams}
|
||||||
[[:burst :bucket "5/1/5s"]]
|
;; [[:burst :bucket "5/5/5s"]]
|
||||||
|
|
||||||
#{:command/get-profile}
|
;; #{:command/get-profile}
|
||||||
[[:burst :bucket "60/60/1m"]]}
|
;; [[:burst :bucket "60/60/1m"]]
|
||||||
|
}
|
||||||
|
|||||||
@ -42,19 +42,40 @@ export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
|
|||||||
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
|
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
|
||||||
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
|
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
|
||||||
|
|
||||||
|
#-J-Djdk.virtualThreadScheduler.parallelism=16
|
||||||
|
|
||||||
|
|
||||||
export OPTIONS="
|
export OPTIONS="
|
||||||
-A:jmx-remote -A:dev \
|
-A:jmx-remote -A:dev \
|
||||||
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||||
-J-Djdk.attach.allowAttachSelf \
|
-J-Djdk.attach.allowAttachSelf \
|
||||||
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
||||||
-J-Xms50m \
|
|
||||||
-J-Xmx1024m \
|
|
||||||
-J-XX:+UseZGC \
|
|
||||||
-J-XX:-OmitStackTraceInFastThrow \
|
-J-XX:-OmitStackTraceInFastThrow \
|
||||||
-J-XX:+UnlockDiagnosticVMOptions \
|
-J-XX:+UnlockDiagnosticVMOptions \
|
||||||
-J-XX:+DebugNonSafepoints";
|
-J-XX:+DebugNonSafepoints \
|
||||||
|
-J-Djdk.tracePinnedThreads=full \
|
||||||
|
-J--enable-preview";
|
||||||
|
|
||||||
# Uncomment for use the ImageMagick v7.x
|
# Setup HEAP
|
||||||
|
export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m"
|
||||||
|
# export OPTIONS="$OPTIONS -J-Xms1100m -J-Xmx1100m -J-XX:+AlwaysPreTouch"
|
||||||
|
|
||||||
|
# Increase virtual thread pool size
|
||||||
|
# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16"
|
||||||
|
|
||||||
|
# Disable C2 Compiler
|
||||||
|
# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1"
|
||||||
|
|
||||||
|
# Disable all compilers
|
||||||
|
# export OPTIONS="$OPTIONS -J-Xint"
|
||||||
|
|
||||||
|
# Setup GC
|
||||||
|
export OPTIONS="$OPTIONS -J-XX:+UseG1GC"
|
||||||
|
|
||||||
|
# Setup GC
|
||||||
|
# export OPTIONS="$OPTIONS -J-XX:+UseZGC"
|
||||||
|
|
||||||
|
# Enable ImageMagick v7.x support
|
||||||
# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS";
|
# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS";
|
||||||
|
|
||||||
export OPTIONS_EVAL="nil"
|
export OPTIONS_EVAL="nil"
|
||||||
|
|||||||
@ -6,15 +6,18 @@
|
|||||||
|
|
||||||
(ns app.auth
|
(ns app.auth
|
||||||
(:require
|
(:require
|
||||||
[buddy.hashers :as hashers]))
|
[buddy.hashers :as hashers]
|
||||||
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
|
(def default-params
|
||||||
|
{:alg :argon2id
|
||||||
|
:memory (* 32768 2)
|
||||||
|
:iterations 5
|
||||||
|
:parallelism (px/get-available-processors)})
|
||||||
|
|
||||||
(defn derive-password
|
(defn derive-password
|
||||||
[password]
|
[password]
|
||||||
(hashers/derive password
|
(hashers/derive password default-params))
|
||||||
{:alg :argon2id
|
|
||||||
:memory 16384
|
|
||||||
:iterations 20
|
|
||||||
:parallelism 2}))
|
|
||||||
|
|
||||||
(defn verify-password
|
(defn verify-password
|
||||||
[attempt password]
|
[attempt password]
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.client :as http]
|
[app.http.client :as http]
|
||||||
[app.http.middleware :as hmw]
|
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
@ -25,14 +24,11 @@
|
|||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.json :as json]
|
[app.util.json :as json]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
[yetti.response :as-alias yrs]))
|
||||||
[promesa.exec :as px]
|
|
||||||
[yetti.response :as yrs]))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; HELPERS
|
;; HELPERS
|
||||||
@ -166,20 +162,22 @@
|
|||||||
|
|
||||||
(defn- retrieve-github-email
|
(defn- retrieve-github-email
|
||||||
[cfg tdata info]
|
[cfg tdata info]
|
||||||
(or (some-> info :email p/resolved)
|
(or (some-> info :email)
|
||||||
(->> (http/req! cfg
|
(let [params {:uri "https://api.github.com/user/emails"
|
||||||
{:uri "https://api.github.com/user/emails"
|
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
|
||||||
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
|
:timeout 6000
|
||||||
:timeout 6000
|
:method :get}
|
||||||
:method :get})
|
|
||||||
(p/map (fn [{:keys [status body] :as response}]
|
{:keys [status body]} (http/req! cfg params {:sync? true})]
|
||||||
(when-not (s/int-in-range? 200 300 status)
|
|
||||||
(ex/raise :type :internal
|
(when-not (s/int-in-range? 200 300 status)
|
||||||
:code :unable-to-retrieve-github-emails
|
(ex/raise :type :internal
|
||||||
:hint "unable to retrieve github emails"
|
:code :unable-to-retrieve-github-emails
|
||||||
:http-status status
|
:hint "unable to retrieve github emails"
|
||||||
:http-body body))
|
:http-status status
|
||||||
(->> response :body json/decode (filter :primary) first :email))))))
|
:http-body body))
|
||||||
|
|
||||||
|
(->> body json/decode (filter :primary) first :email))))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::providers/github [_]
|
(defmethod ig/pre-init-spec ::providers/github [_]
|
||||||
(s/keys :req [::http/client]))
|
(s/keys :req [::http/client]))
|
||||||
@ -290,80 +288,74 @@
|
|||||||
:grant-type (:grant_type params)
|
:grant-type (:grant_type params)
|
||||||
:redirect-uri (:redirect_uri params))
|
:redirect-uri (:redirect_uri params))
|
||||||
|
|
||||||
(->> (http/req! cfg req)
|
(let [{:keys [status body]} (http/req! cfg req {:sync? true})]
|
||||||
(p/map (fn [{:keys [status body] :as res}]
|
(l/trace :hint "access token response" :status status :body body)
|
||||||
(l/trace :hint "access token response"
|
(if (= status 200)
|
||||||
:status status
|
(let [data (json/decode body)]
|
||||||
:body body)
|
{:token (get data :access_token)
|
||||||
(if (= status 200)
|
:type (get data :token_type)})
|
||||||
(let [data (json/decode body)]
|
|
||||||
{:token (get data :access_token)
|
(ex/raise :type :internal
|
||||||
:type (get data :token_type)})
|
:code :unable-to-retrieve-token
|
||||||
(ex/raise :type :internal
|
:hint "unable to retrieve token"
|
||||||
:code :unable-to-retrieve-token
|
:http-status status
|
||||||
:http-status status
|
:http-body body)))))
|
||||||
:http-body body)))))))
|
|
||||||
|
|
||||||
(defn- retrieve-user-info
|
(defn- retrieve-user-info
|
||||||
[{:keys [provider] :as cfg} tdata]
|
[{:keys [provider] :as cfg} tdata]
|
||||||
(letfn [(retrieve []
|
(letfn [(get-email [info]
|
||||||
(l/trace :hint "request user info"
|
|
||||||
:uri (:user-uri provider)
|
|
||||||
:token (obfuscate-string (:token tdata))
|
|
||||||
:token-type (:type tdata))
|
|
||||||
(http/req! cfg
|
|
||||||
{:uri (:user-uri provider)
|
|
||||||
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
|
|
||||||
:timeout 6000
|
|
||||||
:method :get}))
|
|
||||||
(validate-response [response]
|
|
||||||
(l/trace :hint "user info response"
|
|
||||||
:status (:status response)
|
|
||||||
:body (:body response))
|
|
||||||
(when-not (s/int-in-range? 200 300 (:status response))
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :unable-to-retrieve-user-info
|
|
||||||
:hint "unable to retrieve user info"
|
|
||||||
:http-status (:status response)
|
|
||||||
:http-body (:body response)))
|
|
||||||
response)
|
|
||||||
|
|
||||||
(get-email [info]
|
|
||||||
;; Allow providers hook into this for custom email
|
;; Allow providers hook into this for custom email
|
||||||
;; retrieval method.
|
;; retrieval method.
|
||||||
(if-let [get-email-fn (:get-email-fn provider)]
|
(if-let [get-email-fn (:get-email-fn provider)]
|
||||||
(get-email-fn tdata info)
|
(get-email-fn tdata info)
|
||||||
(let [attr-kw (cf/get :oidc-email-attr :email)]
|
(let [attr-kw (cf/get :oidc-email-attr :email)]
|
||||||
(p/resolved (get info attr-kw)))))
|
(get info attr-kw))))
|
||||||
|
|
||||||
(get-name [info]
|
(get-name [info]
|
||||||
(let [attr-kw (cf/get :oidc-name-attr :name)]
|
(let [attr-kw (cf/get :oidc-name-attr :name)]
|
||||||
(get info attr-kw)))
|
(get info attr-kw)))
|
||||||
|
|
||||||
(process-response [response]
|
(process-response [response]
|
||||||
(p/let [info (-> response :body json/decode)
|
(let [info (-> response :body json/decode)
|
||||||
email (get-email info)]
|
email (get-email info)]
|
||||||
{:backend (:name provider)
|
{:backend (:name provider)
|
||||||
:email email
|
:email email
|
||||||
:fullname (or (get-name info) email)
|
:fullname (or (get-name info) email)
|
||||||
:props (->> (dissoc info :name :email)
|
:props (->> (dissoc info :name :email)
|
||||||
(qualify-props provider))}))
|
(qualify-props provider))}))]
|
||||||
|
|
||||||
(validate-info [info]
|
(l/trace :hint "request user info"
|
||||||
(l/trace :hint "authentication info" :info info)
|
:uri (:user-uri provider)
|
||||||
(when-not (s/valid? ::info info)
|
:token (obfuscate-string (:token tdata))
|
||||||
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
|
:token-type (:type tdata))
|
||||||
:info (pr-str info))
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :incomplete-user-info
|
|
||||||
:hint "inconmplete user info"
|
|
||||||
:info info))
|
|
||||||
info)]
|
|
||||||
|
|
||||||
(->> (retrieve)
|
(let [request {:uri (:user-uri provider)
|
||||||
(p/fmap validate-response)
|
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
|
||||||
(p/mcat process-response)
|
:timeout 6000
|
||||||
(p/fmap validate-info))))
|
:method :get}
|
||||||
|
response (http/req! cfg request {:sync? true})]
|
||||||
|
|
||||||
|
(l/trace :hint "user info response"
|
||||||
|
:status (:status response)
|
||||||
|
:body (:body response))
|
||||||
|
|
||||||
|
(when-not (s/int-in-range? 200 300 (:status response))
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :unable-to-retrieve-user-info
|
||||||
|
:hint "unable to retrieve user info"
|
||||||
|
:http-status (:status response)
|
||||||
|
:http-body (:body response)))
|
||||||
|
|
||||||
|
(let [info (process-response response)]
|
||||||
|
(l/trace :hint "authentication info" :info info)
|
||||||
|
|
||||||
|
(when-not (s/valid? ::info info)
|
||||||
|
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :incomplete-user-info
|
||||||
|
:hint "inconmplete user info"
|
||||||
|
:info info))
|
||||||
|
info))))
|
||||||
|
|
||||||
(s/def ::backend ::us/not-empty-string)
|
(s/def ::backend ::us/not-empty-string)
|
||||||
(s/def ::email ::us/not-empty-string)
|
(s/def ::email ::us/not-empty-string)
|
||||||
@ -377,61 +369,55 @@
|
|||||||
|
|
||||||
(defn get-info
|
(defn get-info
|
||||||
[{:keys [provider] :as cfg} {:keys [params] :as request}]
|
[{:keys [provider] :as cfg} {:keys [params] :as request}]
|
||||||
(letfn [(validate-oidc [info]
|
(when-let [error (get params :error)]
|
||||||
;; If the provider is OIDC, we can proceed to check
|
(ex/raise :type :internal
|
||||||
;; roles if they are defined.
|
:code :error-on-retrieving-code
|
||||||
(when (and (= "oidc" (:name provider))
|
:error-id error
|
||||||
(seq (:roles provider)))
|
:error-desc (get params :error_description)))
|
||||||
(let [provider-roles (into #{} (:roles provider))
|
|
||||||
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
|
|
||||||
roles (get info attr)]
|
|
||||||
(cond
|
|
||||||
(string? roles) (into #{} (str/words roles))
|
|
||||||
(vector? roles) (into #{} roles)
|
|
||||||
:else #{}))]
|
|
||||||
|
|
||||||
;; check if profile has a configured set of roles
|
(let [state (get params :state)
|
||||||
(when-not (set/subset? provider-roles profile-roles)
|
code (get params :code)
|
||||||
(ex/raise :type :internal
|
state (tokens/verify (::main/props cfg) {:token state :iss :oauth})
|
||||||
:code :unable-to-auth
|
token (retrieve-access-token cfg code)
|
||||||
:hint "not enough permissions"))))
|
info (retrieve-user-info cfg token)]
|
||||||
info)
|
|
||||||
|
|
||||||
(post-process [state info]
|
;; If the provider is OIDC, we can proceed to check
|
||||||
(cond-> info
|
;; roles if they are defined.
|
||||||
(some? (:invitation-token state))
|
(when (and (= "oidc" (:name provider))
|
||||||
(assoc :invitation-token (:invitation-token state))
|
(seq (:roles provider)))
|
||||||
|
(let [provider-roles (into #{} (:roles provider))
|
||||||
|
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
|
||||||
|
roles (get info attr)]
|
||||||
|
(cond
|
||||||
|
(string? roles) (into #{} (str/words roles))
|
||||||
|
(vector? roles) (into #{} roles)
|
||||||
|
:else #{}))]
|
||||||
|
|
||||||
;; If state token comes with props, merge them. The state token
|
;; check if profile has a configured set of roles
|
||||||
;; props can contain pm_ and utm_ prefixed query params.
|
(when-not (set/subset? provider-roles profile-roles)
|
||||||
(map? (:props state))
|
(ex/raise :type :internal
|
||||||
(update :props merge (:props state))))]
|
:code :unable-to-auth
|
||||||
|
:hint "not enough permissions"))))
|
||||||
|
|
||||||
(when-let [error (get params :error)]
|
(cond-> info
|
||||||
(ex/raise :type :internal
|
(some? (:invitation-token state))
|
||||||
:code :error-on-retrieving-code
|
(assoc :invitation-token (:invitation-token state))
|
||||||
:error-id error
|
|
||||||
:error-desc (get params :error_description)))
|
|
||||||
|
|
||||||
(let [state (get params :state)
|
;; If state token comes with props, merge them. The state token
|
||||||
code (get params :code)
|
;; props can contain pm_ and utm_ prefixed query params.
|
||||||
state (tokens/verify (::main/props cfg) {:token state :iss :oauth})]
|
(map? (:props state))
|
||||||
(-> (p/resolved code)
|
(update :props merge (:props state)))))
|
||||||
(p/then #(retrieve-access-token cfg %))
|
|
||||||
(p/then #(retrieve-user-info cfg %))
|
|
||||||
(p/then' validate-oidc)
|
|
||||||
(p/then' (partial post-process state))))))
|
|
||||||
|
|
||||||
(defn- get-profile
|
(defn- get-profile
|
||||||
[{:keys [::db/pool ::wrk/executor] :as cfg} info]
|
[{:keys [::db/pool] :as cfg} info]
|
||||||
(px/with-dispatch executor
|
(dm/with-open [conn (db/open pool)]
|
||||||
(with-open [conn (db/open pool)]
|
(some->> (:email info)
|
||||||
(some->> (:email info)
|
(profile/get-profile-by-email conn))))
|
||||||
(profile/get-profile-by-email conn)))))
|
|
||||||
|
|
||||||
(defn- redirect-response
|
(defn- redirect-response
|
||||||
[uri]
|
[uri]
|
||||||
(yrs/response :status 302 :headers {"location" (str uri)}))
|
{::yrs/status 302
|
||||||
|
::yrs/headers {"location" (str uri)}})
|
||||||
|
|
||||||
(defn- generate-error-redirect
|
(defn- generate-error-redirect
|
||||||
[_ error]
|
[_ error]
|
||||||
@ -458,11 +444,11 @@
|
|||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
:code :profile-blocked))
|
:code :profile-blocked))
|
||||||
|
|
||||||
(audit/submit! cfg {:type "command"
|
(audit/submit! cfg {::audit/type "command"
|
||||||
:name "login-with-password"
|
::audit/name "login-with-oidc"
|
||||||
:profile-id (:id profile)
|
::audit/profile-id (:id profile)
|
||||||
:ip-addr (audit/parse-client-ip request)
|
::audit/ip-addr (audit/parse-client-ip request)
|
||||||
:props (audit/profile->props profile)})
|
::audit/props (audit/profile->props profile)})
|
||||||
|
|
||||||
(->> (redirect-response uri)
|
(->> (redirect-response uri)
|
||||||
(sxf request)))
|
(sxf request)))
|
||||||
@ -478,6 +464,7 @@
|
|||||||
uri (-> (u/uri (cf/get :public-uri))
|
uri (-> (u/uri (cf/get :public-uri))
|
||||||
(assoc :path "/#/auth/register/validate")
|
(assoc :path "/#/auth/register/validate")
|
||||||
(assoc :query (u/map->query-string params)))]
|
(assoc :query (u/map->query-string params)))]
|
||||||
|
|
||||||
(redirect-response uri))))
|
(redirect-response uri))))
|
||||||
|
|
||||||
(defn- auth-handler
|
(defn- auth-handler
|
||||||
@ -489,27 +476,24 @@
|
|||||||
:props props
|
:props props
|
||||||
:exp (dt/in-future "4h")})
|
:exp (dt/in-future "4h")})
|
||||||
uri (build-auth-uri cfg state)]
|
uri (build-auth-uri cfg state)]
|
||||||
(yrs/response 200 {:redirect-uri uri})))
|
{::yrs/status 200
|
||||||
|
::yrs/body {:redirect-uri uri}}))
|
||||||
|
|
||||||
(defn- callback-handler
|
(defn- callback-handler
|
||||||
[cfg request]
|
[cfg request]
|
||||||
(letfn [(process-request []
|
(try
|
||||||
(p/let [info (get-info cfg request)
|
(let [info (get-info cfg request)
|
||||||
profile (get-profile cfg info)]
|
profile (get-profile cfg info)]
|
||||||
(generate-redirect cfg request info profile)))
|
(generate-redirect cfg request info profile))
|
||||||
|
(catch Throwable cause
|
||||||
(handle-error [cause]
|
(l/error :hint "error on oauth process" :cause cause)
|
||||||
(l/error :hint "error on oauth process" :cause cause)
|
(generate-error-redirect cfg cause))))
|
||||||
(generate-error-redirect cfg cause))]
|
|
||||||
|
|
||||||
(-> (process-request)
|
|
||||||
(p/catch handle-error))))
|
|
||||||
|
|
||||||
(def provider-lookup
|
(def provider-lookup
|
||||||
{:compile
|
{:compile
|
||||||
(fn [& _]
|
(fn [& _]
|
||||||
(fn [handler]
|
(fn [handler {:keys [::providers] :as cfg}]
|
||||||
(fn [{:keys [::providers] :as cfg} request]
|
(fn [request]
|
||||||
(let [provider (some-> request :path-params :provider keyword)]
|
(let [provider (some-> request :path-params :provider keyword)]
|
||||||
(if-let [provider (get providers provider)]
|
(if-let [provider (get providers provider)]
|
||||||
(handler (assoc cfg :provider provider) request)
|
(handler (assoc cfg :provider provider) request)
|
||||||
@ -553,18 +537,15 @@
|
|||||||
[_]
|
[_]
|
||||||
(s/keys :req [::session/manager
|
(s/keys :req [::session/manager
|
||||||
::http/client
|
::http/client
|
||||||
::wrk/executor
|
|
||||||
::main/props
|
::main/props
|
||||||
::db/pool
|
::db/pool
|
||||||
::providers]))
|
::providers]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ {:keys [::wrk/executor] :as cfg}]
|
[_ cfg]
|
||||||
(let [cfg (update cfg :provider d/without-nils)]
|
(let [cfg (update cfg :provider d/without-nils)]
|
||||||
["" {:middleware [[session/authz cfg]
|
["" {:middleware [[session/authz cfg]
|
||||||
[hmw/with-dispatch executor]
|
[provider-lookup cfg]]}
|
||||||
[hmw/with-config cfg]
|
|
||||||
[provider-lookup]]}
|
|
||||||
["/auth/oauth"
|
["/auth/oauth"
|
||||||
["/:provider"
|
["/:provider"
|
||||||
{:handler auth-handler
|
{:handler auth-handler
|
||||||
|
|||||||
@ -37,6 +37,7 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn- parse-address
|
(defn- parse-address
|
||||||
|
^"[Ljakarta.mail.internet.InternetAddress;"
|
||||||
[v]
|
[v]
|
||||||
(InternetAddress/parse ^String v))
|
(InternetAddress/parse ^String v))
|
||||||
|
|
||||||
@ -149,6 +150,7 @@
|
|||||||
"mail.smtp.connectiontimeout" timeout}))
|
"mail.smtp.connectiontimeout" timeout}))
|
||||||
|
|
||||||
(defn- create-smtp-session
|
(defn- create-smtp-session
|
||||||
|
^Session
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [props (opts->props cfg)]
|
(let [props (opts->props cfg)]
|
||||||
(Session/getInstance props)))
|
(Session/getInstance props)))
|
||||||
|
|||||||
@ -19,19 +19,21 @@
|
|||||||
[app.http.middleware :as mw]
|
[app.http.middleware :as mw]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.http.websocket :as-alias ws]
|
[app.http.websocket :as-alias ws]
|
||||||
|
[app.main :as-alias main]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.doc :as-alias rpc.doc]
|
[app.rpc.doc :as-alias rpc.doc]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
|
[promesa.exec :as px]
|
||||||
[reitit.core :as r]
|
[reitit.core :as r]
|
||||||
[reitit.middleware :as rr]
|
[reitit.middleware :as rr]
|
||||||
[yetti.adapter :as yt]
|
[yetti.adapter :as yt]
|
||||||
[yetti.request :as yrq]
|
[yetti.request :as yrq]
|
||||||
[yetti.response :as yrs]))
|
[yetti.response :as-alias yrs]))
|
||||||
|
|
||||||
(declare wrap-router)
|
(declare router-handler)
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; HTTP SERVER
|
;; HTTP SERVER
|
||||||
@ -71,13 +73,16 @@
|
|||||||
:http/host host
|
:http/host host
|
||||||
:http/max-body-size (::max-body-size cfg)
|
:http/max-body-size (::max-body-size cfg)
|
||||||
:http/max-multipart-body-size (::max-multipart-body-size cfg)
|
:http/max-multipart-body-size (::max-multipart-body-size cfg)
|
||||||
:xnio/io-threads (::io-threads cfg)
|
:xnio/io-threads (or (::io-threads cfg)
|
||||||
:xnio/dispatch (::wrk/executor cfg)
|
(max 3 (px/get-available-processors)))
|
||||||
|
:xnio/worker-threads (or (::worker-threads cfg)
|
||||||
|
(max 6 (px/get-available-processors)))
|
||||||
|
:xnio/dispatch true
|
||||||
:ring/async true}
|
:ring/async true}
|
||||||
|
|
||||||
handler (cond
|
handler (cond
|
||||||
(some? router)
|
(some? router)
|
||||||
(wrap-router router)
|
(router-handler router)
|
||||||
|
|
||||||
(some? handler)
|
(some? handler)
|
||||||
handler
|
handler
|
||||||
@ -97,32 +102,35 @@
|
|||||||
|
|
||||||
(defn- not-found-handler
|
(defn- not-found-handler
|
||||||
[_ respond _]
|
[_ respond _]
|
||||||
(respond (yrs/response 404)))
|
(respond {::yrs/status 404}))
|
||||||
|
|
||||||
(defn- wrap-router
|
(defn- router-handler
|
||||||
[router]
|
[router]
|
||||||
(letfn [(handler [request respond raise]
|
(letfn [(resolve-handler [request]
|
||||||
(if-let [match (r/match-by-path router (yrq/path request))]
|
(if-let [match (r/match-by-path router (yrq/path request))]
|
||||||
(let [params (:path-params match)
|
(let [params (:path-params match)
|
||||||
result (:result match)
|
result (:result match)
|
||||||
handler (or (:handler result) not-found-handler)
|
handler (or (:handler result) not-found-handler)
|
||||||
request (assoc request :path-params params)]
|
request (assoc request :path-params params)]
|
||||||
(handler request respond raise))
|
(partial handler request))
|
||||||
(not-found-handler request respond raise)))
|
(partial not-found-handler request)))
|
||||||
|
|
||||||
(on-error [cause request respond]
|
(on-error [cause request]
|
||||||
(let [{:keys [body] :as response} (errors/handle cause request)]
|
(let [{:keys [body] :as response} (errors/handle cause request)]
|
||||||
(respond
|
(cond-> response
|
||||||
(cond-> response
|
(map? body)
|
||||||
(map? body)
|
(-> (update ::yrs/headers assoc "content-type" "application/transit+json")
|
||||||
(-> (update :headers assoc "content-type" "application/transit+json")
|
(assoc ::yrs/body (t/encode-str body {:type :json-verbose}))))))]
|
||||||
(assoc :body (t/encode-str body {:type :json-verbose})))))))]
|
|
||||||
|
|
||||||
(fn [request respond _]
|
(fn [request respond _]
|
||||||
(try
|
(let [handler (resolve-handler request)
|
||||||
(handler request respond #(on-error % request respond))
|
exchange (yrq/exchange request)]
|
||||||
(catch Throwable cause
|
(handler
|
||||||
(on-error cause request respond))))))
|
(fn [response]
|
||||||
|
(yt/dispatch! exchange (partial respond response)))
|
||||||
|
(fn [cause]
|
||||||
|
(let [response (on-error cause request)]
|
||||||
|
(yt/dispatch! exchange (partial respond response)))))))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; HTTP ROUTER
|
;; HTTP ROUTER
|
||||||
@ -130,11 +138,11 @@
|
|||||||
|
|
||||||
(defmethod ig/pre-init-spec ::router [_]
|
(defmethod ig/pre-init-spec ::router [_]
|
||||||
(s/keys :req [::session/manager
|
(s/keys :req [::session/manager
|
||||||
::actoken/manager
|
|
||||||
::ws/routes
|
::ws/routes
|
||||||
::rpc/routes
|
::rpc/routes
|
||||||
::rpc.doc/routes
|
::rpc.doc/routes
|
||||||
::oidc/routes
|
::oidc/routes
|
||||||
|
::main/props
|
||||||
::assets/routes
|
::assets/routes
|
||||||
::debug/routes
|
::debug/routes
|
||||||
::db/pool
|
::db/pool
|
||||||
@ -151,7 +159,8 @@
|
|||||||
[session/soft-auth cfg]
|
[session/soft-auth cfg]
|
||||||
[actoken/soft-auth cfg]
|
[actoken/soft-auth cfg]
|
||||||
[mw/errors errors/handle]
|
[mw/errors errors/handle]
|
||||||
[mw/restrict-methods]]}
|
[mw/restrict-methods]
|
||||||
|
[mw/with-dispatch :vthread]]}
|
||||||
|
|
||||||
(::mtx/routes cfg)
|
(::mtx/routes cfg)
|
||||||
(::assets/routes cfg)
|
(::assets/routes cfg)
|
||||||
|
|||||||
@ -7,26 +7,12 @@
|
|||||||
(ns app.http.access-token
|
(ns app.http.access-token
|
||||||
(:require
|
(:require
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.worker :as-alias wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[integrant.core :as ig]
|
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]
|
|
||||||
[yetti.request :as yrq]))
|
[yetti.request :as yrq]))
|
||||||
|
|
||||||
|
|
||||||
(s/def ::manager
|
|
||||||
(s/keys :req [::db/pool ::wrk/executor ::main/props]))
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::manager [_] ::manager)
|
|
||||||
(defmethod ig/init-key ::manager [_ cfg] cfg)
|
|
||||||
(defmethod ig/halt-key! ::manager [_ _])
|
|
||||||
|
|
||||||
(def header-re #"^Token\s+(.*)")
|
(def header-re #"^Token\s+(.*)")
|
||||||
|
|
||||||
(defn- get-token
|
(defn- get-token
|
||||||
@ -48,40 +34,32 @@
|
|||||||
(db/decode-pgarray #{})))))
|
(db/decode-pgarray #{})))))
|
||||||
|
|
||||||
(defn- wrap-soft-auth
|
(defn- wrap-soft-auth
|
||||||
[handler {:keys [::manager]}]
|
"Soft Authentication, will be executed synchronously on the undertow
|
||||||
(us/assert! ::manager manager)
|
worker thread."
|
||||||
|
[handler {:keys [::main/props]}]
|
||||||
|
(letfn [(handle-request [request]
|
||||||
|
(try
|
||||||
|
(let [token (get-token request)
|
||||||
|
claims (decode-token props token)]
|
||||||
|
(cond-> request
|
||||||
|
(map? claims)
|
||||||
|
(assoc ::id (:tid claims))))
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/trace :hint "exception on decoding malformed token" :cause cause)
|
||||||
|
request)))]
|
||||||
|
|
||||||
(let [{:keys [::wrk/executor ::main/props]} manager]
|
|
||||||
(fn [request respond raise]
|
(fn [request respond raise]
|
||||||
(let [token (get-token request)]
|
(let [request (handle-request request)]
|
||||||
(->> (px/submit! executor (partial decode-token props token))
|
(handler request respond raise)))))
|
||||||
(p/fnly (fn [claims cause]
|
|
||||||
(when cause
|
|
||||||
(l/trace :hint "exception on decoding malformed token" :cause cause))
|
|
||||||
(let [request (cond-> request
|
|
||||||
(map? claims)
|
|
||||||
(assoc ::id (:tid claims)))]
|
|
||||||
(handler request respond raise)))))))))
|
|
||||||
|
|
||||||
(defn- wrap-authz
|
(defn- wrap-authz
|
||||||
[handler {:keys [::manager]}]
|
"Authorization middleware, will be executed synchronously on vthread."
|
||||||
(us/assert! ::manager manager)
|
[handler {:keys [::db/pool]}]
|
||||||
(let [{:keys [::wrk/executor ::db/pool]} manager]
|
(fn [request]
|
||||||
(fn [request respond raise]
|
(let [perms (some->> (::id request) (get-token-perms pool))]
|
||||||
(if-let [token-id (::id request)]
|
(handler (cond-> request
|
||||||
(->> (px/submit! executor (partial get-token-perms pool token-id))
|
(some? perms)
|
||||||
(p/fnly (fn [perms cause]
|
(assoc ::perms perms))))))
|
||||||
(cond
|
|
||||||
(some? cause)
|
|
||||||
(raise cause)
|
|
||||||
|
|
||||||
(nil? perms)
|
|
||||||
(handler request respond raise)
|
|
||||||
|
|
||||||
:else
|
|
||||||
(let [request (assoc request ::perms perms)]
|
|
||||||
(handler request respond raise))))))
|
|
||||||
(handler request respond raise)))))
|
|
||||||
|
|
||||||
(def soft-auth
|
(def soft-auth
|
||||||
{:name ::soft-auth
|
{:name ::soft-auth
|
||||||
|
|||||||
@ -14,11 +14,9 @@
|
|||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
[yetti.response :as-alias yrs]))
|
||||||
[yetti.response :as yrs]))
|
|
||||||
|
|
||||||
(def ^:private cache-max-age
|
(def ^:private cache-max-age
|
||||||
(dt/duration {:hours 24}))
|
(dt/duration {:hours 24}))
|
||||||
@ -28,10 +26,9 @@
|
|||||||
|
|
||||||
(defn get-id
|
(defn get-id
|
||||||
[{:keys [path-params]}]
|
[{:keys [path-params]}]
|
||||||
(if-let [id (some-> path-params :id d/parse-uuid)]
|
(or (some-> path-params :id d/parse-uuid)
|
||||||
(p/resolved id)
|
(ex/raise :type :not-found
|
||||||
(p/rejected (ex/error :type :not-found
|
:hunt "object not found")))
|
||||||
:hunt "object not found"))))
|
|
||||||
|
|
||||||
(defn- get-file-media-object
|
(defn- get-file-media-object
|
||||||
[pool id]
|
[pool id]
|
||||||
@ -39,16 +36,12 @@
|
|||||||
|
|
||||||
(defn- serve-object-from-s3
|
(defn- serve-object-from-s3
|
||||||
[{:keys [::sto/storage] :as cfg} obj]
|
[{:keys [::sto/storage] :as cfg} obj]
|
||||||
(let [mdata (meta obj)]
|
(let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
|
||||||
(->> (sto/get-object-url storage obj {:max-age signature-max-age})
|
{::yrs/status 307
|
||||||
(p/fmap (fn [{:keys [host port] :as url}]
|
::yrs/headers {"location" (str url)
|
||||||
(let [headers {"location" (str url)
|
"x-host" (cond-> host port (str ":" port))
|
||||||
"x-host" (cond-> host port (str ":" port))
|
"x-mtype" (-> obj meta :content-type)
|
||||||
"x-mtype" (:content-type mdata)
|
"cache-control" (str "max-age=" (inst-ms cache-max-age))}}))
|
||||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
|
|
||||||
(yrs/response
|
|
||||||
:status 307
|
|
||||||
:headers headers)))))))
|
|
||||||
|
|
||||||
(defn- serve-object-from-fs
|
(defn- serve-object-from-fs
|
||||||
[{:keys [::path]} obj]
|
[{:keys [::path]} obj]
|
||||||
@ -58,8 +51,8 @@
|
|||||||
headers {"x-accel-redirect" (:path purl)
|
headers {"x-accel-redirect" (:path purl)
|
||||||
"content-type" (:content-type mdata)
|
"content-type" (:content-type mdata)
|
||||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
|
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
|
||||||
(p/resolved
|
{::yrs/status 204
|
||||||
(yrs/response :status 204 :headers headers))))
|
::yrs/headers headers}))
|
||||||
|
|
||||||
(defn- serve-object
|
(defn- serve-object
|
||||||
"Helper function that returns the appropriate response depending on
|
"Helper function that returns the appropriate response depending on
|
||||||
@ -72,42 +65,34 @@
|
|||||||
|
|
||||||
(defn objects-handler
|
(defn objects-handler
|
||||||
"Handler that servers storage objects by id."
|
"Handler that servers storage objects by id."
|
||||||
[{:keys [::sto/storage ::wrk/executor] :as cfg} request respond raise]
|
[{:keys [::sto/storage] :as cfg} request]
|
||||||
(->> (get-id request)
|
(let [id (get-id request)
|
||||||
(p/mcat executor (fn [id] (sto/get-object storage id)))
|
obj (sto/get-object storage id)]
|
||||||
(p/mcat executor (fn [obj]
|
(if obj
|
||||||
(if (some? obj)
|
(serve-object cfg obj)
|
||||||
(serve-object cfg obj)
|
{::yrs/status 404})))
|
||||||
(p/resolved (yrs/response 404)))))
|
|
||||||
(p/fnly executor (fn [result cause]
|
|
||||||
(if cause (raise cause) (respond result))))))
|
|
||||||
|
|
||||||
(defn- generic-handler
|
(defn- generic-handler
|
||||||
"A generic handler helper/common code for file-media based handlers."
|
"A generic handler helper/common code for file-media based handlers."
|
||||||
[{:keys [::sto/storage ::wrk/executor] :as cfg} request kf]
|
[{:keys [::sto/storage] :as cfg} request kf]
|
||||||
(let [pool (::db/pool storage)]
|
(let [pool (::db/pool storage)
|
||||||
(->> (get-id request)
|
id (get-id request)
|
||||||
(p/fmap executor (fn [id] (get-file-media-object pool id)))
|
mobj (get-file-media-object pool id)
|
||||||
(p/mcat executor (fn [mobj] (sto/get-object storage (kf mobj))))
|
sobj (sto/get-object storage (kf mobj))]
|
||||||
(p/mcat executor (fn [sobj]
|
(if sobj
|
||||||
(if sobj
|
(serve-object cfg sobj)
|
||||||
(serve-object cfg sobj)
|
{::yrs/status 404})))
|
||||||
(p/resolved (yrs/response 404))))))))
|
|
||||||
|
|
||||||
(defn file-objects-handler
|
(defn file-objects-handler
|
||||||
"Handler that serves storage objects by file media id."
|
"Handler that serves storage objects by file media id."
|
||||||
[cfg request respond raise]
|
[cfg request]
|
||||||
(->> (generic-handler cfg request :media-id)
|
(generic-handler cfg request :media-id))
|
||||||
(p/fnly (fn [result cause]
|
|
||||||
(if cause (raise cause) (respond result))))))
|
|
||||||
|
|
||||||
(defn file-thumbnails-handler
|
(defn file-thumbnails-handler
|
||||||
"Handler that serves storage objects by thumbnail-id and quick
|
"Handler that serves storage objects by thumbnail-id and quick
|
||||||
fallback to file-media-id if no thumbnail is available."
|
fallback to file-media-id if no thumbnail is available."
|
||||||
[cfg request respond raise]
|
[cfg request]
|
||||||
(->> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))
|
(generic-handler cfg request #(or (:thumbnail-id %) (:media-id %))))
|
||||||
(p/fnly (fn [result cause]
|
|
||||||
(if cause (raise cause) (respond result))))))
|
|
||||||
|
|
||||||
;; --- Initialization
|
;; --- Initialization
|
||||||
|
|
||||||
@ -115,7 +100,7 @@
|
|||||||
(s/def ::routes vector?)
|
(s/def ::routes vector?)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::routes [_]
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
(s/keys :req [::sto/storage ::wrk/executor ::path]))
|
(s/keys :req [::sto/storage ::path]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
[jsonista.core :as j]
|
[jsonista.core :as j]
|
||||||
[promesa.exec :as px]
|
[promesa.exec :as px]
|
||||||
[yetti.request :as yrq]
|
[yetti.request :as yrq]
|
||||||
[yetti.response :as yrs]))
|
[yetti.response :as-alias yrs]))
|
||||||
|
|
||||||
(declare parse-json)
|
(declare parse-json)
|
||||||
(declare handle-request)
|
(declare handle-request)
|
||||||
@ -39,7 +39,7 @@
|
|||||||
(letfn [(handler [request respond _]
|
(letfn [(handler [request respond _]
|
||||||
(let [data (-> request yrq/body slurp)]
|
(let [data (-> request yrq/body slurp)]
|
||||||
(px/run! executor #(handle-request cfg data)))
|
(px/run! executor #(handle-request cfg data)))
|
||||||
(respond (yrs/response 200)))]
|
(respond {::yrs/status 200}))]
|
||||||
["/sns" {:handler handler
|
["/sns" {:handler handler
|
||||||
:allowed-methods #{:post}}]))
|
:allowed-methods #{:post}}]))
|
||||||
|
|
||||||
|
|||||||
@ -40,12 +40,25 @@
|
|||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(p/rejected cause))))))
|
(p/rejected cause))))))
|
||||||
|
|
||||||
|
(defn- resolve-client
|
||||||
|
[params]
|
||||||
|
(cond
|
||||||
|
(instance? HttpClient params)
|
||||||
|
params
|
||||||
|
|
||||||
|
(map? params)
|
||||||
|
(resolve-client (::client params))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(throw (UnsupportedOperationException. "invalid arguments"))))
|
||||||
|
|
||||||
(defn req!
|
(defn req!
|
||||||
"A convencience toplevel function for gradual migration to a new API
|
"A convencience toplevel function for gradual migration to a new API
|
||||||
convention."
|
convention."
|
||||||
([{:keys [::client]} request]
|
([cfg-or-client request]
|
||||||
(us/assert! ::client client)
|
(let [client (resolve-client cfg-or-client)]
|
||||||
(send! client request {}))
|
(send! client request {})))
|
||||||
([{:keys [::client]} request options]
|
([cfg-or-client request options]
|
||||||
(us/assert! ::client client)
|
(let [client (resolve-client cfg-or-client)]
|
||||||
(send! client request options)))
|
(send! client request options))))
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.middleware :as mw]
|
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.rpc.commands.binfile :as binf]
|
[app.rpc.commands.binfile :as binf]
|
||||||
[app.rpc.commands.files-create :refer [create-file]]
|
[app.rpc.commands.files-create :refer [create-file]]
|
||||||
@ -21,7 +20,6 @@
|
|||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
[app.util.template :as tmpl]
|
[app.util.template :as tmpl]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[datoteka.io :as io]
|
[datoteka.io :as io]
|
||||||
@ -48,13 +46,17 @@
|
|||||||
(defn prepare-response
|
(defn prepare-response
|
||||||
[body]
|
[body]
|
||||||
(let [headers {"content-type" "application/transit+json"}]
|
(let [headers {"content-type" "application/transit+json"}]
|
||||||
(yrs/response :status 200 :body body :headers headers)))
|
{::yrs/status 200
|
||||||
|
::yrs/body body
|
||||||
|
::yrs/headers headers}))
|
||||||
|
|
||||||
(defn prepare-download-response
|
(defn prepare-download-response
|
||||||
[body filename]
|
[body filename]
|
||||||
(let [headers {"content-disposition" (str "attachment; filename=" filename)
|
(let [headers {"content-disposition" (str "attachment; filename=" filename)
|
||||||
"content-type" "application/octet-stream"}]
|
"content-type" "application/octet-stream"}]
|
||||||
(yrs/response :status 200 :body body :headers headers)))
|
{::yrs/status 200
|
||||||
|
::yrs/body body
|
||||||
|
::yrs/headers headers}))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; INDEX
|
;; INDEX
|
||||||
@ -65,10 +67,10 @@
|
|||||||
(when-not (authorized? pool request)
|
(when-not (authorized? pool request)
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :authentication
|
||||||
:code :only-admins-allowed))
|
:code :only-admins-allowed))
|
||||||
(yrs/response :status 200
|
{::yrs/status 200
|
||||||
:headers {"content-type" "text/html"}
|
::yrs/headers {"content-type" "text/html"}
|
||||||
:body (-> (io/resource "app/templates/debug.tmpl")
|
::yrs/body (-> (io/resource "app/templates/debug.tmpl")
|
||||||
(tmpl/render {}))))
|
(tmpl/render {}))})
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; FILE CHANGES
|
;; FILE CHANGES
|
||||||
@ -115,7 +117,8 @@
|
|||||||
:project-id project-id
|
:project-id project-id
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:data data})
|
:data data})
|
||||||
(yrs/response 201 "OK CREATED"))
|
{::yrs/status 201
|
||||||
|
::yrs/body "OK CREATED"})
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(prepare-response (blob/decode data))))))
|
(prepare-response (blob/decode data))))))
|
||||||
@ -143,7 +146,8 @@
|
|||||||
(db/update! pool :file
|
(db/update! pool :file
|
||||||
{:data (blob/encode data)}
|
{:data (blob/encode data)}
|
||||||
{:id file-id})
|
{:id file-id})
|
||||||
(yrs/response 200 "OK UPDATED"))
|
{::yrs/status 200
|
||||||
|
::yrs/body "OK UPDATED"})
|
||||||
|
|
||||||
(do
|
(do
|
||||||
(create-file pool {:id file-id
|
(create-file pool {:id file-id
|
||||||
@ -151,9 +155,11 @@
|
|||||||
:project-id project-id
|
:project-id project-id
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:data data})
|
:data data})
|
||||||
(yrs/response 201 "OK CREATED"))))
|
{::yrs/status 201
|
||||||
|
::yrs/body "OK CREATED"})))
|
||||||
|
|
||||||
(yrs/response 500 "ERROR"))))
|
{::yrs/status 500
|
||||||
|
::yrs/body "ERROR"})))
|
||||||
|
|
||||||
(defn file-data-handler
|
(defn file-data-handler
|
||||||
[cfg request]
|
[cfg request]
|
||||||
@ -241,11 +247,12 @@
|
|||||||
(let [result (if (= 1 (:version report))
|
(let [result (if (= 1 (:version report))
|
||||||
(render-template-v1 report)
|
(render-template-v1 report)
|
||||||
(render-template-v2 report))]
|
(render-template-v2 report))]
|
||||||
(yrs/response :status 200
|
{::yrs/status 200
|
||||||
:body result
|
::yrs/body result
|
||||||
:headers {"content-type" "text/html; charset=utf-8"
|
::yrs/headers {"content-type" "text/html; charset=utf-8"
|
||||||
"x-robots-tag" "noindex"}))
|
"x-robots-tag" "noindex"}})
|
||||||
(yrs/response 404 "not found"))))
|
{::yrs/status 404
|
||||||
|
::yrs/body "not found"})))
|
||||||
|
|
||||||
(def sql:error-reports
|
(def sql:error-reports
|
||||||
"SELECT id, created_at,
|
"SELECT id, created_at,
|
||||||
@ -261,11 +268,11 @@
|
|||||||
:code :only-admins-allowed))
|
:code :only-admins-allowed))
|
||||||
(let [items (->> (db/exec! pool [sql:error-reports])
|
(let [items (->> (db/exec! pool [sql:error-reports])
|
||||||
(map #(update % :created-at dt/format-instant :rfc1123)))]
|
(map #(update % :created-at dt/format-instant :rfc1123)))]
|
||||||
(yrs/response :status 200
|
{::yrs/status 200
|
||||||
:body (-> (io/resource "app/templates/error-list.tmpl")
|
::yrs/body (-> (io/resource "app/templates/error-list.tmpl")
|
||||||
(tmpl/render {:items items}))
|
(tmpl/render {:items items}))
|
||||||
:headers {"content-type" "text/html; charset=utf-8"
|
::yrs/headers {"content-type" "text/html; charset=utf-8"
|
||||||
"x-robots-tag" "noindex"})))
|
"x-robots-tag" "noindex"}}))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; EXPORT/IMPORT
|
;; EXPORT/IMPORT
|
||||||
@ -301,16 +308,15 @@
|
|||||||
::binf/profile-id profile-id
|
::binf/profile-id profile-id
|
||||||
::binf/project-id project-id))
|
::binf/project-id project-id))
|
||||||
|
|
||||||
(yrs/response
|
{::yrs/status 200
|
||||||
:status 200
|
::yrs/headers {"content-type" "text/plain"}
|
||||||
:headers {"content-type" "text/plain"}
|
::yrs/body "OK CLONED"})
|
||||||
:body "OK CLONED"))
|
|
||||||
|
{::yrs/status 200
|
||||||
|
::yrs/body (io/input-stream path)
|
||||||
|
::yrs/headers {"content-type" "application/octet-stream"
|
||||||
|
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}}))))
|
||||||
|
|
||||||
(yrs/response
|
|
||||||
:status 200
|
|
||||||
:headers {"content-type" "application/octet-stream"
|
|
||||||
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}
|
|
||||||
:body (io/input-stream path))))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn import-handler
|
(defn import-handler
|
||||||
@ -340,10 +346,9 @@
|
|||||||
::binf/profile-id profile-id
|
::binf/profile-id profile-id
|
||||||
::binf/project-id project-id))
|
::binf/project-id project-id))
|
||||||
|
|
||||||
(yrs/response
|
{::yrs/status 200
|
||||||
:status 200
|
::yrs/headers {"content-type" "text/plain"}
|
||||||
:headers {"content-type" "text/plain"}
|
::yrs/body "OK"}))
|
||||||
:body "OK")))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; OTHER SMALL VIEWS/HANDLERS
|
;; OTHER SMALL VIEWS/HANDLERS
|
||||||
@ -354,11 +359,13 @@
|
|||||||
[{:keys [::db/pool]} _]
|
[{:keys [::db/pool]} _]
|
||||||
(try
|
(try
|
||||||
(db/exec-one! pool ["select count(*) as count from server_prop;"])
|
(db/exec-one! pool ["select count(*) as count from server_prop;"])
|
||||||
(yrs/response 200 "OK")
|
{::yrs/status 200
|
||||||
|
::yrs/body "OK"}
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "unable to execute query on health handler"
|
(l/warn :hint "unable to execute query on health handler"
|
||||||
:cause cause)
|
:cause cause)
|
||||||
(yrs/response 503 "KO"))))
|
{::yrs/status 503
|
||||||
|
::yrs/body "KO"})))
|
||||||
|
|
||||||
(defn changelog-handler
|
(defn changelog-handler
|
||||||
[_ _]
|
[_ _]
|
||||||
@ -367,10 +374,11 @@
|
|||||||
(md->html [text]
|
(md->html [text]
|
||||||
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
|
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
|
||||||
(if-let [clog (io/resource "changelog.md")]
|
(if-let [clog (io/resource "changelog.md")]
|
||||||
(yrs/response :status 200
|
{::yrs/status 200
|
||||||
:headers {"content-type" "text/html; charset=utf-8"}
|
::yrs/headers {"content-type" "text/html; charset=utf-8"}
|
||||||
:body (-> clog slurp md->html))
|
::yrs/body (-> clog slurp md->html)}
|
||||||
(yrs/response :status 404 :body "NOT FOUND"))))
|
{::yrs/status 404
|
||||||
|
::yrs/body "NOT FOUND"})))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; INIT
|
;; INIT
|
||||||
@ -380,33 +388,26 @@
|
|||||||
{:compile
|
{:compile
|
||||||
(fn [& _]
|
(fn [& _]
|
||||||
(fn [handler pool]
|
(fn [handler pool]
|
||||||
(fn [request respond raise]
|
(fn [request]
|
||||||
(if (authorized? pool request)
|
(if (authorized? pool request)
|
||||||
(handler request respond raise)
|
(handler request)
|
||||||
(raise (ex/error :type :authentication
|
(ex/raise :type :authentication
|
||||||
:code :only-admins-allowed))))))})
|
:code :only-admins-allowed)))))})
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::routes [_]
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
(s/keys :req [::db/pool
|
(s/keys :req [::db/pool ::session/manager]))
|
||||||
::wrk/executor
|
|
||||||
::session/manager]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ {:keys [::db/pool ::wrk/executor] :as cfg}]
|
[_ {:keys [::db/pool] :as cfg}]
|
||||||
[["/readyz" {:middleware [[mw/with-dispatch executor]
|
[["/readyz" {:handler (partial health-handler cfg)}]
|
||||||
[mw/with-config cfg]]
|
|
||||||
:handler health-handler}]
|
|
||||||
["/dbg" {:middleware [[session/authz cfg]
|
["/dbg" {:middleware [[session/authz cfg]
|
||||||
[with-authorization pool]
|
[with-authorization pool]]}
|
||||||
[mw/with-dispatch executor]
|
["" {:handler (partial index-handler cfg)}]
|
||||||
[mw/with-config cfg]]}
|
["/health" {:handler (partial health-handler cfg)}]
|
||||||
["" {:handler index-handler}]
|
["/changelog" {:handler (partial changelog-handler cfg)}]
|
||||||
["/health" {:handler health-handler}]
|
["/error/:id" {:handler (partial error-handler cfg)}]
|
||||||
["/changelog" {:handler changelog-handler}]
|
["/error" {:handler (partial error-list-handler cfg)}]
|
||||||
;; ["/error-by-id/:id" {:handler error-handler}]
|
["/file/export" {:handler (partial export-handler cfg)}]
|
||||||
["/error/:id" {:handler error-handler}]
|
["/file/import" {:handler (partial import-handler cfg)}]
|
||||||
["/error" {:handler error-list-handler}]
|
["/file/data" {:handler (partial file-data-handler cfg)}]
|
||||||
["/file/export" {:handler export-handler}]
|
["/file/changes" {:handler (partial file-changes-handler cfg)}]]])
|
||||||
["/file/import" {:handler import-handler}]
|
|
||||||
["/file/data" {:handler file-data-handler}]
|
|
||||||
["/file/changes" {:handler file-changes-handler}]]])
|
|
||||||
|
|||||||
@ -46,20 +46,30 @@
|
|||||||
|
|
||||||
(defmethod handle-exception :authentication
|
(defmethod handle-exception :authentication
|
||||||
[err _]
|
[err _]
|
||||||
(yrs/response 401 (ex-data err)))
|
{::yrs/status 401
|
||||||
|
::yrs/body (ex-data err)})
|
||||||
|
|
||||||
(defmethod handle-exception :authorization
|
(defmethod handle-exception :authorization
|
||||||
[err _]
|
[err _]
|
||||||
(yrs/response 403 (ex-data err)))
|
{::yrs/status 403
|
||||||
|
::yrs/body (ex-data err)})
|
||||||
|
|
||||||
(defmethod handle-exception :restriction
|
(defmethod handle-exception :restriction
|
||||||
[err _]
|
[err _]
|
||||||
(yrs/response 400 (ex-data err)))
|
{::yrs/status 400
|
||||||
|
::yrs/body (ex-data err)})
|
||||||
|
|
||||||
(defmethod handle-exception :rate-limit
|
(defmethod handle-exception :rate-limit
|
||||||
[err _]
|
[err _]
|
||||||
(let [headers (-> err ex-data ::http/headers)]
|
(let [headers (-> err ex-data ::http/headers)]
|
||||||
(yrs/response :status 429 :body "" :headers headers)))
|
{::yrs/status 429
|
||||||
|
::yrs/headers headers}))
|
||||||
|
|
||||||
|
(defmethod handle-exception :concurrency-limit
|
||||||
|
[err _]
|
||||||
|
(let [headers (-> err ex-data ::http/headers)]
|
||||||
|
{::yrs/status 429
|
||||||
|
::yrs/headers headers}))
|
||||||
|
|
||||||
(defmethod handle-exception :validation
|
(defmethod handle-exception :validation
|
||||||
[err _]
|
[err _]
|
||||||
@ -67,16 +77,16 @@
|
|||||||
(cond
|
(cond
|
||||||
(= code :spec-validation)
|
(= code :spec-validation)
|
||||||
(let [explain (ex/explain data)]
|
(let [explain (ex/explain data)]
|
||||||
(yrs/response :status 400
|
{::yrs/status 400
|
||||||
:body (-> data
|
::yrs/body (-> data
|
||||||
(dissoc ::s/problems ::s/value)
|
(dissoc ::s/problems ::s/value)
|
||||||
(cond-> explain (assoc :explain explain)))))
|
(cond-> explain (assoc :explain explain)))})
|
||||||
|
|
||||||
(= code :request-body-too-large)
|
(= code :request-body-too-large)
|
||||||
(yrs/response :status 413 :body data)
|
{::yrs/status 413 ::yrs/body data}
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(yrs/response :status 400 :body data))))
|
{::yrs/status 400 ::yrs/body data})))
|
||||||
|
|
||||||
(defmethod handle-exception :assertion
|
(defmethod handle-exception :assertion
|
||||||
[error request]
|
[error request]
|
||||||
@ -84,31 +94,27 @@
|
|||||||
explain (ex/explain edata)]
|
explain (ex/explain edata)]
|
||||||
(binding [l/*context* (request->context request)]
|
(binding [l/*context* (request->context request)]
|
||||||
(l/error :hint "Assertion error" :message (ex-message error) :cause error)
|
(l/error :hint "Assertion error" :message (ex-message error) :cause error)
|
||||||
(yrs/response :status 500
|
{::yrs/status 500
|
||||||
:body {:type :server-error
|
::yrs/body {:type :server-error
|
||||||
:code :assertion
|
:code :assertion
|
||||||
:data (-> edata
|
:data (-> edata
|
||||||
(dissoc ::s/problems ::s/value ::s/spec)
|
(dissoc ::s/problems ::s/value ::s/spec)
|
||||||
(cond-> explain (assoc :explain explain)))}))))
|
(cond-> explain (assoc :explain explain)))}})))
|
||||||
|
|
||||||
(defmethod handle-exception :not-found
|
(defmethod handle-exception :not-found
|
||||||
[err _]
|
[err _]
|
||||||
(yrs/response 404 (ex-data err)))
|
{::yrs/status 404
|
||||||
|
::yrs/body (ex-data err)})
|
||||||
|
|
||||||
(defmethod handle-exception :internal
|
(defmethod handle-exception :internal
|
||||||
[error request]
|
[error request]
|
||||||
(let [{:keys [code] :as edata} (ex-data error)]
|
(binding [l/*context* (request->context request)]
|
||||||
(cond
|
(l/error :hint "Internal error" :message (ex-message error) :cause error)
|
||||||
(= :concurrency-limit-reached code)
|
{::yrs/status 500
|
||||||
(yrs/response 429)
|
::yrs/body {:type :server-error
|
||||||
|
:code :unhandloed
|
||||||
:else
|
:hint (ex-message error)
|
||||||
(binding [l/*context* (request->context request)]
|
:data (ex-data error)}}))
|
||||||
(l/error :hint "Internal error" :message (ex-message error) :cause error)
|
|
||||||
(yrs/response 500 {:type :server-error
|
|
||||||
:code :unhandled
|
|
||||||
:hint (ex-message error)
|
|
||||||
:data edata})))))
|
|
||||||
|
|
||||||
(defmethod handle-exception org.postgresql.util.PSQLException
|
(defmethod handle-exception org.postgresql.util.PSQLException
|
||||||
[error request]
|
[error request]
|
||||||
@ -117,20 +123,23 @@
|
|||||||
(l/error :hint "PSQL error" :message (ex-message error) :cause error)
|
(l/error :hint "PSQL error" :message (ex-message error) :cause error)
|
||||||
(cond
|
(cond
|
||||||
(= state "57014")
|
(= state "57014")
|
||||||
(yrs/response 504 {:type :server-error
|
{::yrs/status 504
|
||||||
:code :statement-timeout
|
::yrs/body {:type :server-error
|
||||||
:hint (ex-message error)})
|
:code :statement-timeout
|
||||||
|
:hint (ex-message error)}}
|
||||||
|
|
||||||
(= state "25P03")
|
(= state "25P03")
|
||||||
(yrs/response 504 {:type :server-error
|
{::yrs/status 504
|
||||||
:code :idle-in-transaction-timeout
|
::yrs/body {:type :server-error
|
||||||
:hint (ex-message error)})
|
:code :idle-in-transaction-timeout
|
||||||
|
:hint (ex-message error)}}
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(yrs/response 500 {:type :server-error
|
{::yrs/status 500
|
||||||
:code :unexpected
|
::yrs/body {:type :server-error
|
||||||
:hint (ex-message error)
|
:code :unexpected
|
||||||
:state state})))))
|
:hint (ex-message error)
|
||||||
|
:state state}}))))
|
||||||
|
|
||||||
(defmethod handle-exception :default
|
(defmethod handle-exception :default
|
||||||
[error request]
|
[error request]
|
||||||
@ -140,9 +149,10 @@
|
|||||||
(nil? edata)
|
(nil? edata)
|
||||||
(binding [l/*context* (request->context request)]
|
(binding [l/*context* (request->context request)]
|
||||||
(l/error :hint "Unexpected error" :message (ex-message error) :cause error)
|
(l/error :hint "Unexpected error" :message (ex-message error) :cause error)
|
||||||
(yrs/response 500 {:type :server-error
|
{::yrs/status 500
|
||||||
:code :unexpected
|
::yrs/body {:type :server-error
|
||||||
:hint (ex-message error)}))
|
:code :unexpected
|
||||||
|
:hint (ex-message error)}})
|
||||||
|
|
||||||
;; This is a special case for the idle-in-transaction error;
|
;; This is a special case for the idle-in-transaction error;
|
||||||
;; when it happens, the connection is automatically closed and
|
;; when it happens, the connection is automatically closed and
|
||||||
@ -156,10 +166,11 @@
|
|||||||
:else
|
:else
|
||||||
(binding [l/*context* (request->context request)]
|
(binding [l/*context* (request->context request)]
|
||||||
(l/error :hint "Unhandled error" :message (ex-message error) :cause error)
|
(l/error :hint "Unhandled error" :message (ex-message error) :cause error)
|
||||||
(yrs/response 500 {:type :server-error
|
{::yrs/status 500
|
||||||
:code :unhandled
|
::yrs/body {:type :server-error
|
||||||
:hint (ex-message error)
|
:code :unhandled
|
||||||
:data edata})))))
|
:hint (ex-message error)
|
||||||
|
:data edata}}))))
|
||||||
|
|
||||||
(defn handle
|
(defn handle
|
||||||
[cause request]
|
[cause request]
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[promesa.exec :as px]
|
[promesa.exec :as px]
|
||||||
|
[promesa.util :as pu]
|
||||||
[yetti.adapter :as yt]
|
[yetti.adapter :as yt]
|
||||||
[yetti.middleware :as ymw]
|
[yetti.middleware :as ymw]
|
||||||
[yetti.request :as yrq]
|
[yetti.request :as yrq]
|
||||||
@ -22,7 +23,10 @@
|
|||||||
com.fasterxml.jackson.core.JsonParseException
|
com.fasterxml.jackson.core.JsonParseException
|
||||||
com.fasterxml.jackson.core.io.JsonEOFException
|
com.fasterxml.jackson.core.io.JsonEOFException
|
||||||
io.undertow.server.RequestTooBigException
|
io.undertow.server.RequestTooBigException
|
||||||
java.io.OutputStream))
|
java.io.OutputStream
|
||||||
|
java.io.InputStream))
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(def server-timing
|
(def server-timing
|
||||||
{:name ::server-timing
|
{:name ::server-timing
|
||||||
@ -44,14 +48,14 @@
|
|||||||
(let [header (yrq/get-header request "content-type")]
|
(let [header (yrq/get-header request "content-type")]
|
||||||
(cond
|
(cond
|
||||||
(str/starts-with? header "application/transit+json")
|
(str/starts-with? header "application/transit+json")
|
||||||
(with-open [is (yrq/body request)]
|
(with-open [^InputStream is (yrq/body request)]
|
||||||
(let [params (t/read! (t/reader is))]
|
(let [params (t/read! (t/reader is))]
|
||||||
(-> request
|
(-> request
|
||||||
(assoc :body-params params)
|
(assoc :body-params params)
|
||||||
(update :params merge params))))
|
(update :params merge params))))
|
||||||
|
|
||||||
(str/starts-with? header "application/json")
|
(str/starts-with? header "application/json")
|
||||||
(with-open [is (yrq/body request)]
|
(with-open [^InputStream is (yrq/body request)]
|
||||||
(let [params (json/decode is json-mapper)]
|
(let [params (json/decode is json-mapper)]
|
||||||
(-> request
|
(-> request
|
||||||
(assoc :body-params params)
|
(assoc :body-params params)
|
||||||
@ -62,6 +66,11 @@
|
|||||||
|
|
||||||
(handle-error [raise cause]
|
(handle-error [raise cause]
|
||||||
(cond
|
(cond
|
||||||
|
(instance? RuntimeException cause)
|
||||||
|
(if-let [cause (ex-cause cause)]
|
||||||
|
(handle-error raise cause)
|
||||||
|
(raise cause))
|
||||||
|
|
||||||
(instance? RequestTooBigException cause)
|
(instance? RequestTooBigException cause)
|
||||||
(raise (ex/error :type :validation
|
(raise (ex/error :type :validation
|
||||||
:code :request-body-too-large
|
:code :request-body-too-large
|
||||||
@ -78,12 +87,12 @@
|
|||||||
(raise cause)))]
|
(raise cause)))]
|
||||||
|
|
||||||
(fn [request respond raise]
|
(fn [request respond raise]
|
||||||
(let [request (ex/try! (process-request request))]
|
(if (= (yrq/method request) :post)
|
||||||
(if (ex/exception? request)
|
(let [request (ex/try! (process-request request))]
|
||||||
(if (ex/runtime-exception? request)
|
(if (ex/exception? request)
|
||||||
(handle-error raise (or (ex-cause request) request))
|
(handle-error raise request)
|
||||||
(handle-error raise request))
|
(handler request respond raise)))
|
||||||
(handler request respond raise))))))
|
(handler request respond raise)))))
|
||||||
|
|
||||||
(def parse-request
|
(def parse-request
|
||||||
{:name ::parse-request
|
{:name ::parse-request
|
||||||
@ -94,12 +103,7 @@
|
|||||||
needed because transit-java calls flush very aggresivelly on each
|
needed because transit-java calls flush very aggresivelly on each
|
||||||
object write."
|
object write."
|
||||||
[^java.io.OutputStream os ^long chunk-size]
|
[^java.io.OutputStream os ^long chunk-size]
|
||||||
(proxy [java.io.BufferedOutputStream] [os (int chunk-size)]
|
(yetti.util.BufferedOutputStream. os (int chunk-size)))
|
||||||
;; Explicitly do not forward flush
|
|
||||||
(flush [])
|
|
||||||
(close []
|
|
||||||
(proxy-super flush)
|
|
||||||
(proxy-super close))))
|
|
||||||
|
|
||||||
(def ^:const buffer-size (:xnio/buffer-size yt/defaults))
|
(def ^:const buffer-size (:xnio/buffer-size yt/defaults))
|
||||||
|
|
||||||
@ -109,13 +113,10 @@
|
|||||||
(reify yrs/StreamableResponseBody
|
(reify yrs/StreamableResponseBody
|
||||||
(-write-body-to-stream [_ _ output-stream]
|
(-write-body-to-stream [_ _ output-stream]
|
||||||
(try
|
(try
|
||||||
(with-open [bos (buffered-output-stream output-stream buffer-size)]
|
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
|
||||||
(let [tw (t/writer bos opts)]
|
(let [tw (t/writer bos opts)]
|
||||||
(t/write! tw data)))
|
(t/write! tw data)))
|
||||||
|
(catch java.io.IOException _)
|
||||||
(catch java.io.IOException _cause
|
|
||||||
;; Do nothing, EOF means client closes connection abruptly
|
|
||||||
nil)
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "unexpected error on encoding response"
|
(l/warn :hint "unexpected error on encoding response"
|
||||||
:cause cause))
|
:cause cause))
|
||||||
@ -126,13 +127,10 @@
|
|||||||
(reify yrs/StreamableResponseBody
|
(reify yrs/StreamableResponseBody
|
||||||
(-write-body-to-stream [_ _ output-stream]
|
(-write-body-to-stream [_ _ output-stream]
|
||||||
(try
|
(try
|
||||||
|
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
|
||||||
(with-open [bos (buffered-output-stream output-stream buffer-size)]
|
|
||||||
(json/write! bos data json-mapper))
|
(json/write! bos data json-mapper))
|
||||||
|
|
||||||
(catch java.io.IOException _cause
|
(catch java.io.IOException _)
|
||||||
;; Do nothing, EOF means client closes connection abruptly
|
|
||||||
nil)
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "unexpected error on encoding response"
|
(l/warn :hint "unexpected error on encoding response"
|
||||||
:cause cause))
|
:cause cause))
|
||||||
@ -140,15 +138,15 @@
|
|||||||
(.close ^OutputStream output-stream))))))
|
(.close ^OutputStream output-stream))))))
|
||||||
|
|
||||||
(format-response-with-json [response _]
|
(format-response-with-json [response _]
|
||||||
(let [body (yrs/body response)]
|
(let [body (::yrs/body response)]
|
||||||
(if (or (boolean? body) (coll? body))
|
(if (or (boolean? body) (coll? body))
|
||||||
(-> response
|
(-> response
|
||||||
(update :headers assoc "content-type" "application/json")
|
(update ::yrs/headers assoc "content-type" "application/json")
|
||||||
(assoc :body (json-streamable-body body)))
|
(assoc ::yrs/body (json-streamable-body body)))
|
||||||
response)))
|
response)))
|
||||||
|
|
||||||
(format-response-with-transit [response request]
|
(format-response-with-transit [response request]
|
||||||
(let [body (yrs/body response)]
|
(let [body (::yrs/body response)]
|
||||||
(if (or (boolean? body) (coll? body))
|
(if (or (boolean? body) (coll? body))
|
||||||
(let [qs (yrq/query request)
|
(let [qs (yrq/query request)
|
||||||
opts (if (or (contains? cf/flags :transit-readable-response)
|
opts (if (or (contains? cf/flags :transit-readable-response)
|
||||||
@ -156,8 +154,8 @@
|
|||||||
{:type :json-verbose}
|
{:type :json-verbose}
|
||||||
{:type :json})]
|
{:type :json})]
|
||||||
(-> response
|
(-> response
|
||||||
(update :headers assoc "content-type" "application/transit+json")
|
(update ::yrs/headers assoc "content-type" "application/transit+json")
|
||||||
(assoc :body (transit-streamable-body body opts))))
|
(assoc ::yrs/body (transit-streamable-body body opts))))
|
||||||
response)))
|
response)))
|
||||||
|
|
||||||
(format-response [response request]
|
(format-response [response request]
|
||||||
@ -181,8 +179,7 @@
|
|||||||
(fn [request respond raise]
|
(fn [request respond raise]
|
||||||
(handler request
|
(handler request
|
||||||
(fn [response]
|
(fn [response]
|
||||||
(let [response (process-response response request)]
|
(respond (process-response response request)))
|
||||||
(respond response)))
|
|
||||||
raise))))
|
raise))))
|
||||||
|
|
||||||
(def format-response
|
(def format-response
|
||||||
@ -191,74 +188,59 @@
|
|||||||
|
|
||||||
(defn wrap-errors
|
(defn wrap-errors
|
||||||
[handler on-error]
|
[handler on-error]
|
||||||
(fn [request respond _]
|
(fn [request respond raise]
|
||||||
(handler request respond (fn [cause]
|
(handler request respond (fn [cause]
|
||||||
(-> cause (on-error request) respond)))))
|
(try
|
||||||
|
(respond (on-error cause request))
|
||||||
|
(catch Throwable cause
|
||||||
|
(raise cause)))))))
|
||||||
|
|
||||||
(def errors
|
(def errors
|
||||||
{:name ::errors
|
{:name ::errors
|
||||||
:compile (constantly wrap-errors)})
|
:compile (constantly wrap-errors)})
|
||||||
|
|
||||||
|
(defn- with-cors-headers
|
||||||
|
[headers origin]
|
||||||
|
(-> headers
|
||||||
|
(assoc "access-control-allow-origin" origin)
|
||||||
|
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
|
||||||
|
(assoc "access-control-allow-credentials" "true")
|
||||||
|
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
|
||||||
|
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width")))
|
||||||
|
|
||||||
(defn wrap-cors
|
(defn wrap-cors
|
||||||
[handler]
|
[handler]
|
||||||
(if-not (contains? cf/flags :cors)
|
(fn [request]
|
||||||
handler
|
(let [response (if (= (yrq/method request) :options)
|
||||||
(letfn [(add-headers [headers request]
|
{::yrs/status 200}
|
||||||
(let [origin (yrq/get-header request "origin")]
|
(handler request))
|
||||||
(-> headers
|
origin (yrq/get-header request "origin")]
|
||||||
(assoc "access-control-allow-origin" origin)
|
(update response ::yrs/headers with-cors-headers origin))))
|
||||||
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
|
|
||||||
(assoc "access-control-allow-credentials" "true")
|
|
||||||
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
|
|
||||||
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))))
|
|
||||||
|
|
||||||
(update-response [response request]
|
|
||||||
(update response :headers add-headers request))]
|
|
||||||
|
|
||||||
(fn [request respond raise]
|
|
||||||
(if (= (yrq/method request) :options)
|
|
||||||
(-> (yrs/response 200)
|
|
||||||
(update-response request)
|
|
||||||
(respond))
|
|
||||||
(handler request
|
|
||||||
(fn [response]
|
|
||||||
(respond (update-response response request)))
|
|
||||||
raise))))))
|
|
||||||
|
|
||||||
(def cors
|
(def cors
|
||||||
{:name ::cors
|
{:name ::cors
|
||||||
:compile (constantly wrap-cors)})
|
:compile (fn [& _]
|
||||||
|
(when (contains? cf/flags :cors)
|
||||||
(defn compile-restrict-methods
|
wrap-cors))})
|
||||||
[data _]
|
|
||||||
(when-let [allowed (:allowed-methods data)]
|
|
||||||
(fn [handler]
|
|
||||||
(fn [request respond raise]
|
|
||||||
(let [method (yrq/method request)]
|
|
||||||
(if (contains? allowed method)
|
|
||||||
(handler request respond raise)
|
|
||||||
(respond (yrs/response 405))))))))
|
|
||||||
|
|
||||||
(def restrict-methods
|
(def restrict-methods
|
||||||
{:name ::restrict-methods
|
{:name ::restrict-methods
|
||||||
:compile compile-restrict-methods})
|
:compile
|
||||||
|
(fn [data _]
|
||||||
|
(when-let [allowed (:allowed-methods data)]
|
||||||
|
(fn [handler]
|
||||||
|
(fn [request respond raise]
|
||||||
|
(let [method (yrq/method request)]
|
||||||
|
(if (contains? allowed method)
|
||||||
|
(handler request respond raise)
|
||||||
|
(respond {::yrs/status 405})))))))})
|
||||||
|
|
||||||
(def with-dispatch
|
(def with-dispatch
|
||||||
{:name ::with-dispatch
|
{:name ::with-dispatch
|
||||||
:compile
|
:compile
|
||||||
(fn [& _]
|
(fn [& _]
|
||||||
(fn [handler executor]
|
(fn [handler executor]
|
||||||
(fn [request respond raise]
|
(let [executor (px/resolve-executor executor)]
|
||||||
(-> (px/submit! executor #(handler request))
|
(fn [request respond raise]
|
||||||
(p/bind p/wrap)
|
(->> (px/submit! executor (partial handler request))
|
||||||
(p/then respond)
|
(p/fnly (pu/handler respond raise)))))))})
|
||||||
(p/catch raise)))))})
|
|
||||||
|
|
||||||
(def with-config
|
|
||||||
{:name ::with-config
|
|
||||||
:compile
|
|
||||||
(fn [& _]
|
|
||||||
(fn [handler config]
|
|
||||||
(fn
|
|
||||||
([request] (handler config request))
|
|
||||||
([request respond raise] (handler config request respond raise)))))})
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
(:refer-clojure :exclude [read])
|
(:refer-clojure :exclude [read])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
@ -18,12 +17,9 @@
|
|||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]
|
|
||||||
[yetti.request :as yrq]))
|
[yetti.request :as yrq]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -76,69 +72,56 @@
|
|||||||
:id key})
|
:id key})
|
||||||
|
|
||||||
(defn- database-manager
|
(defn- database-manager
|
||||||
[{:keys [::db/pool ::wrk/executor ::main/props]}]
|
[pool]
|
||||||
^{::wrk/executor executor
|
|
||||||
::db/pool pool
|
|
||||||
::main/props props}
|
|
||||||
(reify ISessionManager
|
(reify ISessionManager
|
||||||
(read [_ token]
|
(read [_ token]
|
||||||
(px/with-dispatch executor
|
(db/exec-one! pool (sql/select :http-session {:id token})))
|
||||||
(db/exec-one! pool (sql/select :http-session {:id token}))))
|
|
||||||
|
|
||||||
(write! [_ key params]
|
(write! [_ key params]
|
||||||
(px/with-dispatch executor
|
(let [params (prepare-session-params key params)]
|
||||||
(let [params (prepare-session-params key params)]
|
(db/insert! pool :http-session params)
|
||||||
(db/insert! pool :http-session params)
|
params))
|
||||||
params)))
|
|
||||||
|
|
||||||
(update! [_ params]
|
(update! [_ params]
|
||||||
(let [updated-at (dt/now)]
|
(let [updated-at (dt/now)]
|
||||||
(px/with-dispatch executor
|
(db/update! pool :http-session
|
||||||
(db/update! pool :http-session
|
{:updated-at updated-at}
|
||||||
{:updated-at updated-at}
|
{:id (:id params)})
|
||||||
{:id (:id params)})
|
(assoc params :updated-at updated-at)))
|
||||||
(assoc params :updated-at updated-at))))
|
|
||||||
|
|
||||||
(delete! [_ token]
|
(delete! [_ token]
|
||||||
(px/with-dispatch executor
|
(db/delete! pool :http-session {:id token})
|
||||||
(db/delete! pool :http-session {:id token})
|
nil)))
|
||||||
nil))))
|
|
||||||
|
|
||||||
(defn inmemory-manager
|
(defn inmemory-manager
|
||||||
[{:keys [::db/pool ::wrk/executor ::main/props]}]
|
[]
|
||||||
(let [cache (atom {})]
|
(let [cache (atom {})]
|
||||||
^{::main/props props
|
|
||||||
::wrk/executor executor
|
|
||||||
::db/pool pool}
|
|
||||||
(reify ISessionManager
|
(reify ISessionManager
|
||||||
(read [_ token]
|
(read [_ token]
|
||||||
(p/do (get @cache token)))
|
(get @cache token))
|
||||||
|
|
||||||
(write! [_ key params]
|
(write! [_ key params]
|
||||||
(p/do
|
(let [params (prepare-session-params key params)]
|
||||||
(let [params (prepare-session-params key params)]
|
(swap! cache assoc key params)
|
||||||
(swap! cache assoc key params)
|
params))
|
||||||
params)))
|
|
||||||
|
|
||||||
(update! [_ params]
|
(update! [_ params]
|
||||||
(p/do
|
(let [updated-at (dt/now)]
|
||||||
(let [updated-at (dt/now)]
|
(swap! cache update (:id params) assoc :updated-at updated-at)
|
||||||
(swap! cache update (:id params) assoc :updated-at updated-at)
|
(assoc params :updated-at updated-at)))
|
||||||
(assoc params :updated-at updated-at))))
|
|
||||||
|
|
||||||
(delete! [_ token]
|
(delete! [_ token]
|
||||||
(p/do
|
(swap! cache dissoc token)
|
||||||
(swap! cache dissoc token)
|
nil))))
|
||||||
nil)))))
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::manager [_]
|
(defmethod ig/pre-init-spec ::manager [_]
|
||||||
(s/keys :req [::db/pool ::wrk/executor ::main/props]))
|
(s/keys :req [::db/pool]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::manager
|
(defmethod ig/init-key ::manager
|
||||||
[_ {:keys [::db/pool] :as cfg}]
|
[_ {:keys [::db/pool]}]
|
||||||
(if (db/read-only? pool)
|
(if (db/read-only? pool)
|
||||||
(inmemory-manager cfg)
|
(inmemory-manager)
|
||||||
(database-manager cfg)))
|
(database-manager pool)))
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::manager
|
(defmethod ig/halt-key! ::manager
|
||||||
[_ _])
|
[_ _])
|
||||||
@ -154,40 +137,35 @@
|
|||||||
(declare ^:private gen-token)
|
(declare ^:private gen-token)
|
||||||
|
|
||||||
(defn create-fn
|
(defn create-fn
|
||||||
[{:keys [::manager]} profile-id]
|
[{:keys [::manager ::main/props]} profile-id]
|
||||||
(us/assert! ::manager manager)
|
(us/assert! ::manager manager)
|
||||||
(us/assert! ::us/uuid profile-id)
|
(us/assert! ::us/uuid profile-id)
|
||||||
|
|
||||||
(let [props (-> manager meta ::main/props)]
|
(fn [request response]
|
||||||
(fn [request response]
|
(let [uagent (yrq/get-header request "user-agent")
|
||||||
(let [uagent (yrq/get-header request "user-agent")
|
params {:profile-id profile-id
|
||||||
params {:profile-id profile-id
|
:user-agent uagent
|
||||||
:user-agent uagent
|
:created-at (dt/now)}
|
||||||
:created-at (dt/now)}
|
token (gen-token props params)
|
||||||
token (gen-token props params)]
|
session (write! manager token params)]
|
||||||
|
(l/trace :hint "create" :profile-id (str profile-id))
|
||||||
|
(-> response
|
||||||
|
(assign-auth-token-cookie session)
|
||||||
|
(assign-authenticated-cookie session)))))
|
||||||
|
|
||||||
(->> (write! manager token params)
|
|
||||||
(p/fmap (fn [session]
|
|
||||||
(l/trace :hint "create" :profile-id (str profile-id))
|
|
||||||
(-> response
|
|
||||||
(assign-auth-token-cookie session)
|
|
||||||
(assign-authenticated-cookie session)))))))))
|
|
||||||
(defn delete-fn
|
(defn delete-fn
|
||||||
[{:keys [::manager]}]
|
[{:keys [::manager]}]
|
||||||
(us/assert! ::manager manager)
|
(us/assert! ::manager manager)
|
||||||
(letfn [(delete [{:keys [profile-id] :as request}]
|
(fn [request response]
|
||||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||||
cookie (yrq/get-cookie request cname)]
|
cookie (yrq/get-cookie request cname)]
|
||||||
(l/trace :hint "delete" :profile-id profile-id)
|
(l/trace :hint "delete" :profile-id (:profile-id request))
|
||||||
(some->> (:value cookie) (delete! manager))))]
|
(some->> (:value cookie) (delete! manager))
|
||||||
(fn [request response]
|
(-> response
|
||||||
(p/do
|
(assoc :status 204)
|
||||||
(delete request)
|
(assoc :body nil)
|
||||||
(-> response
|
(clear-auth-token-cookie)
|
||||||
(assoc :status 204)
|
(clear-authenticated-cookie)))))
|
||||||
(assoc :body nil)
|
|
||||||
(clear-auth-token-cookie)
|
|
||||||
(clear-authenticated-cookie))))))
|
|
||||||
|
|
||||||
(defn- gen-token
|
(defn- gen-token
|
||||||
[props {:keys [profile-id created-at]}]
|
[props {:keys [profile-id created-at]}]
|
||||||
@ -216,58 +194,39 @@
|
|||||||
(let [elapsed (dt/diff updated-at (dt/now))]
|
(let [elapsed (dt/diff updated-at (dt/now))]
|
||||||
(neg? (compare default-renewal-max-age elapsed)))))
|
(neg? (compare default-renewal-max-age elapsed)))))
|
||||||
|
|
||||||
(defn- wrap-reneval
|
|
||||||
[respond manager session]
|
|
||||||
(fn [response]
|
|
||||||
(p/let [session (update! manager session)]
|
|
||||||
(-> response
|
|
||||||
(assign-auth-token-cookie session)
|
|
||||||
(assign-authenticated-cookie session)
|
|
||||||
(respond)))))
|
|
||||||
|
|
||||||
(defn- wrap-soft-auth
|
(defn- wrap-soft-auth
|
||||||
[handler {:keys [::manager]}]
|
[handler {:keys [::manager ::main/props]}]
|
||||||
(us/assert! ::manager manager)
|
(us/assert! ::manager manager)
|
||||||
|
(letfn [(handle-request [request]
|
||||||
|
(try
|
||||||
|
(let [token (get-token request)
|
||||||
|
claims (decode-token props token)]
|
||||||
|
(cond-> request
|
||||||
|
(map? claims)
|
||||||
|
(-> (assoc ::token-claims claims)
|
||||||
|
(assoc ::token token))))
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/trace :hint "exception on decoding malformed token" :cause cause)
|
||||||
|
request)))]
|
||||||
|
|
||||||
(let [{:keys [::wrk/executor ::main/props]} (meta manager)]
|
|
||||||
(fn [request respond raise]
|
(fn [request respond raise]
|
||||||
(let [token (ex/try! (get-token request))]
|
(let [request (handle-request request)]
|
||||||
(if (ex/exception? token)
|
(handler request respond raise)))))
|
||||||
(raise token)
|
|
||||||
(->> (px/submit! executor (partial decode-token props token))
|
|
||||||
(p/fnly (fn [claims cause]
|
|
||||||
(when cause
|
|
||||||
(l/trace :hint "exception on decoding malformed token" :cause cause))
|
|
||||||
(let [request (cond-> request
|
|
||||||
(map? claims)
|
|
||||||
(-> (assoc ::token-claims claims)
|
|
||||||
(assoc ::token token)))]
|
|
||||||
(handler request respond raise))))))))))
|
|
||||||
|
|
||||||
(defn- wrap-authz
|
(defn- wrap-authz
|
||||||
[handler {:keys [::manager]}]
|
[handler {:keys [::manager]}]
|
||||||
(us/assert! ::manager manager)
|
(us/assert! ::manager manager)
|
||||||
(fn [request respond raise]
|
(fn [request]
|
||||||
(if-let [token (::token request)]
|
(let [session (get-session manager (::token request))
|
||||||
(->> (get-session manager token)
|
request (cond-> request
|
||||||
(p/fnly (fn [session cause]
|
(some? session)
|
||||||
(cond
|
(assoc ::profile-id (:profile-id session)
|
||||||
(some? cause)
|
::id (:id session)))]
|
||||||
(raise cause)
|
|
||||||
|
|
||||||
(nil? session)
|
(cond-> (handler request)
|
||||||
(handler request respond raise)
|
(renew-session? session)
|
||||||
|
(-> (assign-auth-token-cookie session)
|
||||||
:else
|
(assign-authenticated-cookie session))))))
|
||||||
(let [request (-> request
|
|
||||||
(assoc ::profile-id (:profile-id session))
|
|
||||||
(assoc ::id (:id session)))
|
|
||||||
respond (cond-> respond
|
|
||||||
(renew-session? session)
|
|
||||||
(wrap-reneval manager session))]
|
|
||||||
(handler request respond raise))))))
|
|
||||||
|
|
||||||
(handler request respond raise))))
|
|
||||||
|
|
||||||
(def soft-auth
|
(def soft-auth
|
||||||
{:name ::soft-auth
|
{:name ::soft-auth
|
||||||
|
|||||||
@ -17,9 +17,9 @@
|
|||||||
[app.msgbus :as mbus]
|
[app.msgbus :as mbus]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.websocket :as ws]
|
[app.util.websocket :as ws]
|
||||||
[clojure.core.async :as a]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
|
[promesa.exec.csp :as sp]
|
||||||
[yetti.websocket :as yws]))
|
[yetti.websocket :as yws]))
|
||||||
|
|
||||||
(def recv-labels
|
(def recv-labels
|
||||||
@ -34,70 +34,38 @@
|
|||||||
|
|
||||||
(def state (atom {}))
|
(def state (atom {}))
|
||||||
|
|
||||||
(defn- on-connect
|
|
||||||
[{:keys [::mtx/metrics]} wsp]
|
|
||||||
(let [created-at (dt/now)]
|
|
||||||
(swap! state assoc (::ws/id @wsp) wsp)
|
|
||||||
(mtx/run! metrics
|
|
||||||
:id :websocket-active-connections
|
|
||||||
:inc 1)
|
|
||||||
(fn []
|
|
||||||
(swap! state dissoc (::ws/id @wsp))
|
|
||||||
(mtx/run! metrics :id :websocket-active-connections :dec 1)
|
|
||||||
(mtx/run! metrics
|
|
||||||
:id :websocket-session-timing
|
|
||||||
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)))))
|
|
||||||
|
|
||||||
(defn- on-rcv-message
|
|
||||||
[{:keys [::mtx/metrics]} _ message]
|
|
||||||
(mtx/run! metrics
|
|
||||||
:id :websocket-messages-total
|
|
||||||
:labels recv-labels
|
|
||||||
:inc 1)
|
|
||||||
message)
|
|
||||||
|
|
||||||
(defn- on-snd-message
|
|
||||||
[{:keys [::mtx/metrics]} _ message]
|
|
||||||
(mtx/run! metrics
|
|
||||||
:id :websocket-messages-total
|
|
||||||
:labels send-labels
|
|
||||||
:inc 1)
|
|
||||||
message)
|
|
||||||
|
|
||||||
;; REPL HELPERS
|
;; REPL HELPERS
|
||||||
|
|
||||||
(defn repl-get-connections-for-file
|
(defn repl-get-connections-for-file
|
||||||
[file-id]
|
[file-id]
|
||||||
(->> (vals @state)
|
(->> (vals @state)
|
||||||
(filter #(= file-id (-> % deref ::file-subscription :file-id)))
|
(filter #(= file-id (-> % deref ::file-subscription :file-id)))
|
||||||
(map deref)
|
|
||||||
(map ::ws/id)))
|
(map ::ws/id)))
|
||||||
|
|
||||||
(defn repl-get-connections-for-team
|
(defn repl-get-connections-for-team
|
||||||
[team-id]
|
[team-id]
|
||||||
(->> (vals @state)
|
(->> (vals @state)
|
||||||
(filter #(= team-id (-> % deref ::team-subscription :team-id)))
|
(filter #(= team-id (-> % deref ::team-subscription :team-id)))
|
||||||
(map deref)
|
|
||||||
(map ::ws/id)))
|
(map ::ws/id)))
|
||||||
|
|
||||||
(defn repl-close-connection
|
(defn repl-close-connection
|
||||||
[id]
|
[id]
|
||||||
(when-let [wsp (get @state id)]
|
(when-let [{:keys [::ws/close-ch] :as wsp} (get @state id)]
|
||||||
(a/>!! (::ws/close-ch @wsp) [8899 "closed from server"])
|
(sp/put! close-ch [8899 "closed from server"])
|
||||||
(a/close! (::ws/close-ch @wsp))))
|
(sp/close! close-ch)))
|
||||||
|
|
||||||
(defn repl-get-connection-info
|
(defn repl-get-connection-info
|
||||||
[id]
|
[id]
|
||||||
(when-let [wsp (get @state id)]
|
(when-let [wsp (get @state id)]
|
||||||
{:id id
|
{:id id
|
||||||
:created-at (::created-at @wsp)
|
:created-at (::created-at wsp)
|
||||||
:profile-id (::profile-id @wsp)
|
:profile-id (::profile-id wsp)
|
||||||
:session-id (::session-id @wsp)
|
:session-id (::session-id wsp)
|
||||||
:user-agent (::ws/user-agent @wsp)
|
:user-agent (::ws/user-agent wsp)
|
||||||
:ip-addr (::ws/remote-addr @wsp)
|
:ip-addr (::ws/remote-addr wsp)
|
||||||
:last-activity-at (::ws/last-activity-at @wsp)
|
:last-activity-at (::ws/last-activity-at wsp)
|
||||||
:subscribed-file (-> wsp deref ::file-subscription :file-id)
|
:subscribed-file (-> wsp ::file-subscription :file-id)
|
||||||
:subscribed-team (-> wsp deref ::team-subscription :team-id)}))
|
:subscribed-team (-> wsp ::team-subscription :team-id)}))
|
||||||
|
|
||||||
(defn repl-print-connection-info
|
(defn repl-print-connection-info
|
||||||
[id]
|
[id]
|
||||||
@ -117,223 +85,215 @@
|
|||||||
(fn [_ _ message]
|
(fn [_ _ message]
|
||||||
(:type message)))
|
(:type message)))
|
||||||
|
|
||||||
(defmethod handle-message :connect
|
(defmethod handle-message :open
|
||||||
[cfg wsp _]
|
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/output-ch ::ws/state ::profile-id ::session-id] :as wsp} _]
|
||||||
|
(l/trace :fn "handle-message" :event "open" :conn-id id)
|
||||||
|
(let [ch (sp/chan :buf (sp/dropping-buffer 16)
|
||||||
|
:xf (remove #(= (:session-id %) session-id)))]
|
||||||
|
|
||||||
(let [msgbus (::mbus/msgbus cfg)
|
;; Subscribe to the profile channel and forward all messages to websocket output
|
||||||
conn-id (::ws/id @wsp)
|
;; channel (send them to the client).
|
||||||
profile-id (::profile-id @wsp)
|
(swap! state assoc ::profile-subscription {:channel ch})
|
||||||
session-id (::session-id @wsp)
|
|
||||||
output-ch (::ws/output-ch @wsp)
|
|
||||||
|
|
||||||
xform (remove #(= (:session-id %) session-id))
|
;; Forward the subscription messages directly to the websocket output channel
|
||||||
channel (a/chan (a/dropping-buffer 16) xform)]
|
(sp/pipe ch output-ch false)
|
||||||
|
|
||||||
(l/trace :fn "handle-message" :event "connect" :conn-id conn-id)
|
;; Subscribe to the profile topic on msgbus/redis
|
||||||
|
(mbus/sub! msgbus :topic profile-id :chan ch)))
|
||||||
|
|
||||||
;; Subscribe to the profile channel and forward all messages to
|
(defmethod handle-message :close
|
||||||
;; websocket output channel (send them to the client).
|
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::profile-id ::session-id]} _]
|
||||||
(swap! wsp assoc ::profile-subscription channel)
|
(l/trace :fn "handle-message" :event "close" :conn-id id)
|
||||||
(a/pipe channel output-ch false)
|
(let [psub (::profile-subscription @state)
|
||||||
(mbus/sub! msgbus :topic profile-id :chan channel)))
|
fsub (::file-subscription @state)
|
||||||
|
tsub (::team-subscription @state)
|
||||||
|
msg {:type :disconnect
|
||||||
|
:subs-id profile-id
|
||||||
|
:profile-id profile-id
|
||||||
|
:session-id session-id}]
|
||||||
|
|
||||||
(defmethod handle-message :disconnect
|
;; Close profile subscription if exists
|
||||||
[cfg wsp _]
|
(when-let [ch (:channel psub)]
|
||||||
(let [msgbus (::mbus/msgbus cfg)
|
(sp/close! ch)
|
||||||
conn-id (::ws/id @wsp)
|
(mbus/purge! msgbus [ch]))
|
||||||
profile-id (::profile-id @wsp)
|
|
||||||
session-id (::session-id @wsp)
|
|
||||||
profile-ch (::profile-subscription @wsp)
|
|
||||||
fsub (::file-subscription @wsp)
|
|
||||||
tsub (::team-subscription @wsp)
|
|
||||||
|
|
||||||
message {:type :disconnect
|
;; Close team subscription if exists
|
||||||
:subs-id profile-id
|
(when-let [ch (:channel tsub)]
|
||||||
:profile-id profile-id
|
(sp/close! ch)
|
||||||
:session-id session-id}]
|
(mbus/purge! msgbus [ch]))
|
||||||
|
|
||||||
(l/trace :fn "handle-message"
|
|
||||||
:event :disconnect
|
|
||||||
:conn-id conn-id)
|
|
||||||
|
|
||||||
(a/go
|
|
||||||
;; Close the main profile subscription
|
|
||||||
(a/close! profile-ch)
|
|
||||||
(a/<! (mbus/purge! msgbus [profile-ch]))
|
|
||||||
|
|
||||||
;; Close tram subscription if exists
|
|
||||||
(when-let [channel (:channel tsub)]
|
|
||||||
(a/close! channel)
|
|
||||||
(a/<! (mbus/purge! msgbus channel)))
|
|
||||||
|
|
||||||
|
;; Close file subscription if exists
|
||||||
(when-let [{:keys [topic channel]} fsub]
|
(when-let [{:keys [topic channel]} fsub]
|
||||||
(a/close! channel)
|
(sp/close! channel)
|
||||||
(a/<! (mbus/purge! msgbus channel))
|
(mbus/purge! msgbus [channel])
|
||||||
(a/<! (mbus/pub! msgbus :topic topic :message message))))))
|
(mbus/pub! msgbus :topic topic :message msg))))
|
||||||
|
|
||||||
(defmethod handle-message :subscribe-team
|
(defmethod handle-message :subscribe-team
|
||||||
[cfg wsp {:keys [team-id] :as params}]
|
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::ws/output-ch ::session-id]} {:keys [team-id] :as params}]
|
||||||
(let [msgbus (::mbus/msgbus cfg)
|
(l/trace :fn "handle-message" :event "subscribe-team" :team-id team-id :conn-id id)
|
||||||
conn-id (::ws/id @wsp)
|
(let [prev-subs (get @state ::team-subscription)
|
||||||
session-id (::session-id @wsp)
|
channel (sp/chan :buf (sp/dropping-buffer 64)
|
||||||
output-ch (::ws/output-ch @wsp)
|
:xf (comp
|
||||||
prev-subs (get @wsp ::team-subscription)
|
(remove #(= (:session-id %) session-id))
|
||||||
xform (comp
|
(map #(assoc % :subs-id team-id))))]
|
||||||
(remove #(= (:session-id %) session-id))
|
|
||||||
(map #(assoc % :subs-id team-id)))
|
|
||||||
|
|
||||||
channel (a/chan (a/dropping-buffer 64) xform)]
|
(sp/pipe channel output-ch false)
|
||||||
|
(mbus/sub! msgbus :topic team-id :chan channel)
|
||||||
|
|
||||||
(l/trace :fn "handle-message"
|
(let [subs {:team-id team-id :channel channel :topic team-id}]
|
||||||
:event :subscribe-team
|
(swap! state assoc ::team-subscription subs))
|
||||||
:team-id team-id
|
|
||||||
:conn-id conn-id)
|
|
||||||
|
|
||||||
(a/pipe channel output-ch false)
|
;; Close previous subscription if exists
|
||||||
|
(when-let [ch (:channel prev-subs)]
|
||||||
|
(sp/close! ch)
|
||||||
|
(mbus/purge! msgbus [ch]))))
|
||||||
|
|
||||||
(let [state {:team-id team-id :channel channel :topic team-id}]
|
|
||||||
(swap! wsp assoc ::team-subscription state))
|
|
||||||
|
|
||||||
(a/go
|
|
||||||
;; Close previous subscription if exists
|
|
||||||
(when-let [channel (:channel prev-subs)]
|
|
||||||
(a/close! channel)
|
|
||||||
(a/<! (mbus/purge! msgbus channel))))
|
|
||||||
|
|
||||||
(a/go
|
|
||||||
(a/<! (mbus/sub! msgbus :topic team-id :chan channel)))))
|
|
||||||
|
|
||||||
(defmethod handle-message :subscribe-file
|
(defmethod handle-message :subscribe-file
|
||||||
[cfg wsp {:keys [file-id] :as params}]
|
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::ws/output-ch ::session-id ::profile-id]} {:keys [file-id] :as params}]
|
||||||
(let [msgbus (::mbus/msgbus cfg)
|
(l/trace :fn "handle-message" :event "subscribe-file" :file-id file-id :conn-id id)
|
||||||
conn-id (::ws/id @wsp)
|
(let [psub (::file-subscription @state)
|
||||||
profile-id (::profile-id @wsp)
|
fch (sp/chan :buf (sp/dropping-buffer 64)
|
||||||
session-id (::session-id @wsp)
|
:xf (comp (remove #(= (:session-id %) session-id))
|
||||||
output-ch (::ws/output-ch @wsp)
|
(map #(assoc % :subs-id file-id))))]
|
||||||
prev-subs (::file-subscription @wsp)
|
|
||||||
xform (comp (remove #(= (:session-id %) session-id))
|
|
||||||
(map #(assoc % :subs-id file-id)))
|
|
||||||
channel (a/chan (a/dropping-buffer 64) xform)]
|
|
||||||
|
|
||||||
(l/trace :fn "handle-message"
|
(let [subs {:file-id file-id :channel fch :topic file-id}]
|
||||||
:event :subscribe-file
|
(swap! state assoc ::file-subscription subs))
|
||||||
:file-id file-id
|
|
||||||
:conn-id conn-id)
|
|
||||||
|
|
||||||
(let [state {:file-id file-id :channel channel :topic file-id}]
|
;; Close previous subscription if exists
|
||||||
(swap! wsp assoc ::file-subscription state))
|
(when-let [ch (:channel psub)]
|
||||||
|
(sp/close! ch)
|
||||||
|
(mbus/purge! msgbus [ch]))
|
||||||
|
|
||||||
(a/go
|
(sp/go-loop []
|
||||||
;; Close previous subscription if exists
|
(when-let [{:keys [type] :as message} (sp/take! fch)]
|
||||||
(when-let [channel (:channel prev-subs)]
|
(sp/put! output-ch message)
|
||||||
(a/close! channel)
|
(when (or (= :join-file type)
|
||||||
(a/<! (mbus/purge! msgbus channel))))
|
(= :leave-file type)
|
||||||
|
(= :disconnect type))
|
||||||
;; Message forwarding
|
(let [message {:type :presence
|
||||||
(a/go
|
:file-id file-id
|
||||||
(loop []
|
:session-id session-id
|
||||||
(when-let [{:keys [type] :as message} (a/<! channel)]
|
|
||||||
(when (or (= :join-file type)
|
|
||||||
(= :leave-file type)
|
|
||||||
(= :disconnect type))
|
|
||||||
(let [message {:type :presence
|
|
||||||
:file-id file-id
|
|
||||||
:session-id session-id
|
|
||||||
:profile-id profile-id}]
|
:profile-id profile-id}]
|
||||||
(a/<! (mbus/pub! msgbus :topic file-id :message message))))
|
(mbus/pub! msgbus
|
||||||
(a/>! output-ch message)
|
:topic file-id
|
||||||
(recur))))
|
:message message)))
|
||||||
|
(recur)))
|
||||||
|
|
||||||
(a/go
|
;; Subscribe to file topic
|
||||||
;; Subscribe to file topic
|
(mbus/sub! msgbus :topic file-id :chan fch)
|
||||||
(a/<! (mbus/sub! msgbus :topic file-id :chan channel))
|
|
||||||
|
|
||||||
;; Notifify the rest of participants of the new connection.
|
;; Notifify the rest of participants of the new connection.
|
||||||
(let [message {:type :join-file
|
(let [message {:type :join-file
|
||||||
:file-id file-id
|
:file-id file-id
|
||||||
:subs-id file-id
|
:subs-id file-id
|
||||||
:session-id session-id
|
:session-id session-id
|
||||||
:profile-id profile-id}]
|
:profile-id profile-id}]
|
||||||
(a/<! (mbus/pub! msgbus :topic file-id :message message))))))
|
(mbus/pub! msgbus :topic file-id :message message))))
|
||||||
|
|
||||||
(defmethod handle-message :unsubscribe-file
|
(defmethod handle-message :unsubscribe-file
|
||||||
[cfg wsp {:keys [file-id] :as params}]
|
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::session-id ::profile-id]} {:keys [file-id] :as params}]
|
||||||
(let [msgbus (::mbus/msgbus cfg)
|
(l/trace :fn "handle-message" :event "unsubscribe-file" :file-id file-id :conn-id id)
|
||||||
conn-id (::ws/id @wsp)
|
|
||||||
session-id (::session-id @wsp)
|
|
||||||
profile-id (::profile-id @wsp)
|
|
||||||
subs (::file-subscription @wsp)
|
|
||||||
|
|
||||||
message {:type :leave-file
|
(let [subs (::file-subscription @state)
|
||||||
:file-id file-id
|
message {:type :leave-file
|
||||||
:session-id session-id
|
:file-id file-id
|
||||||
:profile-id profile-id}]
|
:session-id session-id
|
||||||
|
:profile-id profile-id}]
|
||||||
|
|
||||||
(l/trace :fn "handle-message"
|
(when (= (:file-id subs) file-id)
|
||||||
:event :unsubscribe-file
|
(mbus/pub! msgbus :topic file-id :message message)
|
||||||
:file-id file-id
|
(let [ch (:channel subs)]
|
||||||
:conn-id conn-id)
|
(sp/close! ch)
|
||||||
|
(mbus/purge! msgbus [ch])))))
|
||||||
(a/go
|
|
||||||
(when (= (:file-id subs) file-id)
|
|
||||||
(let [channel (:channel subs)]
|
|
||||||
(a/close! channel)
|
|
||||||
(a/<! (mbus/purge! msgbus channel))
|
|
||||||
(a/<! (mbus/pub! msgbus :topic file-id :message message)))))))
|
|
||||||
|
|
||||||
(defmethod handle-message :keepalive
|
(defmethod handle-message :keepalive
|
||||||
[_ _ _]
|
[_ _ _]
|
||||||
(l/trace :fn "handle-message" :event :keepalive)
|
(l/trace :fn "handle-message" :event :keepalive))
|
||||||
(a/go :nothing))
|
|
||||||
|
(defmethod handle-message :broadcast
|
||||||
|
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::session-id ::profile-id]} message]
|
||||||
|
(l/trace :fn "handle-message" :event "broadcast" :conn-id id)
|
||||||
|
(let [message (-> message
|
||||||
|
(assoc :subs-id profile-id)
|
||||||
|
(assoc :profile-id profile-id)
|
||||||
|
(assoc :session-id session-id))]
|
||||||
|
(mbus/pub! msgbus :topic profile-id :message message)))
|
||||||
|
|
||||||
(defmethod handle-message :pointer-update
|
(defmethod handle-message :pointer-update
|
||||||
[cfg wsp {:keys [file-id] :as message}]
|
[{:keys [::mbus/msgbus]} {:keys [::ws/state ::session-id ::profile-id]} {:keys [file-id] :as message}]
|
||||||
(let [msgbus (::mbus/msgbus cfg)
|
(when (::file-subscription @state)
|
||||||
profile-id (::profile-id @wsp)
|
(let [message (-> message
|
||||||
session-id (::session-id @wsp)
|
(assoc :subs-id file-id)
|
||||||
subs (::file-subscription @wsp)
|
(assoc :profile-id profile-id)
|
||||||
message (-> message
|
(assoc :session-id session-id))]
|
||||||
(assoc :subs-id file-id)
|
(mbus/pub! msgbus :topic file-id :message message))))
|
||||||
(assoc :profile-id profile-id)
|
|
||||||
(assoc :session-id session-id))]
|
|
||||||
(a/go
|
|
||||||
;; Only allow receive pointer updates when active subscription
|
|
||||||
(when subs
|
|
||||||
(a/<! (mbus/pub! msgbus :topic file-id :message message))))))
|
|
||||||
|
|
||||||
(defmethod handle-message :default
|
(defmethod handle-message :default
|
||||||
[_ wsp message]
|
[_ {:keys [::ws/id]} message]
|
||||||
(let [conn-id (::ws/id @wsp)]
|
(l/warn :hint "received unexpected message"
|
||||||
(l/warn :hint "received unexpected message"
|
:message message
|
||||||
:message message
|
:conn-id id))
|
||||||
:conn-id conn-id)
|
|
||||||
(a/go :none)))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; HTTP HANDLER
|
;; HTTP HANDLER
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- on-connect
|
||||||
|
[{:keys [::mtx/metrics]} {:keys [::ws/id] :as wsp}]
|
||||||
|
(let [created-at (dt/now)]
|
||||||
|
(l/trace :fn "on-connect" :conn-id id)
|
||||||
|
(swap! state assoc id wsp)
|
||||||
|
(mtx/run! metrics
|
||||||
|
:id :websocket-active-connections
|
||||||
|
:inc 1)
|
||||||
|
|
||||||
|
(assoc wsp ::ws/on-disconnect
|
||||||
|
(fn []
|
||||||
|
(l/trace :fn "on-disconnect" :conn-id id)
|
||||||
|
(swap! state dissoc id)
|
||||||
|
(mtx/run! metrics :id :websocket-active-connections :dec 1)
|
||||||
|
(mtx/run! metrics
|
||||||
|
:id :websocket-session-timing
|
||||||
|
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0))))))
|
||||||
|
|
||||||
|
(defn- on-rcv-message
|
||||||
|
[{:keys [::mtx/metrics ::profile-id ::session-id]} message]
|
||||||
|
(mtx/run! metrics
|
||||||
|
:id :websocket-messages-total
|
||||||
|
:labels recv-labels
|
||||||
|
:inc 1)
|
||||||
|
(assoc message :profile-id profile-id :session-id session-id))
|
||||||
|
|
||||||
|
(defn- on-snd-message
|
||||||
|
[{:keys [::mtx/metrics]} message]
|
||||||
|
(mtx/run! metrics
|
||||||
|
:id :websocket-messages-total
|
||||||
|
:labels send-labels
|
||||||
|
:inc 1)
|
||||||
|
message)
|
||||||
|
|
||||||
|
|
||||||
(s/def ::session-id ::us/uuid)
|
(s/def ::session-id ::us/uuid)
|
||||||
(s/def ::handler-params
|
(s/def ::handler-params
|
||||||
(s/keys :req-un [::session-id]))
|
(s/keys :req-un [::session-id]))
|
||||||
|
|
||||||
(defn- http-handler
|
(defn- http-handler
|
||||||
[cfg {:keys [params ::session/profile-id] :as request} respond raise]
|
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||||
(let [{:keys [session-id]} (us/conform ::handler-params params)]
|
(let [{:keys [session-id]} (us/conform ::handler-params params)]
|
||||||
(cond
|
(cond
|
||||||
(not profile-id)
|
(not profile-id)
|
||||||
(raise (ex/error :type :authentication
|
(ex/raise :type :authentication
|
||||||
:hint "Authentication required."))
|
:hint "Authentication required.")
|
||||||
|
|
||||||
(not (yws/upgrade-request? request))
|
(not (yws/upgrade-request? request))
|
||||||
(raise (ex/error :type :validation
|
(ex/raise :type :validation
|
||||||
:code :websocket-request-expected
|
:code :websocket-request-expected
|
||||||
:hint "this endpoint only accepts websocket connections"))
|
:hint "this endpoint only accepts websocket connections")
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(do
|
(do
|
||||||
(l/trace :hint "websocket request" :profile-id profile-id :session-id session-id)
|
(l/trace :hint "websocket request" :profile-id profile-id :session-id session-id)
|
||||||
|
|
||||||
(->> (ws/handler
|
(->> (ws/handler
|
||||||
::ws/on-rcv-message (partial on-rcv-message cfg)
|
::ws/on-rcv-message (partial on-rcv-message cfg)
|
||||||
::ws/on-snd-message (partial on-snd-message cfg)
|
::ws/on-snd-message (partial on-snd-message cfg)
|
||||||
@ -341,8 +301,7 @@
|
|||||||
::ws/handler (partial handle-message cfg)
|
::ws/handler (partial handle-message cfg)
|
||||||
::profile-id profile-id
|
::profile-id profile-id
|
||||||
::session-id session-id)
|
::session-id session-id)
|
||||||
(yws/upgrade request)
|
(yws/upgrade request))))))
|
||||||
(respond))))))
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::routes [_]
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
(s/keys :req [::mbus/msgbus
|
(s/keys :req [::mbus/msgbus
|
||||||
|
|||||||
@ -16,13 +16,15 @@
|
|||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.client :as http]
|
[app.http :as-alias http]
|
||||||
|
[app.http.client :as http.client]
|
||||||
[app.loggers.audit.tasks :as-alias tasks]
|
[app.loggers.audit.tasks :as-alias tasks]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.retry :as rtry]
|
[app.util.retry :as rtry]
|
||||||
|
[app.util.services :as-alias sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
@ -92,6 +94,15 @@
|
|||||||
|
|
||||||
;; --- SPECS
|
;; --- SPECS
|
||||||
|
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; COLLECTOR
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
;; Defines a service that collects the audit/activity log using
|
||||||
|
;; internal database. Later this audit log can be transferred to
|
||||||
|
;; an external storage and data cleared.
|
||||||
|
|
||||||
(s/def ::profile-id ::us/uuid)
|
(s/def ::profile-id ::us/uuid)
|
||||||
(s/def ::name ::us/string)
|
(s/def ::name ::us/string)
|
||||||
(s/def ::type ::us/string)
|
(s/def ::type ::us/string)
|
||||||
@ -104,20 +115,13 @@
|
|||||||
(s/or :fn fn? :str string? :kw keyword?))
|
(s/or :fn fn? :str string? :kw keyword?))
|
||||||
|
|
||||||
(s/def ::event
|
(s/def ::event
|
||||||
(s/keys :req-un [::type ::name ::profile-id]
|
(s/keys :req [::type ::name ::profile-id]
|
||||||
:opt-un [::ip-addr ::props]
|
:opt [::ip-addr
|
||||||
:opt [::webhooks/event?
|
::props
|
||||||
|
::webhooks/event?
|
||||||
::webhooks/batch-timeout
|
::webhooks/batch-timeout
|
||||||
::webhooks/batch-key]))
|
::webhooks/batch-key]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; COLLECTOR
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
;; Defines a service that collects the audit/activity log using
|
|
||||||
;; internal database. Later this audit log can be transferred to
|
|
||||||
;; an external storage and data cleared.
|
|
||||||
|
|
||||||
(s/def ::collector
|
(s/def ::collector
|
||||||
(s/keys :req [::wrk/executor ::db/pool]))
|
(s/keys :req [::wrk/executor ::db/pool]))
|
||||||
|
|
||||||
@ -133,15 +137,58 @@
|
|||||||
:else
|
:else
|
||||||
cfg))
|
cfg))
|
||||||
|
|
||||||
|
(defn prepare-event
|
||||||
|
[cfg mdata params result]
|
||||||
|
(let [resultm (meta result)
|
||||||
|
request (::http/request params)
|
||||||
|
profile-id (or (::profile-id resultm)
|
||||||
|
(:profile-id result)
|
||||||
|
(::rpc/profile-id params)
|
||||||
|
uuid/zero)
|
||||||
|
|
||||||
|
props (-> (or (::replace-props resultm)
|
||||||
|
(-> params
|
||||||
|
(merge (::props resultm))
|
||||||
|
(dissoc :profile-id)
|
||||||
|
(dissoc :type)))
|
||||||
|
|
||||||
|
(clean-props))]
|
||||||
|
|
||||||
|
{::type (or (::type resultm)
|
||||||
|
(::rpc/type cfg))
|
||||||
|
::name (or (::name resultm)
|
||||||
|
(::sv/name mdata))
|
||||||
|
::profile-id profile-id
|
||||||
|
::ip-addr (some-> request parse-client-ip)
|
||||||
|
::props props
|
||||||
|
|
||||||
|
;; NOTE: for batch-key lookup we need the params as-is
|
||||||
|
;; because the rpc api does not need to know the
|
||||||
|
;; audit/webhook specific object layout.
|
||||||
|
::rpc/params (dissoc params ::http/request)
|
||||||
|
|
||||||
|
::webhooks/batch-key
|
||||||
|
(or (::webhooks/batch-key mdata)
|
||||||
|
(::webhooks/batch-key resultm))
|
||||||
|
|
||||||
|
::webhooks/batch-timeout
|
||||||
|
(or (::webhooks/batch-timeout mdata)
|
||||||
|
(::webhooks/batch-timeout resultm))
|
||||||
|
|
||||||
|
::webhooks/event?
|
||||||
|
(or (::webhooks/event? mdata)
|
||||||
|
(::webhooks/event? resultm)
|
||||||
|
false)}))
|
||||||
|
|
||||||
(defn- handle-event!
|
(defn- handle-event!
|
||||||
[conn-or-pool event]
|
[conn-or-pool event]
|
||||||
(us/verify! ::event event)
|
(us/verify! ::event event)
|
||||||
(let [params {:id (uuid/next)
|
(let [params {:id (uuid/next)
|
||||||
:name (:name event)
|
:name (::name event)
|
||||||
:type (:type event)
|
:type (::type event)
|
||||||
:profile-id (:profile-id event)
|
:profile-id (::profile-id event)
|
||||||
:ip-addr (:ip-addr event)
|
:ip-addr (::ip-addr event)
|
||||||
:props (:props event)}]
|
:props (::props event)}]
|
||||||
|
|
||||||
(when (contains? cf/flags :audit-log)
|
(when (contains? cf/flags :audit-log)
|
||||||
;; NOTE: this operation may cause primary key conflicts on inserts
|
;; NOTE: this operation may cause primary key conflicts on inserts
|
||||||
@ -207,7 +254,7 @@
|
|||||||
(s/def ::tasks/uri ::us/string)
|
(s/def ::tasks/uri ::us/string)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::tasks/archive-task [_]
|
(defmethod ig/pre-init-spec ::tasks/archive-task [_]
|
||||||
(s/keys :req [::db/pool ::main/props ::http/client]))
|
(s/keys :req [::db/pool ::main/props ::http.client/client]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::tasks/archive
|
(defmethod ig/init-key ::tasks/archive
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
@ -231,7 +278,7 @@
|
|||||||
(if n
|
(if n
|
||||||
(do
|
(do
|
||||||
(px/sleep 100)
|
(px/sleep 100)
|
||||||
(recur (+ total n)))
|
(recur (+ total ^long n)))
|
||||||
(when (pos? total)
|
(when (pos? total)
|
||||||
(l/debug :hint "events archived" :total total)))))))))
|
(l/debug :hint "events archived" :total total)))))))))
|
||||||
|
|
||||||
@ -281,7 +328,7 @@
|
|||||||
:method :post
|
:method :post
|
||||||
:headers headers
|
:headers headers
|
||||||
:body body}
|
:body body}
|
||||||
resp (http/req! cfg params {:sync? true})]
|
resp (http.client/req! cfg params {:sync? true})]
|
||||||
(if (= (:status resp) 204)
|
(if (= (:status resp) 204)
|
||||||
true
|
true
|
||||||
(do
|
(do
|
||||||
|
|||||||
@ -62,6 +62,11 @@
|
|||||||
(dissoc ::s/problems ::s/value ::s/spec :hint)
|
(dissoc ::s/problems ::s/value ::s/spec :hint)
|
||||||
(pp/pprint-str :width 200))})))
|
(pp/pprint-str :width 200))})))
|
||||||
|
|
||||||
|
(defn error-record?
|
||||||
|
[{:keys [::l/level ::l/cause]}]
|
||||||
|
(and (= :error level)
|
||||||
|
(ex/exception? cause)))
|
||||||
|
|
||||||
(defn- handle-event
|
(defn- handle-event
|
||||||
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
|
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
|
||||||
(try
|
(try
|
||||||
@ -74,20 +79,16 @@
|
|||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
||||||
|
|
||||||
(defn error-record?
|
|
||||||
[{:keys [::l/level ::l/cause]}]
|
|
||||||
(and (= :error level)
|
|
||||||
(ex/exception? cause)))
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::reporter [_]
|
(defmethod ig/pre-init-spec ::reporter [_]
|
||||||
(s/keys :req [::db/pool]))
|
(s/keys :req [::db/pool]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::reporter
|
(defmethod ig/init-key ::reporter
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(let [input (sp/chan (sp/sliding-buffer 32) (filter error-record?))]
|
(let [input (sp/chan :buf (sp/sliding-buffer 32)
|
||||||
|
:xf (filter error-record?))]
|
||||||
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
||||||
(px/thread
|
|
||||||
{:name "penpot/database-reporter" :virtual true}
|
(px/thread {:name "penpot/database-reporter" :virtual true}
|
||||||
(l/info :hint "initializing database error persistence")
|
(l/info :hint "initializing database error persistence")
|
||||||
(try
|
(try
|
||||||
(loop []
|
(loop []
|
||||||
|
|||||||
@ -77,7 +77,8 @@
|
|||||||
{:name "penpot/mattermost-reporter"
|
{:name "penpot/mattermost-reporter"
|
||||||
:virtual true}
|
:virtual true}
|
||||||
(l/info :hint "initializing error reporter" :uri uri)
|
(l/info :hint "initializing error reporter" :uri uri)
|
||||||
(let [input (sp/chan (sp/sliding-buffer 128) (filter ldb/error-record?))]
|
(let [input (sp/chan :buf (sp/sliding-buffer 128)
|
||||||
|
:xf (filter ldb/error-record?))]
|
||||||
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
||||||
(try
|
(try
|
||||||
(loop []
|
(loop []
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
[app.db :as-alias db]
|
[app.db :as-alias db]
|
||||||
[app.email :as-alias email]
|
[app.email :as-alias email]
|
||||||
[app.http :as-alias http]
|
[app.http :as-alias http]
|
||||||
[app.http.access-token :as-alias actoken]
|
|
||||||
[app.http.assets :as-alias http.assets]
|
[app.http.assets :as-alias http.assets]
|
||||||
[app.http.awsns :as http.awsns]
|
[app.http.awsns :as http.awsns]
|
||||||
[app.http.client :as-alias http.client]
|
[app.http.client :as-alias http.client]
|
||||||
@ -37,7 +36,8 @@
|
|||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as-alias wrk]
|
[app.worker :as-alias wrk]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig])
|
[integrant.core :as ig]
|
||||||
|
[promesa.exec :as px])
|
||||||
(:gen-class))
|
(:gen-class))
|
||||||
|
|
||||||
(def default-metrics
|
(def default-metrics
|
||||||
@ -102,15 +102,15 @@
|
|||||||
::mdef/labels ["name"]
|
::mdef/labels ["name"]
|
||||||
::mdef/type :summary}
|
::mdef/type :summary}
|
||||||
|
|
||||||
:rpc-climit-queue-size
|
:rpc-climit-queue
|
||||||
{::mdef/name "penpot_rpc_climit_queue_size"
|
{::mdef/name "penpot_rpc_climit_queue"
|
||||||
::mdef/help "Current number of queued submissions on the CLIMIT."
|
::mdef/help "Current number of queued submissions."
|
||||||
::mdef/labels ["name"]
|
::mdef/labels ["name"]
|
||||||
::mdef/type :gauge}
|
::mdef/type :gauge}
|
||||||
|
|
||||||
:rpc-climit-concurrency
|
:rpc-climit-permits
|
||||||
{::mdef/name "penpot_rpc_climit_concurrency"
|
{::mdef/name "penpot_rpc_climit_permits"
|
||||||
::mdef/help "Current number of used concurrency capacity on the CLIMIT"
|
::mdef/help "Current number of available permits"
|
||||||
::mdef/labels ["name"]
|
::mdef/labels ["name"]
|
||||||
::mdef/type :gauge}
|
::mdef/type :gauge}
|
||||||
|
|
||||||
@ -174,10 +174,8 @@
|
|||||||
|
|
||||||
;; Default thread pool for IO operations
|
;; Default thread pool for IO operations
|
||||||
::wrk/executor
|
::wrk/executor
|
||||||
{::wrk/parallelism (cf/get :default-executor-parallelism 100)}
|
{::wrk/parallelism (cf/get :default-executor-parallelism
|
||||||
|
(+ 3 (* (px/get-available-processors) 3)))}
|
||||||
::wrk/scheduled-executor
|
|
||||||
{::wrk/parallelism (cf/get :scheduled-executor-parallelism 20)}
|
|
||||||
|
|
||||||
::wrk/monitor
|
::wrk/monitor
|
||||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||||
@ -194,17 +192,16 @@
|
|||||||
{::mtx/metrics (ig/ref ::mtx/metrics)}
|
{::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||||
|
|
||||||
::rds/redis
|
::rds/redis
|
||||||
{::rds/uri (cf/get :redis-uri)
|
{::rds/uri (cf/get :redis-uri)
|
||||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||||
|
::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
|
|
||||||
::mbus/msgbus
|
::mbus/msgbus
|
||||||
{:backend (cf/get :msgbus-backend :redis)
|
{::wrk/executor (ig/ref ::wrk/executor)
|
||||||
:executor (ig/ref ::wrk/executor)
|
::rds/redis (ig/ref ::rds/redis)}
|
||||||
:redis (ig/ref ::rds/redis)}
|
|
||||||
|
|
||||||
:app.storage.tmp/cleaner
|
:app.storage.tmp/cleaner
|
||||||
{::wrk/executor (ig/ref ::wrk/executor)
|
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
|
|
||||||
|
|
||||||
::sto/gc-deleted-task
|
::sto/gc-deleted-task
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
@ -217,14 +214,7 @@
|
|||||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
|
|
||||||
::session/manager
|
::session/manager
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
|
||||||
::props (ig/ref :app.setup/props)}
|
|
||||||
|
|
||||||
::actoken/manager
|
|
||||||
{::db/pool (ig/ref ::db/pool)
|
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
|
||||||
::props (ig/ref :app.setup/props)}
|
|
||||||
|
|
||||||
::session.tasks/gc
|
::session.tasks/gc
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
@ -239,8 +229,7 @@
|
|||||||
{::http/port (cf/get :http-server-port)
|
{::http/port (cf/get :http-server-port)
|
||||||
::http/host (cf/get :http-server-host)
|
::http/host (cf/get :http-server-host)
|
||||||
::http/router (ig/ref ::http/router)
|
::http/router (ig/ref ::http/router)
|
||||||
::http/metrics (ig/ref ::mtx/metrics)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
::http/executor (ig/ref ::wrk/executor)
|
|
||||||
::http/io-threads (cf/get :http-server-io-threads)
|
::http/io-threads (cf/get :http-server-io-threads)
|
||||||
::http/max-body-size (cf/get :http-server-max-body-size)
|
::http/max-body-size (cf/get :http-server-max-body-size)
|
||||||
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
||||||
@ -275,7 +264,6 @@
|
|||||||
{::http.client/client (ig/ref ::http.client/client)
|
{::http.client/client (ig/ref ::http.client/client)
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::props (ig/ref :app.setup/props)
|
::props (ig/ref :app.setup/props)
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
|
||||||
::oidc/providers {:google (ig/ref ::oidc.providers/google)
|
::oidc/providers {:google (ig/ref ::oidc.providers/google)
|
||||||
:github (ig/ref ::oidc.providers/github)
|
:github (ig/ref ::oidc.providers/github)
|
||||||
:gitlab (ig/ref ::oidc.providers/gitlab)
|
:gitlab (ig/ref ::oidc.providers/gitlab)
|
||||||
@ -284,8 +272,6 @@
|
|||||||
|
|
||||||
:app.http/router
|
:app.http/router
|
||||||
{::session/manager (ig/ref ::session/manager)
|
{::session/manager (ig/ref ::session/manager)
|
||||||
::actoken/manager (ig/ref ::actoken/manager)
|
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::rpc/routes (ig/ref ::rpc/routes)
|
::rpc/routes (ig/ref ::rpc/routes)
|
||||||
::rpc.doc/routes (ig/ref ::rpc.doc/routes)
|
::rpc.doc/routes (ig/ref ::rpc.doc/routes)
|
||||||
@ -302,10 +288,10 @@
|
|||||||
::wrk/executor (ig/ref ::wrk/executor)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
::session/manager (ig/ref ::session/manager)}
|
::session/manager (ig/ref ::session/manager)}
|
||||||
|
|
||||||
:app.http.websocket/routes
|
::http.ws/routes
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||||
::mbus/msgbus (ig/ref :app.msgbus/msgbus)
|
::mbus/msgbus (ig/ref ::mbus/msgbus)
|
||||||
::session/manager (ig/ref ::session/manager)}
|
::session/manager (ig/ref ::session/manager)}
|
||||||
|
|
||||||
:app.http.assets/routes
|
:app.http.assets/routes
|
||||||
@ -320,8 +306,7 @@
|
|||||||
::wrk/executor (ig/ref ::wrk/executor)}
|
::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
|
|
||||||
:app.rpc/rlimit
|
:app.rpc/rlimit
|
||||||
{::wrk/executor (ig/ref ::wrk/executor)
|
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||||
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
|
|
||||||
|
|
||||||
:app.rpc/methods
|
:app.rpc/methods
|
||||||
{::http.client/client (ig/ref ::http.client/client)
|
{::http.client/client (ig/ref ::http.client/client)
|
||||||
@ -351,7 +336,6 @@
|
|||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::wrk/executor (ig/ref ::wrk/executor)
|
::wrk/executor (ig/ref ::wrk/executor)
|
||||||
::session/manager (ig/ref ::session/manager)
|
::session/manager (ig/ref ::session/manager)
|
||||||
::actoken/manager (ig/ref ::actoken/manager)
|
|
||||||
::props (ig/ref :app.setup/props)}
|
::props (ig/ref :app.setup/props)}
|
||||||
|
|
||||||
::wrk/registry
|
::wrk/registry
|
||||||
@ -467,8 +451,7 @@
|
|||||||
|
|
||||||
(def worker-config
|
(def worker-config
|
||||||
{::wrk/cron
|
{::wrk/cron
|
||||||
{::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)
|
{::wrk/registry (ig/ref ::wrk/registry)
|
||||||
::wrk/registry (ig/ref ::wrk/registry)
|
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::wrk/entries
|
::wrk/entries
|
||||||
[{:cron #app/cron "0 0 * * * ?" ;; hourly
|
[{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
[app.storage :as-alias sto]
|
[app.storage :as-alias sto]
|
||||||
[app.storage.tmp :as tmp]
|
[app.storage.tmp :as tmp]
|
||||||
[app.util.svg :as svg]
|
[app.util.svg :as svg]
|
||||||
|
[app.util.time :as dt]
|
||||||
[buddy.core.bytes :as bb]
|
[buddy.core.bytes :as bb]
|
||||||
[buddy.core.codecs :as bc]
|
[buddy.core.codecs :as bc]
|
||||||
[clojure.java.shell :as sh]
|
[clojure.java.shell :as sh]
|
||||||
@ -168,7 +169,7 @@
|
|||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :invalid-svg-file
|
:code :invalid-svg-file
|
||||||
:hint "uploaded svg does not provides dimensions"))
|
:hint "uploaded svg does not provides dimensions"))
|
||||||
(merge input info))
|
(merge input info {:ts (dt/now)}))
|
||||||
|
|
||||||
(let [instance (Info. (str path))
|
(let [instance (Info. (str path))
|
||||||
mtype' (.getProperty instance "Mime type")]
|
mtype' (.getProperty instance "Mime type")]
|
||||||
@ -183,7 +184,8 @@
|
|||||||
;; any frame.
|
;; any frame.
|
||||||
(assoc input
|
(assoc input
|
||||||
:width (.getPageWidth instance)
|
:width (.getPageWidth instance)
|
||||||
:height (.getPageHeight instance))))))
|
:height (.getPageHeight instance)
|
||||||
|
:ts (dt/now))))))
|
||||||
|
|
||||||
(defmethod process-error org.im4java.core.InfoException
|
(defmethod process-error org.im4java.core.InfoException
|
||||||
[error]
|
[error]
|
||||||
|
|||||||
@ -8,20 +8,18 @@
|
|||||||
"The msgbus abstraction implemented using redis as underlying backend."
|
"The msgbus abstraction implemented using redis as underlying backend."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.redis :as redis]
|
[app.redis :as rds]
|
||||||
[app.util.async :as aa]
|
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.core.async :as a]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[promesa.exec :as px]))
|
[promesa.exec :as px]
|
||||||
|
[promesa.exec.csp :as sp]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
@ -34,132 +32,116 @@
|
|||||||
(def ^:private xform-prefix-topic
|
(def ^:private xform-prefix-topic
|
||||||
(map (fn [obj] (update obj :topic prefix-topic))))
|
(map (fn [obj] (update obj :topic prefix-topic))))
|
||||||
|
|
||||||
(declare ^:private redis-connect)
|
(declare ^:private redis-pub!)
|
||||||
(declare ^:private redis-disconnect)
|
(declare ^:private redis-sub!)
|
||||||
(declare ^:private redis-pub)
|
(declare ^:private redis-unsub!)
|
||||||
(declare ^:private redis-sub)
|
|
||||||
(declare ^:private redis-unsub)
|
|
||||||
(declare ^:private start-io-loop!)
|
(declare ^:private start-io-loop!)
|
||||||
(declare ^:private subscribe-to-topics)
|
(declare ^:private subscribe-to-topics)
|
||||||
(declare ^:private unsubscribe-channels)
|
(declare ^:private unsubscribe-channels)
|
||||||
|
|
||||||
(defmethod ig/prep-key ::msgbus
|
(s/def ::cmd-ch sp/chan?)
|
||||||
[_ cfg]
|
(s/def ::rcv-ch sp/chan?)
|
||||||
(merge {:buffer-size 128
|
(s/def ::pub-ch sp/chan?)
|
||||||
:timeout (dt/duration {:seconds 30})}
|
|
||||||
(d/without-nils cfg)))
|
|
||||||
|
|
||||||
(s/def ::cmd-ch ::aa/channel)
|
|
||||||
(s/def ::rcv-ch ::aa/channel)
|
|
||||||
(s/def ::pub-ch ::aa/channel)
|
|
||||||
(s/def ::state ::us/agent)
|
(s/def ::state ::us/agent)
|
||||||
(s/def ::pconn ::redis/connection-holder)
|
(s/def ::pconn ::rds/connection-holder)
|
||||||
(s/def ::sconn ::redis/connection-holder)
|
(s/def ::sconn ::rds/connection-holder)
|
||||||
(s/def ::msgbus
|
(s/def ::msgbus
|
||||||
(s/keys :req [::cmd-ch ::rcv-ch ::pub-ch ::state ::pconn ::sconn ::wrk/executor]))
|
(s/keys :req [::cmd-ch ::rcv-ch ::pub-ch ::state ::pconn ::sconn ::wrk/executor]))
|
||||||
|
|
||||||
(s/def ::buffer-size ::us/integer)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::msgbus [_]
|
(defmethod ig/pre-init-spec ::msgbus [_]
|
||||||
(s/keys :req-un [::buffer-size ::redis/timeout ::redis/redis ::wrk/executor]))
|
(s/keys :req [::rds/redis ::wrk/executor]))
|
||||||
|
|
||||||
|
(defmethod ig/prep-key ::msgbus
|
||||||
|
[_ cfg]
|
||||||
|
(-> cfg
|
||||||
|
(assoc ::buffer-size 128)
|
||||||
|
(assoc ::timeout (dt/duration {:seconds 30}))))
|
||||||
|
|
||||||
(defmethod ig/init-key ::msgbus
|
(defmethod ig/init-key ::msgbus
|
||||||
[_ {:keys [buffer-size executor] :as cfg}]
|
[_ {:keys [::buffer-size ::wrk/executor ::timeout ::rds/redis] :as cfg}]
|
||||||
(l/info :hint "initialize msgbus" :buffer-size buffer-size)
|
(l/info :hint "initialize msgbus" :buffer-size buffer-size)
|
||||||
(let [cmd-ch (a/chan buffer-size)
|
(let [cmd-ch (sp/chan :buf buffer-size)
|
||||||
rcv-ch (a/chan (a/dropping-buffer buffer-size))
|
rcv-ch (sp/chan :buf (sp/dropping-buffer buffer-size))
|
||||||
pub-ch (a/chan (a/dropping-buffer buffer-size) xform-prefix-topic)
|
pub-ch (sp/chan :buf (sp/dropping-buffer buffer-size)
|
||||||
|
:xf xform-prefix-topic)
|
||||||
state (agent {})
|
state (agent {})
|
||||||
msgbus (-> (redis-connect cfg)
|
|
||||||
|
pconn (rds/connect redis :timeout timeout)
|
||||||
|
sconn (rds/connect redis :type :pubsub :timeout timeout)
|
||||||
|
msgbus (-> cfg
|
||||||
|
(assoc ::pconn pconn)
|
||||||
|
(assoc ::sconn sconn)
|
||||||
(assoc ::cmd-ch cmd-ch)
|
(assoc ::cmd-ch cmd-ch)
|
||||||
(assoc ::rcv-ch rcv-ch)
|
(assoc ::rcv-ch rcv-ch)
|
||||||
(assoc ::pub-ch pub-ch)
|
(assoc ::pub-ch pub-ch)
|
||||||
(assoc ::state state)
|
(assoc ::state state)
|
||||||
(assoc ::wrk/executor executor))]
|
(assoc ::wrk/executor executor))]
|
||||||
|
|
||||||
(us/verify! ::msgbus msgbus)
|
|
||||||
|
|
||||||
(set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true))
|
(set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true))
|
||||||
(set-error-mode! state :continue)
|
(set-error-mode! state :continue)
|
||||||
(start-io-loop! msgbus)
|
|
||||||
|
|
||||||
msgbus))
|
(assoc msgbus ::io-thr (start-io-loop! msgbus))))
|
||||||
|
|
||||||
(defn sub!
|
|
||||||
[{:keys [::state ::wrk/executor] :as cfg} & {:keys [topic topics chan]}]
|
|
||||||
(let [done-ch (a/chan)
|
|
||||||
topics (into [] (map prefix-topic) (if topic [topic] topics))]
|
|
||||||
(l/debug :hint "subscribe" :topics topics)
|
|
||||||
(send-via executor state subscribe-to-topics cfg topics chan done-ch)
|
|
||||||
done-ch))
|
|
||||||
|
|
||||||
(defn pub!
|
|
||||||
[{::keys [pub-ch]} & {:as params}]
|
|
||||||
(a/go
|
|
||||||
(a/>! pub-ch params)))
|
|
||||||
|
|
||||||
(defn purge!
|
|
||||||
[{:keys [::state ::wrk/executor] :as msgbus} chans]
|
|
||||||
(l/trace :hint "purge" :chans (count chans))
|
|
||||||
(let [done-ch (a/chan)]
|
|
||||||
(send-via executor state unsubscribe-channels msgbus chans done-ch)
|
|
||||||
done-ch))
|
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::msgbus
|
(defmethod ig/halt-key! ::msgbus
|
||||||
[_ msgbus]
|
[_ msgbus]
|
||||||
(redis-disconnect msgbus)
|
(px/interrupt! (::io-thr msgbus))
|
||||||
(a/close! (::cmd-ch msgbus))
|
(sp/close! (::cmd-ch msgbus))
|
||||||
(a/close! (::rcv-ch msgbus))
|
(sp/close! (::rcv-ch msgbus))
|
||||||
(a/close! (::pub-ch msgbus)))
|
(sp/close! (::pub-ch msgbus))
|
||||||
|
(d/close! (::pconn msgbus))
|
||||||
|
(d/close! (::sconn msgbus)))
|
||||||
|
|
||||||
|
(defn sub!
|
||||||
|
[{:keys [::state ::wrk/executor] :as cfg} & {:keys [topic topics chan]}]
|
||||||
|
(let [topics (into [] (map prefix-topic) (if topic [topic] topics))]
|
||||||
|
(l/debug :hint "subscribe" :topics topics :chan (hash chan))
|
||||||
|
(send-via executor state subscribe-to-topics cfg topics chan)
|
||||||
|
nil))
|
||||||
|
|
||||||
|
(defn pub!
|
||||||
|
[{::keys [pub-ch]} & {:as params}]
|
||||||
|
(sp/put! pub-ch params))
|
||||||
|
|
||||||
|
(defn purge!
|
||||||
|
[{:keys [::state ::wrk/executor] :as msgbus} chans]
|
||||||
|
(l/debug :hint "purge" :chans (count chans))
|
||||||
|
(send-via executor state unsubscribe-channels msgbus chans)
|
||||||
|
nil)
|
||||||
|
|
||||||
;; --- IMPL
|
;; --- IMPL
|
||||||
|
|
||||||
(defn- redis-connect
|
|
||||||
[{:keys [timeout redis] :as cfg}]
|
|
||||||
(let [pconn (redis/connect redis :timeout timeout)
|
|
||||||
sconn (redis/connect redis :type :pubsub :timeout timeout)]
|
|
||||||
{::pconn pconn
|
|
||||||
::sconn sconn}))
|
|
||||||
|
|
||||||
(defn- redis-disconnect
|
|
||||||
[{:keys [::pconn ::sconn] :as cfg}]
|
|
||||||
(d/close! pconn)
|
|
||||||
(d/close! sconn))
|
|
||||||
|
|
||||||
(defn- conj-subscription
|
(defn- conj-subscription
|
||||||
"A low level function that is responsible to create on-demand
|
"A low level function that is responsible to create on-demand
|
||||||
subscriptions on redis. It reuses the same subscription if it is
|
subscriptions on redis. It reuses the same subscription if it is
|
||||||
already established. Intended to be executed in agent."
|
already established."
|
||||||
[nsubs cfg topic chan]
|
[nsubs cfg topic chan]
|
||||||
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
|
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
|
||||||
(when (= 1 (count nsubs))
|
(when (= 1 (count nsubs))
|
||||||
(l/trace :hint "open subscription" :topic topic ::l/sync? true)
|
(l/trace :hint "open subscription" :topic topic ::l/sync? true)
|
||||||
(redis-sub cfg topic))
|
(redis-sub! cfg topic))
|
||||||
nsubs))
|
nsubs))
|
||||||
|
|
||||||
(defn- disj-subscription
|
(defn- disj-subscription
|
||||||
"A low level function responsible on removing subscriptions. The
|
"A low level function responsible on removing subscriptions. The
|
||||||
subscription is truly removed from redis once no single local
|
subscription is truly removed from redis once no single local
|
||||||
subscription is look for it. Intended to be executed in agent."
|
subscription is look for it."
|
||||||
[nsubs cfg topic chan]
|
[nsubs cfg topic chan]
|
||||||
(let [nsubs (disj nsubs chan)]
|
(let [nsubs (disj nsubs chan)]
|
||||||
(when (empty? nsubs)
|
(when (empty? nsubs)
|
||||||
(l/trace :hint "close subscription" :topic topic ::l/sync? true)
|
(l/trace :hint "close subscription" :topic topic ::l/sync? true)
|
||||||
(redis-unsub cfg topic))
|
(redis-unsub! cfg topic))
|
||||||
nsubs))
|
nsubs))
|
||||||
|
|
||||||
(defn- subscribe-to-topics
|
(defn- subscribe-to-topics
|
||||||
"Function responsible to attach local subscription to the
|
"Function responsible to attach local subscription to the state."
|
||||||
state. Intended to be used in agent."
|
[state cfg topics chan]
|
||||||
[state cfg topics chan done-ch]
|
(let [state (update state :chans assoc chan topics)]
|
||||||
(aa/with-closing done-ch
|
(reduce (fn [state topic]
|
||||||
(let [state (update state :chans assoc chan topics)]
|
(update-in state [:topics topic] conj-subscription cfg topic chan))
|
||||||
(reduce (fn [state topic]
|
state
|
||||||
(update-in state [:topics topic] conj-subscription cfg topic chan))
|
topics)))
|
||||||
state
|
|
||||||
topics))))
|
|
||||||
|
|
||||||
(defn- unsubscribe-single-channel
|
(defn- unsubscribe-channel
|
||||||
"Auxiliary function responsible on removing a single local
|
"Auxiliary function responsible on removing a single local
|
||||||
subscription from the state."
|
subscription from the state."
|
||||||
[state cfg chan]
|
[state cfg chan]
|
||||||
@ -174,87 +156,113 @@
|
|||||||
"Function responsible from detach from state a seq of channels,
|
"Function responsible from detach from state a seq of channels,
|
||||||
useful when client disconnects or in-bulk unsubscribe
|
useful when client disconnects or in-bulk unsubscribe
|
||||||
operations. Intended to be executed in agent."
|
operations. Intended to be executed in agent."
|
||||||
[state cfg channels done-ch]
|
[state cfg channels]
|
||||||
(aa/with-closing done-ch
|
(reduce #(unsubscribe-channel %1 cfg %2) state channels))
|
||||||
(reduce #(unsubscribe-single-channel %1 cfg %2) state channels)))
|
|
||||||
|
|
||||||
(defn- create-listener
|
(defn- create-listener
|
||||||
[rcv-ch]
|
[rcv-ch]
|
||||||
(redis/pubsub-listener
|
(rds/pubsub-listener
|
||||||
:on-message (fn [_ topic message]
|
:on-message (fn [_ topic message]
|
||||||
;; There are no back pressure, so we use a slidding
|
;; There are no back pressure, so we use a slidding
|
||||||
;; buffer for cases when the pubsub broker sends
|
;; buffer for cases when the pubsub broker sends
|
||||||
;; more messages that we can process.
|
;; more messages that we can process.
|
||||||
(let [val {:topic topic :message (t/decode message)}]
|
(let [val {:topic topic :message (t/decode message)}]
|
||||||
(when-not (a/offer! rcv-ch val)
|
(when-not (sp/offer! rcv-ch val)
|
||||||
(l/warn :msg "dropping message on subscription loop"))))))
|
(l/warn :msg "dropping message on subscription loop"))))))
|
||||||
|
|
||||||
|
(defn- process-input!
|
||||||
|
[{:keys [::state ::wrk/executor] :as cfg} topic message]
|
||||||
|
(let [chans (get-in @state [:topics topic])]
|
||||||
|
(when-let [closed (loop [chans (seq chans)
|
||||||
|
closed #{}]
|
||||||
|
(if-let [ch (first chans)]
|
||||||
|
(if (sp/put! ch message)
|
||||||
|
(recur (rest chans) closed)
|
||||||
|
(recur (rest chans) (conj closed ch)))
|
||||||
|
(seq closed)))]
|
||||||
|
(send-via executor state unsubscribe-channels cfg closed))))
|
||||||
|
|
||||||
|
|
||||||
(defn start-io-loop!
|
(defn start-io-loop!
|
||||||
[{:keys [::sconn ::rcv-ch ::pub-ch ::state ::wrk/executor] :as cfg}]
|
[{:keys [::sconn ::rcv-ch ::pub-ch ::state ::wrk/executor] :as cfg}]
|
||||||
(redis/add-listener! sconn (create-listener rcv-ch))
|
(rds/add-listener! sconn (create-listener rcv-ch))
|
||||||
(letfn [(send-to-topic [topic message]
|
|
||||||
(a/go-loop [chans (seq (get-in @state [:topics topic]))
|
|
||||||
closed #{}]
|
|
||||||
(if-let [ch (first chans)]
|
|
||||||
(if (a/>! ch message)
|
|
||||||
(recur (rest chans) closed)
|
|
||||||
(recur (rest chans) (conj closed ch)))
|
|
||||||
(seq closed))))
|
|
||||||
|
|
||||||
(process-incoming [{:keys [topic message]}]
|
(px/thread
|
||||||
(a/go
|
{:name "penpot/msgbus/io-loop"
|
||||||
(when-let [closed (a/<! (send-to-topic topic message))]
|
:virtual true}
|
||||||
(send-via executor state unsubscribe-channels cfg closed nil))))
|
(try
|
||||||
]
|
|
||||||
(px/thread
|
|
||||||
{:name "penpot/msgbus-io-loop"}
|
|
||||||
(loop []
|
(loop []
|
||||||
(let [[val port] (a/alts!! [pub-ch rcv-ch])]
|
(let [timeout-ch (sp/timeout-chan 1000)
|
||||||
|
[val port] (sp/alts! [timeout-ch pub-ch rcv-ch])]
|
||||||
(cond
|
(cond
|
||||||
(nil? val)
|
(identical? port timeout-ch)
|
||||||
(do
|
(let [closed (->> (:chans @state)
|
||||||
(l/trace :hint "stopping io-loop, nil received")
|
(map key)
|
||||||
(send-via executor state (fn [state]
|
(filter sp/closed?))]
|
||||||
(->> (vals state)
|
(when (seq closed)
|
||||||
(mapcat identity)
|
(send-via executor state unsubscribe-channels cfg closed)
|
||||||
(filter some?)
|
(l/debug :hint "proactively purge channels" :count (count closed)))
|
||||||
(run! a/close!))
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
(= port rcv-ch)
|
|
||||||
(do
|
|
||||||
(a/<!! (process-incoming val))
|
|
||||||
(recur))
|
(recur))
|
||||||
|
|
||||||
(= port pub-ch)
|
(nil? val)
|
||||||
(let [result (a/<!! (redis-pub cfg val))]
|
(throw (InterruptedException. "internally interrupted"))
|
||||||
(when (ex/exception? result)
|
|
||||||
(l/error :hint "unexpected error on publishing"
|
|
||||||
:message val
|
|
||||||
:cause result))
|
|
||||||
(recur))))))))
|
|
||||||
|
|
||||||
(defn- redis-pub
|
(identical? port rcv-ch)
|
||||||
|
(let [{:keys [topic message]} val]
|
||||||
|
(process-input! cfg topic message)
|
||||||
|
(recur))
|
||||||
|
|
||||||
|
(identical? port pub-ch)
|
||||||
|
(do
|
||||||
|
(redis-pub! cfg val)
|
||||||
|
(recur)))))
|
||||||
|
|
||||||
|
(catch InterruptedException _
|
||||||
|
(l/trace :hint "io-loop thread interrumpted"))
|
||||||
|
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/error :hint "unexpected exception on io-loop thread"
|
||||||
|
:cause cause))
|
||||||
|
(finally
|
||||||
|
(l/trace :hint "clearing io-loop state")
|
||||||
|
(when-let [chans (:chans @state)]
|
||||||
|
(run! sp/close! (keys chans)))
|
||||||
|
|
||||||
|
(l/debug :hint "io-loop thread terminated")))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- redis-pub!
|
||||||
"Publish a message to the redis server. Asynchronous operation,
|
"Publish a message to the redis server. Asynchronous operation,
|
||||||
intended to be used in core.async go blocks."
|
intended to be used in core.async go blocks."
|
||||||
[{:keys [::pconn] :as cfg} {:keys [topic message]}]
|
[{:keys [::pconn] :as cfg} {:keys [topic message]}]
|
||||||
(let [message (t/encode message)
|
(try
|
||||||
res (a/chan 1)]
|
(p/await! (rds/publish! pconn topic (t/encode message)))
|
||||||
(-> (redis/publish! pconn topic message)
|
(catch InterruptedException cause
|
||||||
(p/finally (fn [_ cause]
|
(throw cause))
|
||||||
(when (and cause (redis/open? pconn))
|
(catch Throwable cause
|
||||||
(a/offer! res cause))
|
(l/error :hint "unexpected error on publishing"
|
||||||
(a/close! res))))
|
:message message
|
||||||
res))
|
:cause cause))))
|
||||||
|
|
||||||
(defn redis-sub
|
(defn- redis-sub!
|
||||||
"Create redis subscription. Blocking operation, intended to be used
|
"Create redis subscription. Blocking operation, intended to be used
|
||||||
inside an agent."
|
inside an agent."
|
||||||
[{:keys [::sconn] :as cfg} topic]
|
[{:keys [::sconn] :as cfg} topic]
|
||||||
(redis/subscribe! sconn topic))
|
(try
|
||||||
|
(rds/subscribe! sconn topic)
|
||||||
|
(catch InterruptedException cause
|
||||||
|
(throw cause))
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/trace :hint "exception on subscribing" :topic topic :cause cause))))
|
||||||
|
|
||||||
(defn redis-unsub
|
(defn- redis-unsub!
|
||||||
"Removes redis subscription. Blocking operation, intended to be used
|
"Removes redis subscription. Blocking operation, intended to be used
|
||||||
inside an agent."
|
inside an agent."
|
||||||
[{:keys [::sconn] :as cfg} topic]
|
[{:keys [::sconn] :as cfg} topic]
|
||||||
(redis/unsubscribe! sconn topic))
|
(try
|
||||||
|
(rds/unsubscribe! sconn topic)
|
||||||
|
(catch InterruptedException cause
|
||||||
|
(throw cause))
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/trace :hint "exception on unsubscribing" :topic topic :cause cause))))
|
||||||
|
|
||||||
|
|||||||
@ -8,17 +8,21 @@
|
|||||||
"The msgbus abstraction implemented using redis as underlying backend."
|
"The msgbus abstraction implemented using redis as underlying backend."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.redis.script :as-alias rscript]
|
[app.redis.script :as-alias rscript]
|
||||||
|
[app.util.cache :as cache]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as-alias wrk]
|
||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p])
|
[promesa.core :as p]
|
||||||
|
[promesa.exec :as px])
|
||||||
(:import
|
(:import
|
||||||
clojure.lang.IDeref
|
clojure.lang.IDeref
|
||||||
clojure.lang.MapEntry
|
clojure.lang.MapEntry
|
||||||
@ -87,7 +91,7 @@
|
|||||||
(s/def ::connect? ::us/boolean)
|
(s/def ::connect? ::us/boolean)
|
||||||
(s/def ::io-threads ::us/integer)
|
(s/def ::io-threads ::us/integer)
|
||||||
(s/def ::worker-threads ::us/integer)
|
(s/def ::worker-threads ::us/integer)
|
||||||
(s/def ::cache #(instance? clojure.lang.Atom %))
|
(s/def ::cache some?)
|
||||||
|
|
||||||
(s/def ::redis
|
(s/def ::redis
|
||||||
(s/keys :req [::resources
|
(s/keys :req [::resources
|
||||||
@ -99,11 +103,11 @@
|
|||||||
|
|
||||||
(defmethod ig/prep-key ::redis
|
(defmethod ig/prep-key ::redis
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(let [runtime (Runtime/getRuntime)
|
(let [cpus (px/get-available-processors)
|
||||||
cpus (.availableProcessors ^Runtime runtime)]
|
threads (max 1 (int (* cpus 0.2)))]
|
||||||
(merge {::timeout (dt/duration "10s")
|
(merge {::timeout (dt/duration "10s")
|
||||||
::io-threads (max 3 cpus)
|
::io-threads (max 3 threads)
|
||||||
::worker-threads (max 3 cpus)}
|
::worker-threads (max 3 threads)}
|
||||||
(d/without-nils cfg))))
|
(d/without-nils cfg))))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::redis [_]
|
(defmethod ig/pre-init-spec ::redis [_]
|
||||||
@ -129,6 +133,15 @@
|
|||||||
(def string-codec
|
(def string-codec
|
||||||
(RedisCodec/of StringCodec/UTF8 StringCodec/UTF8))
|
(RedisCodec/of StringCodec/UTF8 StringCodec/UTF8))
|
||||||
|
|
||||||
|
(defn- create-cache
|
||||||
|
[{:keys [::wrk/executor] :as cfg}]
|
||||||
|
(letfn [(on-remove [key val cause]
|
||||||
|
(l/trace :hint "evict connection (cache)" :key key :reason cause)
|
||||||
|
(some-> val d/close!))]
|
||||||
|
(cache/create :executor executor
|
||||||
|
:on-remove on-remove
|
||||||
|
:keepalive "5m")))
|
||||||
|
|
||||||
(defn- initialize-resources
|
(defn- initialize-resources
|
||||||
"Initialize redis connection resources"
|
"Initialize redis connection resources"
|
||||||
[{:keys [::uri ::io-threads ::worker-threads ::connect?] :as cfg}]
|
[{:keys [::uri ::io-threads ::worker-threads ::connect?] :as cfg}]
|
||||||
@ -145,19 +158,21 @@
|
|||||||
(timer ^Timer timer)
|
(timer ^Timer timer)
|
||||||
(build))
|
(build))
|
||||||
|
|
||||||
redis-uri (RedisURI/create ^String uri)]
|
redis-uri (RedisURI/create ^String uri)
|
||||||
|
cfg (-> cfg
|
||||||
|
(assoc ::resources resources)
|
||||||
|
(assoc ::timer timer)
|
||||||
|
(assoc ::redis-uri redis-uri))]
|
||||||
|
|
||||||
(-> cfg
|
(assoc cfg ::cache (create-cache cfg))))
|
||||||
(assoc ::resources resources)
|
|
||||||
(assoc ::timer timer)
|
|
||||||
(assoc ::cache (atom {}))
|
|
||||||
(assoc ::redis-uri redis-uri))))
|
|
||||||
|
|
||||||
(defn- shutdown-resources
|
(defn- shutdown-resources
|
||||||
[{:keys [::resources ::cache ::timer]}]
|
[{:keys [::resources ::cache ::timer]}]
|
||||||
(run! d/close! (vals @cache))
|
(cache/invalidate-all! cache)
|
||||||
|
|
||||||
(when resources
|
(when resources
|
||||||
(.shutdown ^ClientResources resources))
|
(.shutdown ^ClientResources resources))
|
||||||
|
|
||||||
(when timer
|
(when timer
|
||||||
(.stop ^Timer timer)))
|
(.stop ^Timer timer)))
|
||||||
|
|
||||||
@ -173,6 +188,7 @@
|
|||||||
:default (.connect ^RedisClient client ^RedisCodec codec)
|
:default (.connect ^RedisClient client ^RedisCodec codec)
|
||||||
:pubsub (.connectPubSub ^RedisClient client ^RedisCodec codec))]
|
:pubsub (.connectPubSub ^RedisClient client ^RedisCodec codec))]
|
||||||
|
|
||||||
|
(l/trc :hint "connect" :hid (hash client))
|
||||||
(.setTimeout ^StatefulConnection conn ^Duration timeout)
|
(.setTimeout ^StatefulConnection conn ^Duration timeout)
|
||||||
(reify
|
(reify
|
||||||
IDeref
|
IDeref
|
||||||
@ -180,8 +196,9 @@
|
|||||||
|
|
||||||
AutoCloseable
|
AutoCloseable
|
||||||
(close [_]
|
(close [_]
|
||||||
(.close ^StatefulConnection conn)
|
(ex/ignoring (.close ^StatefulConnection conn))
|
||||||
(.shutdown ^RedisClient client)))))
|
(ex/ignoring (.shutdown ^RedisClient client))
|
||||||
|
(l/trc :hint "disconnect" :hid (hash client))))))
|
||||||
|
|
||||||
(defn connect
|
(defn connect
|
||||||
[state & {:as opts}]
|
[state & {:as opts}]
|
||||||
@ -194,15 +211,10 @@
|
|||||||
(defn get-or-connect
|
(defn get-or-connect
|
||||||
[{:keys [::cache] :as state} key options]
|
[{:keys [::cache] :as state} key options]
|
||||||
(us/assert! ::redis state)
|
(us/assert! ::redis state)
|
||||||
(-> state
|
(let [connection (cache/get cache key (fn [_] (connect* state options)))]
|
||||||
(assoc ::connection
|
(-> state
|
||||||
(or (get @cache key)
|
(dissoc ::cache)
|
||||||
(-> (swap! cache (fn [cache]
|
(assoc ::connection connection))))
|
||||||
(when-let [prev (get cache key)]
|
|
||||||
(d/close! prev))
|
|
||||||
(assoc cache key (connect* state options))))
|
|
||||||
(get key))))
|
|
||||||
(dissoc ::cache)))
|
|
||||||
|
|
||||||
(defn add-listener!
|
(defn add-listener!
|
||||||
[{:keys [::connection] :as conn} listener]
|
[{:keys [::connection] :as conn} listener]
|
||||||
@ -344,7 +356,7 @@
|
|||||||
(do
|
(do
|
||||||
(l/error :hint "no script found" :name sname :cause cause)
|
(l/error :hint "no script found" :name sname :cause cause)
|
||||||
(->> (load-script)
|
(->> (load-script)
|
||||||
(p/mapcat eval-script)))
|
(p/mcat eval-script)))
|
||||||
(if-let [on-error (::rscript/on-error script)]
|
(if-let [on-error (::rscript/on-error script)]
|
||||||
(on-error cause)
|
(on-error cause)
|
||||||
(p/rejected cause))))
|
(p/rejected cause))))
|
||||||
@ -375,15 +387,16 @@
|
|||||||
(load-script []
|
(load-script []
|
||||||
(l/trace :hint "load script" :name sname)
|
(l/trace :hint "load script" :name sname)
|
||||||
(->> (.scriptLoad ^RedisScriptingAsyncCommands cmd
|
(->> (.scriptLoad ^RedisScriptingAsyncCommands cmd
|
||||||
^String (read-script))
|
^String (read-script))
|
||||||
(p/map (fn [sha]
|
(p/fmap (fn [sha]
|
||||||
(swap! scripts-cache assoc sname sha)
|
(swap! scripts-cache assoc sname sha)
|
||||||
sha))))]
|
sha))))]
|
||||||
|
|
||||||
(if-let [sha (get @scripts-cache sname)]
|
(p/await!
|
||||||
(eval-script sha)
|
(if-let [sha (get @scripts-cache sname)]
|
||||||
(->> (load-script)
|
(eval-script sha)
|
||||||
(p/mapcat eval-script))))))
|
(->> (load-script)
|
||||||
|
(p/mapcat eval-script)))))))
|
||||||
|
|
||||||
(defn timeout-exception?
|
(defn timeout-exception?
|
||||||
[cause]
|
[cause]
|
||||||
|
|||||||
@ -11,7 +11,6 @@
|
|||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http :as-alias http]
|
[app.http :as-alias http]
|
||||||
@ -19,7 +18,6 @@
|
|||||||
[app.http.client :as-alias http.client]
|
[app.http.client :as-alias http.client]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.msgbus :as-alias mbus]
|
[app.msgbus :as-alias mbus]
|
||||||
@ -35,7 +33,6 @@
|
|||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[promesa.exec :as px]
|
|
||||||
[yetti.request :as yrq]
|
[yetti.request :as yrq]
|
||||||
[yetti.response :as yrs]))
|
[yetti.response :as yrs]))
|
||||||
|
|
||||||
@ -47,12 +44,10 @@
|
|||||||
|
|
||||||
(defn- handle-response-transformation
|
(defn- handle-response-transformation
|
||||||
[response request mdata]
|
[response request mdata]
|
||||||
(let [transform-fn (reduce (fn [res-fn transform-fn]
|
(reduce (fn [response transform-fn]
|
||||||
(fn [request response]
|
(transform-fn request response))
|
||||||
(p/then (res-fn request response) #(transform-fn request %))))
|
response
|
||||||
(constantly response)
|
(::response-transform-fns mdata)))
|
||||||
(::response-transform-fns mdata))]
|
|
||||||
(transform-fn request response)))
|
|
||||||
|
|
||||||
(defn- handle-before-comple-hook
|
(defn- handle-before-comple-hook
|
||||||
[response mdata]
|
[response mdata]
|
||||||
@ -63,18 +58,18 @@
|
|||||||
(defn- handle-response
|
(defn- handle-response
|
||||||
[request result]
|
[request result]
|
||||||
(if (fn? result)
|
(if (fn? result)
|
||||||
(p/wrap (result request))
|
(result request)
|
||||||
(let [mdata (meta result)]
|
(let [mdata (meta result)]
|
||||||
(p/-> (yrs/response {:status (::http/status mdata 200)
|
(-> {::yrs/status (::http/status mdata 200)
|
||||||
:headers (::http/headers mdata {})
|
::yrs/headers (::http/headers mdata {})
|
||||||
:body (rph/unwrap result)})
|
::yrs/body (rph/unwrap result)}
|
||||||
(handle-response-transformation request mdata)
|
(handle-response-transformation request mdata)
|
||||||
(handle-before-comple-hook mdata)))))
|
(handle-before-comple-hook mdata)))))
|
||||||
|
|
||||||
(defn- rpc-query-handler
|
(defn- rpc-query-handler
|
||||||
"Ring handler that dispatches query requests and convert between
|
"Ring handler that dispatches query requests and convert between
|
||||||
internal async flow into ring async flow."
|
internal async flow into ring async flow."
|
||||||
[methods {:keys [params path-params] :as request} respond raise]
|
[methods {:keys [params path-params] :as request}]
|
||||||
(let [type (keyword (:type path-params))
|
(let [type (keyword (:type path-params))
|
||||||
profile-id (or (::session/profile-id request)
|
profile-id (or (::session/profile-id request)
|
||||||
(::actoken/profile-id request))
|
(::actoken/profile-id request))
|
||||||
@ -87,19 +82,14 @@
|
|||||||
(assoc :profile-id profile-id)
|
(assoc :profile-id profile-id)
|
||||||
(assoc ::profile-id profile-id))
|
(assoc ::profile-id profile-id))
|
||||||
(dissoc data :profile-id ::profile-id))
|
(dissoc data :profile-id ::profile-id))
|
||||||
method (get methods type default-handler)]
|
method (get methods type default-handler)
|
||||||
|
response (method data)]
|
||||||
(->> (method data)
|
(handle-response request response)))
|
||||||
(p/mcat (partial handle-response request))
|
|
||||||
(p/fnly (fn [response cause]
|
|
||||||
(if cause
|
|
||||||
(raise cause)
|
|
||||||
(respond response)))))))
|
|
||||||
|
|
||||||
(defn- rpc-mutation-handler
|
(defn- rpc-mutation-handler
|
||||||
"Ring handler that dispatches mutation requests and convert between
|
"Ring handler that dispatches mutation requests and convert between
|
||||||
internal async flow into ring async flow."
|
internal async flow into ring async flow."
|
||||||
[methods {:keys [params path-params] :as request} respond raise]
|
[methods {:keys [params path-params] :as request}]
|
||||||
(let [type (keyword (:type path-params))
|
(let [type (keyword (:type path-params))
|
||||||
profile-id (or (::session/profile-id request)
|
profile-id (or (::session/profile-id request)
|
||||||
(::actoken/profile-id request))
|
(::actoken/profile-id request))
|
||||||
@ -111,24 +101,18 @@
|
|||||||
(assoc :profile-id profile-id)
|
(assoc :profile-id profile-id)
|
||||||
(assoc ::profile-id profile-id))
|
(assoc ::profile-id profile-id))
|
||||||
(dissoc data :profile-id))
|
(dissoc data :profile-id))
|
||||||
method (get methods type default-handler)]
|
method (get methods type default-handler)
|
||||||
|
response (method data)]
|
||||||
(->> (method data)
|
(handle-response request response)))
|
||||||
(p/mcat (partial handle-response request))
|
|
||||||
(p/fnly (fn [response cause]
|
|
||||||
(if cause
|
|
||||||
(raise cause)
|
|
||||||
(respond response)))))))
|
|
||||||
|
|
||||||
(defn- rpc-command-handler
|
(defn- rpc-command-handler
|
||||||
"Ring handler that dispatches cmd requests and convert between
|
"Ring handler that dispatches cmd requests and convert between
|
||||||
internal async flow into ring async flow."
|
internal async flow into ring async flow."
|
||||||
[methods {:keys [params path-params] :as request} respond raise]
|
[methods {:keys [params path-params] :as request}]
|
||||||
(let [type (keyword (:type path-params))
|
(let [type (keyword (:type path-params))
|
||||||
etag (yrq/get-header request "if-none-match")
|
etag (yrq/get-header request "if-none-match")
|
||||||
profile-id (or (::session/profile-id request)
|
profile-id (or (::session/profile-id request)
|
||||||
(::actoken/profile-id request))
|
(::actoken/profile-id request))
|
||||||
|
|
||||||
data (-> params
|
data (-> params
|
||||||
(assoc ::request-at (dt/now))
|
(assoc ::request-at (dt/now))
|
||||||
(assoc ::session/id (::session/id request))
|
(assoc ::session/id (::session/id request))
|
||||||
@ -140,12 +124,8 @@
|
|||||||
method (get methods type default-handler)]
|
method (get methods type default-handler)]
|
||||||
|
|
||||||
(binding [cond/*enabled* true]
|
(binding [cond/*enabled* true]
|
||||||
(->> (method data)
|
(let [response (method data)]
|
||||||
(p/mcat (partial handle-response request))
|
(handle-response request response)))))
|
||||||
(p/fnly (fn [response cause]
|
|
||||||
(if cause
|
|
||||||
(raise cause)
|
|
||||||
(respond response))))))))
|
|
||||||
|
|
||||||
(defn- wrap-metrics
|
(defn- wrap-metrics
|
||||||
"Wrap service method with metrics measurement."
|
"Wrap service method with metrics measurement."
|
||||||
@ -153,23 +133,22 @@
|
|||||||
(let [labels (into-array String [(::sv/name mdata)])]
|
(let [labels (into-array String [(::sv/name mdata)])]
|
||||||
(fn [cfg params]
|
(fn [cfg params]
|
||||||
(let [tp (dt/tpoint)]
|
(let [tp (dt/tpoint)]
|
||||||
(->> (f cfg params)
|
(try
|
||||||
(p/fnly (fn [_ _]
|
(f cfg params)
|
||||||
(mtx/run! metrics
|
(finally
|
||||||
:id metrics-id
|
(mtx/run! metrics
|
||||||
:val (inst-ms (tp))
|
:id metrics-id
|
||||||
:labels labels))))))))
|
:val (inst-ms (tp))
|
||||||
|
:labels labels)))))))
|
||||||
|
|
||||||
(defn- wrap-authentication
|
(defn- wrap-authentication
|
||||||
[_ f mdata]
|
[_ f mdata]
|
||||||
(fn [cfg params]
|
(fn [cfg params]
|
||||||
(let [profile-id (::profile-id params)]
|
(let [profile-id (::profile-id params)]
|
||||||
(if (and (::auth mdata true) (not (uuid? profile-id)))
|
(if (and (::auth mdata true) (not (uuid? profile-id)))
|
||||||
(p/rejected
|
(ex/raise :type :authentication
|
||||||
(ex/error :type :authentication
|
:code :authentication-required
|
||||||
:code :authentication-required
|
:hint "authentication required for this endpoint")
|
||||||
:hint "authentication required for this endpoint"))
|
|
||||||
(f cfg params)))))
|
(f cfg params)))))
|
||||||
|
|
||||||
(defn- wrap-access-token
|
(defn- wrap-access-token
|
||||||
@ -182,98 +161,34 @@
|
|||||||
(let [perms (::actoken/perms request #{})]
|
(let [perms (::actoken/perms request #{})]
|
||||||
(if (contains? perms name)
|
(if (contains? perms name)
|
||||||
(f cfg params)
|
(f cfg params)
|
||||||
(p/rejected
|
(ex/raise :type :authorization
|
||||||
(ex/error :type :authorization
|
:code :operation-not-allowed
|
||||||
:code :operation-not-allowed
|
:allowed perms)))
|
||||||
:allowed perms))))
|
|
||||||
(f cfg params))))
|
(f cfg params))))
|
||||||
f))
|
f))
|
||||||
|
|
||||||
(defn- wrap-dispatch
|
|
||||||
"Wraps service method into async flow, with the ability to dispatching
|
|
||||||
it to a preconfigured executor service."
|
|
||||||
[{:keys [::wrk/executor] :as cfg} f mdata]
|
|
||||||
(with-meta
|
|
||||||
(fn [cfg params]
|
|
||||||
(->> (px/submit! executor (px/wrap-bindings #(f cfg params)))
|
|
||||||
(p/mapcat p/wrap)
|
|
||||||
(p/map rph/wrap)))
|
|
||||||
mdata))
|
|
||||||
|
|
||||||
(defn- wrap-audit
|
(defn- wrap-audit
|
||||||
[cfg f mdata]
|
[_ f mdata]
|
||||||
(if (or (contains? cf/flags :webhooks)
|
(if (or (contains? cf/flags :webhooks)
|
||||||
(contains? cf/flags :audit-log))
|
(contains? cf/flags :audit-log))
|
||||||
(letfn [(handle-audit [params result]
|
(if-not (::audit/skip mdata)
|
||||||
(let [resultm (meta result)
|
(fn [cfg params]
|
||||||
request (::http/request params)
|
(let [result (f cfg params)]
|
||||||
|
(->> (audit/prepare-event cfg mdata params result)
|
||||||
profile-id (or (::audit/profile-id resultm)
|
(audit/submit! cfg))
|
||||||
(:profile-id result)
|
result))
|
||||||
(if (= (::type cfg) "command")
|
f)
|
||||||
(::profile-id params)
|
|
||||||
(:profile-id params))
|
|
||||||
uuid/zero)
|
|
||||||
|
|
||||||
props (-> (or (::audit/replace-props resultm)
|
|
||||||
(-> params
|
|
||||||
(merge (::audit/props resultm))
|
|
||||||
(dissoc :profile-id)
|
|
||||||
(dissoc :type)))
|
|
||||||
(audit/clean-props))
|
|
||||||
|
|
||||||
event {:type (or (::audit/type resultm)
|
|
||||||
(::type cfg))
|
|
||||||
:name (or (::audit/name resultm)
|
|
||||||
(::sv/name mdata))
|
|
||||||
:profile-id profile-id
|
|
||||||
:ip-addr (some-> request audit/parse-client-ip)
|
|
||||||
:props props
|
|
||||||
|
|
||||||
;; NOTE: for batch-key lookup we need the params as-is
|
|
||||||
;; because the rpc api does not need to know the
|
|
||||||
;; audit/webhook specific object layout.
|
|
||||||
::params (dissoc params ::http/request)
|
|
||||||
|
|
||||||
::webhooks/batch-key
|
|
||||||
(or (::webhooks/batch-key mdata)
|
|
||||||
(::webhooks/batch-key resultm))
|
|
||||||
|
|
||||||
::webhooks/batch-timeout
|
|
||||||
(or (::webhooks/batch-timeout mdata)
|
|
||||||
(::webhooks/batch-timeout resultm))
|
|
||||||
|
|
||||||
::webhooks/event?
|
|
||||||
(or (::webhooks/event? mdata)
|
|
||||||
(::webhooks/event? resultm)
|
|
||||||
false)}]
|
|
||||||
|
|
||||||
(audit/submit! cfg event)))
|
|
||||||
|
|
||||||
(handle-request [cfg params]
|
|
||||||
(->> (f cfg params)
|
|
||||||
(p/fnly (fn [result cause]
|
|
||||||
(when-not cause
|
|
||||||
(handle-audit params result))))))]
|
|
||||||
|
|
||||||
(if-not (::audit/skip mdata)
|
|
||||||
(with-meta handle-request mdata)
|
|
||||||
f))
|
|
||||||
f))
|
f))
|
||||||
|
|
||||||
(defn- wrap-spec-conform
|
(defn- wrap-spec-conform
|
||||||
[_ f mdata]
|
[_ f mdata]
|
||||||
(let [spec (or (::sv/spec mdata) (s/spec any?))]
|
(let [spec (or (::sv/spec mdata) (s/spec any?))]
|
||||||
(fn [cfg params]
|
(fn [cfg params]
|
||||||
(let [params (ex/try! (us/conform spec params))]
|
(f cfg (us/conform spec params)))))
|
||||||
(if (ex/exception? params)
|
|
||||||
(p/rejected params)
|
|
||||||
(f cfg params))))))
|
|
||||||
|
|
||||||
(defn- wrap-all
|
(defn- wrap-all
|
||||||
[cfg f mdata]
|
[cfg f mdata]
|
||||||
(as-> f $
|
(as-> f $
|
||||||
(wrap-dispatch cfg $ mdata)
|
|
||||||
(wrap-metrics cfg $ mdata)
|
(wrap-metrics cfg $ mdata)
|
||||||
(cond/wrap cfg $ mdata)
|
(cond/wrap cfg $ mdata)
|
||||||
(retry/wrap-retry cfg $ mdata)
|
(retry/wrap-retry cfg $ mdata)
|
||||||
@ -288,13 +203,11 @@
|
|||||||
[cfg f mdata]
|
[cfg f mdata]
|
||||||
(l/debug :hint "register method" :name (::sv/name mdata))
|
(l/debug :hint "register method" :name (::sv/name mdata))
|
||||||
(let [f (wrap-all cfg f mdata)]
|
(let [f (wrap-all cfg f mdata)]
|
||||||
(with-meta #(f cfg %) mdata)))
|
(partial f cfg)))
|
||||||
|
|
||||||
(defn- process-method
|
(defn- process-method
|
||||||
[cfg vfn]
|
[cfg [vfn mdata]]
|
||||||
(let [mdata (meta vfn)]
|
[(keyword (::sv/name mdata)) [mdata (wrap cfg vfn mdata)]])
|
||||||
[(keyword (::sv/name mdata))
|
|
||||||
(wrap cfg vfn mdata)]))
|
|
||||||
|
|
||||||
(defn- resolve-query-methods
|
(defn- resolve-query-methods
|
||||||
[cfg]
|
[cfg]
|
||||||
@ -371,13 +284,13 @@
|
|||||||
:commands (resolve-command-methods cfg)}))
|
:commands (resolve-command-methods cfg)}))
|
||||||
|
|
||||||
(s/def ::mutations
|
(s/def ::mutations
|
||||||
(s/map-of keyword? fn?))
|
(s/map-of keyword? (s/tuple map? fn?)))
|
||||||
|
|
||||||
(s/def ::queries
|
(s/def ::queries
|
||||||
(s/map-of keyword? fn?))
|
(s/map-of keyword? (s/tuple map? fn?)))
|
||||||
|
|
||||||
(s/def ::commands
|
(s/def ::commands
|
||||||
(s/map-of keyword? fn?))
|
(s/map-of keyword? (s/tuple map? fn?)))
|
||||||
|
|
||||||
(s/def ::methods
|
(s/def ::methods
|
||||||
(s/keys :req-un [::mutations
|
(s/keys :req-un [::mutations
|
||||||
@ -391,15 +304,18 @@
|
|||||||
::db/pool
|
::db/pool
|
||||||
::main/props
|
::main/props
|
||||||
::wrk/executor
|
::wrk/executor
|
||||||
::session/manager
|
::session/manager]))
|
||||||
::actoken/manager]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ {:keys [::methods] :as cfg}]
|
[_ {:keys [::methods] :as cfg}]
|
||||||
[["/rpc" {:middleware [[session/authz cfg]
|
(let [methods (-> methods
|
||||||
[actoken/authz cfg]]}
|
(update :commands update-vals peek)
|
||||||
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}]
|
(update :queries update-vals peek)
|
||||||
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
|
(update :mutations update-vals peek))]
|
||||||
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
|
[["/rpc" {:middleware [[session/authz cfg]
|
||||||
:allowed-methods #{:post}}]]])
|
[actoken/authz cfg]]}
|
||||||
|
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}]
|
||||||
|
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
|
||||||
|
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
|
||||||
|
:allowed-methods #{:post}}]]]))
|
||||||
|
|
||||||
|
|||||||
@ -6,14 +6,16 @@
|
|||||||
|
|
||||||
(ns app.rpc.climit
|
(ns app.rpc.climit
|
||||||
"Concurrencly limiter for RPC."
|
"Concurrencly limiter for RPC."
|
||||||
|
(:refer-clojure :exclude [run!])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.climit.config :as-alias config]
|
||||||
|
[app.util.cache :as cache]
|
||||||
[app.util.services :as-alias sv]
|
[app.util.services :as-alias sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as-alias wrk]
|
[app.worker :as-alias wrk]
|
||||||
@ -23,184 +25,200 @@
|
|||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[promesa.exec :as px]
|
[promesa.exec :as px]
|
||||||
[promesa.exec.bulkhead :as pxb])
|
[promesa.exec.bulkhead :as pbh])
|
||||||
(:import
|
(:import
|
||||||
com.github.benmanes.caffeine.cache.Cache
|
clojure.lang.ExceptionInfo))
|
||||||
com.github.benmanes.caffeine.cache.CacheLoader
|
|
||||||
com.github.benmanes.caffeine.cache.Caffeine
|
|
||||||
com.github.benmanes.caffeine.cache.RemovalListener))
|
|
||||||
|
|
||||||
(defn- capacity-exception?
|
(set! *warn-on-reflection* true)
|
||||||
[o]
|
|
||||||
(and (ex/error? o)
|
|
||||||
(let [data (ex-data o)]
|
|
||||||
(and (= :bulkhead-error (:type data))
|
|
||||||
(= :capacity-limit-reached (:code data))))))
|
|
||||||
|
|
||||||
(defn invoke!
|
(defn- create-bulkhead-cache
|
||||||
[limiter f]
|
[{:keys [::wrk/executor]} config]
|
||||||
(->> (px/submit! limiter f)
|
(letfn [(load-fn [key]
|
||||||
(p/hcat (fn [result cause]
|
(let [config (get config (nth key 0))]
|
||||||
(cond
|
(l/trace :hint "insert into cache" :key key)
|
||||||
(capacity-exception? cause)
|
(pbh/create :permits (or (:permits config) (:concurrency config))
|
||||||
(p/rejected
|
:queue (or (:queue config) (:queue-size config))
|
||||||
(ex/error :type :internal
|
:timeout (:timeout config)
|
||||||
:code :concurrency-limit-reached
|
:executor executor
|
||||||
:queue (-> limiter meta ::bkey name)
|
:type (:type config :semaphore))))
|
||||||
:cause cause))
|
|
||||||
|
|
||||||
(some? cause)
|
(on-remove [_ _ cause]
|
||||||
(p/rejected cause)
|
(l/trace :hint "evict from cache" :key key :reason (str cause)))]
|
||||||
|
|
||||||
:else
|
(cache/create :executor :same-thread
|
||||||
(p/resolved result))))))
|
:on-remove on-remove
|
||||||
|
:keepalive "5m"
|
||||||
|
:load-fn load-fn)))
|
||||||
|
|
||||||
(defn- create-limiter
|
(s/def ::config/permits ::us/integer)
|
||||||
[{:keys [::wrk/executor ::mtx/metrics ::bkey ::skey concurrency queue-size]}]
|
(s/def ::config/queue ::us/integer)
|
||||||
(let [labels (into-array String [(name bkey)])
|
(s/def ::config/timeout ::us/integer)
|
||||||
on-queue (fn [instance]
|
|
||||||
(l/trace :hint "enqueued"
|
|
||||||
:key (name bkey)
|
|
||||||
:skey (str skey)
|
|
||||||
:queue-size (get instance ::pxb/current-queue-size)
|
|
||||||
:concurrency (get instance ::pxb/current-concurrency))
|
|
||||||
(mtx/run! metrics
|
|
||||||
:id :rpc-climit-queue-size
|
|
||||||
:val (get instance ::pxb/current-queue-size)
|
|
||||||
:labels labels)
|
|
||||||
(mtx/run! metrics
|
|
||||||
:id :rpc-climit-concurrency
|
|
||||||
:val (get instance ::pxb/current-concurrency)
|
|
||||||
:labels labels))
|
|
||||||
|
|
||||||
on-run (fn [instance task]
|
|
||||||
(let [elapsed (- (inst-ms (dt/now))
|
|
||||||
(inst-ms task))]
|
|
||||||
(l/trace :hint "execute"
|
|
||||||
:key (name bkey)
|
|
||||||
:skey (str skey)
|
|
||||||
:elapsed (str elapsed "ms"))
|
|
||||||
(mtx/run! metrics
|
|
||||||
:id :rpc-climit-timing
|
|
||||||
:val elapsed
|
|
||||||
:labels labels)
|
|
||||||
(mtx/run! metrics
|
|
||||||
:id :rpc-climit-queue-size
|
|
||||||
:val (get instance ::pxb/current-queue-size)
|
|
||||||
:labels labels)
|
|
||||||
(mtx/run! metrics
|
|
||||||
:id :rpc-climit-concurrency
|
|
||||||
:val (get instance ::pxb/current-concurrency)
|
|
||||||
:labels labels)))
|
|
||||||
|
|
||||||
options {:executor executor
|
|
||||||
:concurrency concurrency
|
|
||||||
:queue-size (or queue-size Integer/MAX_VALUE)
|
|
||||||
:on-queue on-queue
|
|
||||||
:on-run on-run}]
|
|
||||||
|
|
||||||
(-> (pxb/create options)
|
|
||||||
(vary-meta assoc ::bkey bkey ::skey skey))))
|
|
||||||
|
|
||||||
(defn- create-cache
|
|
||||||
[{:keys [::wrk/executor] :as params} config]
|
|
||||||
(let [listener (reify RemovalListener
|
|
||||||
(onRemoval [_ key _val cause]
|
|
||||||
(l/trace :hint "cache: remove" :key key :reason (str cause))))
|
|
||||||
|
|
||||||
loader (reify CacheLoader
|
|
||||||
(load [_ key]
|
|
||||||
(let [[bkey skey] key]
|
|
||||||
(when-let [config (get config bkey)]
|
|
||||||
(-> (merge params config)
|
|
||||||
(assoc ::bkey bkey)
|
|
||||||
(assoc ::skey skey)
|
|
||||||
(create-limiter))))))]
|
|
||||||
|
|
||||||
(.. (Caffeine/newBuilder)
|
|
||||||
(weakValues)
|
|
||||||
(executor executor)
|
|
||||||
(removalListener listener)
|
|
||||||
(build loader))))
|
|
||||||
|
|
||||||
(defprotocol IConcurrencyManager)
|
|
||||||
|
|
||||||
(s/def ::concurrency ::us/integer)
|
|
||||||
(s/def ::queue-size ::us/integer)
|
|
||||||
(s/def ::config
|
(s/def ::config
|
||||||
(s/map-of keyword?
|
(s/map-of keyword?
|
||||||
(s/keys :req-un [::concurrency]
|
(s/keys :opt-un [::config/permits
|
||||||
:opt-un [::queue-size])))
|
::config/queue
|
||||||
|
::config/timeout])))
|
||||||
|
|
||||||
(defmethod ig/prep-key ::rpc/climit
|
(defmethod ig/prep-key ::rpc/climit
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(merge {::path (cf/get :rpc-climit-config)}
|
(assoc cfg ::path (cf/get :rpc-climit-config)))
|
||||||
(d/without-nils cfg)))
|
|
||||||
|
|
||||||
(s/def ::path ::fs/path)
|
(s/def ::path ::fs/path)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::rpc/climit [_]
|
(defmethod ig/pre-init-spec ::rpc/climit [_]
|
||||||
(s/keys :req [::wrk/executor ::mtx/metrics ::path]))
|
(s/keys :req [::wrk/executor ::mtx/metrics ::path]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::rpc/climit
|
(defmethod ig/init-key ::rpc/climit
|
||||||
[_ {:keys [::path] :as params}]
|
[_ {:keys [::path ::mtx/metrics ::wrk/executor] :as cfg}]
|
||||||
(when (contains? cf/flags :rpc-climit)
|
(when (contains? cf/flags :rpc-climit)
|
||||||
(if-let [config (some->> path slurp edn/read-string)]
|
(when-let [params (some->> path slurp edn/read-string)]
|
||||||
(do
|
(l/info :hint "initializing concurrency limit" :config (str path))
|
||||||
(l/info :hint "initializing concurrency limit" :config (str path))
|
(us/verify! ::config params)
|
||||||
(us/verify! ::config config)
|
{::cache (create-bulkhead-cache cfg params)
|
||||||
|
::config params
|
||||||
(let [cache (create-cache params config)]
|
::wrk/executor executor
|
||||||
^{::cache cache}
|
::mtx/metrics metrics})))
|
||||||
(reify
|
|
||||||
IConcurrencyManager
|
|
||||||
clojure.lang.IDeref
|
|
||||||
(deref [_] config)
|
|
||||||
|
|
||||||
clojure.lang.ILookup
|
|
||||||
(valAt [_ key]
|
|
||||||
(let [key (if (vector? key) key [key])]
|
|
||||||
(.get ^Cache cache key))))))
|
|
||||||
|
|
||||||
(l/warn :hint "unable to load configuration" :config (str path)))))
|
|
||||||
|
|
||||||
|
(s/def ::cache cache/cache?)
|
||||||
|
(s/def ::instance
|
||||||
|
(s/keys :req [::cache ::config ::wrk/executor]))
|
||||||
|
|
||||||
(s/def ::rpc/climit
|
(s/def ::rpc/climit
|
||||||
(s/nilable #(satisfies? IConcurrencyManager %)))
|
(s/nilable ::instance))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; PUBLIC API
|
;; PUBLIC API
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn invoke!
|
||||||
|
[cache metrics id key f]
|
||||||
|
(let [limiter (cache/get cache [id key])
|
||||||
|
tpoint (dt/tpoint)
|
||||||
|
labels (into-array String [(name id)])
|
||||||
|
|
||||||
|
wrapped
|
||||||
|
(fn []
|
||||||
|
(let [elapsed (tpoint)
|
||||||
|
stats (pbh/get-stats limiter)]
|
||||||
|
(l/trace :hint "executed"
|
||||||
|
:id (name id)
|
||||||
|
:key key
|
||||||
|
:fnh (hash f)
|
||||||
|
:permits (:permits stats)
|
||||||
|
:queue (:queue stats)
|
||||||
|
:max-permits (:max-permits stats)
|
||||||
|
:max-queue (:max-queue stats)
|
||||||
|
:elapsed (dt/format-duration elapsed))
|
||||||
|
(mtx/run! metrics
|
||||||
|
:id :rpc-climit-timing
|
||||||
|
:val (inst-ms elapsed)
|
||||||
|
:labels labels)
|
||||||
|
(try
|
||||||
|
(f)
|
||||||
|
(finally
|
||||||
|
(let [elapsed (tpoint)]
|
||||||
|
(l/trace :hint "finished"
|
||||||
|
:id (name id)
|
||||||
|
:key key
|
||||||
|
:fnh (hash f)
|
||||||
|
:permits (:permits stats)
|
||||||
|
:queue (:queue stats)
|
||||||
|
:max-permits (:max-permits stats)
|
||||||
|
:max-queue (:max-queue stats)
|
||||||
|
:elapsed (dt/format-duration elapsed)))))))
|
||||||
|
measure!
|
||||||
|
(fn [stats]
|
||||||
|
(mtx/run! metrics
|
||||||
|
:id :rpc-climit-queue
|
||||||
|
:val (:queue stats)
|
||||||
|
:labels labels)
|
||||||
|
(mtx/run! metrics
|
||||||
|
:id :rpc-climit-permits
|
||||||
|
:val (:permits stats)
|
||||||
|
:labels labels))]
|
||||||
|
|
||||||
|
(try
|
||||||
|
(let [stats (pbh/get-stats limiter)]
|
||||||
|
(measure! stats)
|
||||||
|
(l/trace :hint "enqueued"
|
||||||
|
:id (name id)
|
||||||
|
:key key
|
||||||
|
:fnh (hash f)
|
||||||
|
:permits (:permits stats)
|
||||||
|
:queue (:queue stats)
|
||||||
|
:max-permits (:max-permits stats)
|
||||||
|
:max-queue (:max-queue stats))
|
||||||
|
(pbh/invoke! limiter wrapped))
|
||||||
|
(catch ExceptionInfo cause
|
||||||
|
(let [{:keys [type code]} (ex-data cause)]
|
||||||
|
(if (= :bulkhead-error type)
|
||||||
|
(ex/raise :type :concurrency-limit
|
||||||
|
:code code
|
||||||
|
:hint "concurrency limit reached")
|
||||||
|
(throw cause))))
|
||||||
|
|
||||||
|
(finally
|
||||||
|
(measure! (pbh/get-stats limiter))))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn run!
|
||||||
|
[{:keys [::id ::cache ::mtx/metrics]} f]
|
||||||
|
(if (and cache id)
|
||||||
|
(invoke! cache metrics id nil f)
|
||||||
|
(f)))
|
||||||
|
|
||||||
|
(defn submit!
|
||||||
|
[{:keys [::id ::cache ::wrk/executor ::mtx/metrics]} f]
|
||||||
|
(let [f (partial px/submit! executor f)]
|
||||||
|
(if (and cache id)
|
||||||
|
(p/await! (invoke! cache metrics id nil f))
|
||||||
|
(p/await! (f)))))
|
||||||
|
|
||||||
|
(defn configure
|
||||||
|
([{:keys [::rpc/climit]} id]
|
||||||
|
(us/assert! ::rpc/climit climit)
|
||||||
|
(assoc climit ::id id))
|
||||||
|
([{:keys [::rpc/climit]} id executor]
|
||||||
|
(us/assert! ::rpc/climit climit)
|
||||||
|
(-> climit
|
||||||
|
(assoc ::id id)
|
||||||
|
(assoc ::wrk/executor executor))))
|
||||||
|
|
||||||
|
(defmacro with-dispatch!
|
||||||
|
"Dispatch blocking operation to a separated thread protected with the
|
||||||
|
specified concurrency limiter. If climit is not active, the function
|
||||||
|
will be scheduled to execute without concurrency monitoring."
|
||||||
|
[instance & body]
|
||||||
|
(if (vector? instance)
|
||||||
|
`(-> (app.rpc.climit/configure ~@instance)
|
||||||
|
(app.rpc.climit/run! (^:once fn* [] ~@body)))
|
||||||
|
`(run! ~instance (^:once fn* [] ~@body))))
|
||||||
|
|
||||||
(defmacro with-dispatch
|
(defmacro with-dispatch
|
||||||
[lim & body]
|
"Dispatch blocking operation to a separated thread protected with
|
||||||
`(if ~lim
|
the specified semaphore.
|
||||||
(invoke! ~lim (^:once fn [] (p/wrap (do ~@body))))
|
DEPRECATED"
|
||||||
(p/wrap (do ~@body))))
|
[& params]
|
||||||
|
`(with-dispatch! ~@params))
|
||||||
|
|
||||||
|
(def noop-fn (constantly nil))
|
||||||
|
|
||||||
(defn wrap
|
(defn wrap
|
||||||
[{:keys [::rpc/climit]} f {:keys [::queue ::key-fn] :as mdata}]
|
[{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}]
|
||||||
(if (and (some? climit)
|
(if (and (some? climit) (some? id))
|
||||||
(some? queue))
|
(if-let [config (get-in climit [::config id])]
|
||||||
(if-let [config (get @climit queue)]
|
(let [cache (::cache climit)]
|
||||||
(do
|
|
||||||
(l/debug :hint "wrap: instrumenting method"
|
(l/debug :hint "wrap: instrumenting method"
|
||||||
:limit-name (name queue)
|
:limit (name id)
|
||||||
:service-name (::sv/name mdata)
|
:service-name (::sv/name mdata)
|
||||||
:queue-size (or (:queue-size config) Integer/MAX_VALUE)
|
:timeout (:timeout config)
|
||||||
:concurrency (:concurrency config)
|
:permits (:permits config)
|
||||||
|
:queue (:queue config)
|
||||||
:keyed? (some? key-fn))
|
:keyed? (some? key-fn))
|
||||||
(if (some? key-fn)
|
(fn [cfg params]
|
||||||
(fn [cfg params]
|
(invoke! cache metrics id (key-fn params) (partial f cfg params))))
|
||||||
(let [key [queue (key-fn params)]
|
|
||||||
lim (get climit key)]
|
|
||||||
(invoke! lim (partial f cfg params))))
|
|
||||||
(let [lim (get climit queue)]
|
|
||||||
(fn [cfg params]
|
|
||||||
(invoke! lim (partial f cfg params))))))
|
|
||||||
(do
|
(do
|
||||||
(l/warn :hint "wrap: no config found"
|
(l/warn :hint "no config found for specified queue" :id id)
|
||||||
:queue (name queue)
|
|
||||||
:service (::sv/name mdata))
|
|
||||||
f))
|
f))
|
||||||
|
|
||||||
f))
|
f))
|
||||||
|
|||||||
@ -21,10 +21,7 @@
|
|||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[clojure.spec.alpha :as s]))
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
(defn- event->row [event]
|
(defn- event->row [event]
|
||||||
[(uuid/next)
|
[(uuid/next)
|
||||||
@ -71,17 +68,22 @@
|
|||||||
:req-un [::events]))
|
:req-un [::events]))
|
||||||
|
|
||||||
(sv/defmethod ::push-audit-events
|
(sv/defmethod ::push-audit-events
|
||||||
{::climit/queue :push-audit-events
|
{::climit/id :submit-audit-events-by-profile
|
||||||
::climit/key-fn ::rpc/profile-id
|
::climit/key-fn ::rpc/profile-id
|
||||||
::audit/skip true
|
::audit/skip true
|
||||||
::doc/added "1.17"}
|
::doc/added "1.17"}
|
||||||
[{:keys [::db/pool ::wrk/executor] :as cfg} params]
|
[{:keys [::db/pool] :as cfg} params]
|
||||||
(if (or (db/read-only? pool)
|
(if (or (db/read-only? pool)
|
||||||
(not (contains? cf/flags :audit-log)))
|
(not (contains? cf/flags :audit-log)))
|
||||||
(do
|
(do
|
||||||
(l/warn :hint "audit: http handler disabled or db is read-only")
|
(l/warn :hint "audit: http handler disabled or db is read-only")
|
||||||
(rph/wrap nil))
|
(rph/wrap nil))
|
||||||
|
|
||||||
(->> (px/submit! executor #(handle-events cfg params))
|
(do
|
||||||
(p/fmap (constantly nil)))))
|
(try
|
||||||
|
(handle-events cfg params)
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/error :hint "unexpected error on persisting audit events from frontend"
|
||||||
|
:cause cause)))
|
||||||
|
|
||||||
|
(rph/wrap nil))))
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
(ns app.rpc.commands.auth
|
(ns app.rpc.commands.auth
|
||||||
(:require
|
(:require
|
||||||
[app.auth :as auth]
|
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
@ -18,7 +18,6 @@
|
|||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.climit :as climit]
|
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
@ -63,14 +62,20 @@
|
|||||||
:code :login-disabled
|
:code :login-disabled
|
||||||
:hint "login is disabled in this instance"))
|
:hint "login is disabled in this instance"))
|
||||||
|
|
||||||
(letfn [(check-password [profile password]
|
(letfn [(check-password [conn profile password]
|
||||||
(when (= (:password profile) "!")
|
(when (= (:password profile) "!")
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :account-without-password
|
:code :account-without-password
|
||||||
:hint "the current account does not have password"))
|
:hint "the current account does not have password"))
|
||||||
(:valid (auth/verify-password password (:password profile))))
|
(let [result (profile/verify-password cfg password (:password profile))]
|
||||||
|
(when (:update result)
|
||||||
|
(l/trace :hint "updating profile password" :id (:id profile) :email (:email profile))
|
||||||
|
(profile/update-profile-password! (assoc cfg ::db/conn conn)
|
||||||
|
(assoc profile :password password)))
|
||||||
|
(:valid result)))
|
||||||
|
|
||||||
(validate-profile [profile]
|
|
||||||
|
(validate-profile [conn profile]
|
||||||
(when-not profile
|
(when-not profile
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :wrong-credentials))
|
:code :wrong-credentials))
|
||||||
@ -80,7 +85,7 @@
|
|||||||
(when (:is-blocked profile)
|
(when (:is-blocked profile)
|
||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
:code :profile-blocked))
|
:code :profile-blocked))
|
||||||
(when-not (check-password profile password)
|
(when-not (check-password conn profile password)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :wrong-credentials))
|
:code :wrong-credentials))
|
||||||
(when-let [deleted-at (:deleted-at profile)]
|
(when-let [deleted-at (:deleted-at profile)]
|
||||||
@ -92,8 +97,7 @@
|
|||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [profile (->> (profile/get-profile-by-email conn email)
|
(let [profile (->> (profile/get-profile-by-email conn email)
|
||||||
(validate-profile)
|
(validate-profile conn)
|
||||||
(profile/decode-row)
|
|
||||||
(profile/strip-private-attrs))
|
(profile/strip-private-attrs))
|
||||||
|
|
||||||
invitation (when-let [token (:invitation-token params)]
|
invitation (when-let [token (:invitation-token params)]
|
||||||
@ -118,7 +122,6 @@
|
|||||||
(sv/defmethod ::login-with-password
|
(sv/defmethod ::login-with-password
|
||||||
"Performs authentication using penpot password."
|
"Performs authentication using penpot password."
|
||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
::climit/queue :auth
|
|
||||||
::doc/added "1.15"}
|
::doc/added "1.15"}
|
||||||
[cfg params]
|
[cfg params]
|
||||||
(login-with-password cfg params))
|
(login-with-password cfg params))
|
||||||
@ -144,7 +147,7 @@
|
|||||||
(:profile-id tdata)))
|
(:profile-id tdata)))
|
||||||
|
|
||||||
(update-password [conn profile-id]
|
(update-password [conn profile-id]
|
||||||
(let [pwd (auth/derive-password password)]
|
(let [pwd (profile/derive-password cfg password)]
|
||||||
(db/update! conn :profile {:password pwd} {:id profile-id})))]
|
(db/update! conn :profile {:password pwd} {:id profile-id})))]
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
@ -158,7 +161,6 @@
|
|||||||
|
|
||||||
(sv/defmethod ::recover-profile
|
(sv/defmethod ::recover-profile
|
||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
::climit/queue :auth
|
|
||||||
::doc/added "1.15"}
|
::doc/added "1.15"}
|
||||||
[cfg params]
|
[cfg params]
|
||||||
(recover-profile cfg params))
|
(recover-profile cfg params))
|
||||||
@ -264,9 +266,7 @@
|
|||||||
:nudge {:big 10 :small 1}})
|
:nudge {:big 10 :small 1}})
|
||||||
(db/tjson))
|
(db/tjson))
|
||||||
|
|
||||||
password (if-let [password (:password params)]
|
password (or (:password params) "!")
|
||||||
(auth/derive-password password)
|
|
||||||
"!")
|
|
||||||
|
|
||||||
locale (:locale params)
|
locale (:locale params)
|
||||||
locale (when (and (string? locale) (not (str/blank? locale)))
|
locale (when (and (string? locale) (not (str/blank? locale)))
|
||||||
@ -344,8 +344,11 @@
|
|||||||
|
|
||||||
profile (if-let [profile-id (:profile-id claims)]
|
profile (if-let [profile-id (:profile-id claims)]
|
||||||
(profile/get-profile conn profile-id)
|
(profile/get-profile conn profile-id)
|
||||||
(->> (create-profile! conn (assoc params :is-active is-active))
|
(let [params (-> params
|
||||||
(create-profile-rels! conn)))
|
(assoc :is-active is-active)
|
||||||
|
(update :password #(profile/derive-password cfg %)))]
|
||||||
|
(->> (create-profile! conn params)
|
||||||
|
(create-profile-rels! conn))))
|
||||||
|
|
||||||
invitation (when-let [token (:invitation-token params)]
|
invitation (when-let [token (:invitation-token params)]
|
||||||
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))]
|
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))]
|
||||||
@ -356,9 +359,9 @@
|
|||||||
(when-let [id (:profile-id claims)]
|
(when-let [id (:profile-id claims)]
|
||||||
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
|
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
|
||||||
(audit/submit! cfg
|
(audit/submit! cfg
|
||||||
{:type "fact"
|
{::audit/type "fact"
|
||||||
:name "register-profile-retry"
|
::audit/name "register-profile-retry"
|
||||||
:profile-id id}))
|
::audit/profile-id id}))
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
;; If invitation token comes in params, this is because the
|
;; If invitation token comes in params, this is because the
|
||||||
@ -406,7 +409,6 @@
|
|||||||
|
|
||||||
(sv/defmethod ::register-profile
|
(sv/defmethod ::register-profile
|
||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
::climit/queue :auth
|
|
||||||
::doc/added "1.15"}
|
::doc/added "1.15"}
|
||||||
[{:keys [::db/pool] :as cfg} params]
|
[{:keys [::db/pool] :as cfg} params]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
|
|||||||
@ -354,7 +354,6 @@
|
|||||||
(with-open [^AutoCloseable conn (db/open pool)]
|
(with-open [^AutoCloseable conn (db/open pool)]
|
||||||
(db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)])))
|
(db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)])))
|
||||||
|
|
||||||
|
|
||||||
(defn- create-or-update-file
|
(defn- create-or-update-file
|
||||||
[conn params]
|
[conn params]
|
||||||
(let [sql (str "INSERT INTO file (id, project_id, name, revn, is_shared, data, created_at, modified_at) "
|
(let [sql (str "INSERT INTO file (id, project_id, name, revn, is_shared, data, created_at, modified_at) "
|
||||||
@ -527,13 +526,13 @@
|
|||||||
(write-obj! output sids)
|
(write-obj! output sids)
|
||||||
|
|
||||||
(doseq [id sids]
|
(doseq [id sids]
|
||||||
(let [{:keys [size] :as obj} @(sto/get-object storage id)]
|
(let [{:keys [size] :as obj} (sto/get-object storage id)]
|
||||||
(l/debug :hint "write sobject" :id id ::l/sync? true)
|
(l/debug :hint "write sobject" :id id ::l/sync? true)
|
||||||
(doto output
|
(doto output
|
||||||
(write-uuid! id)
|
(write-uuid! id)
|
||||||
(write-obj! (meta obj)))
|
(write-obj! (meta obj)))
|
||||||
|
|
||||||
(with-open [^InputStream stream @(sto/get-object-data storage obj)]
|
(with-open [^InputStream stream (sto/get-object-data storage obj)]
|
||||||
(let [written (write-stream! output stream size)]
|
(let [written (write-stream! output stream size)]
|
||||||
(when (not= written size)
|
(when (not= written size)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
@ -719,7 +718,7 @@
|
|||||||
(assoc ::sto/touched-at (dt/now))
|
(assoc ::sto/touched-at (dt/now))
|
||||||
(assoc :bucket "file-media-object"))
|
(assoc :bucket "file-media-object"))
|
||||||
|
|
||||||
sobject @(sto/put-object! storage params)]
|
sobject (sto/put-object! storage params)]
|
||||||
|
|
||||||
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true)
|
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true)
|
||||||
(vswap! *state* update :index assoc id (:id sobject)))))
|
(vswap! *state* update :index assoc id (:id sobject)))))
|
||||||
@ -910,7 +909,9 @@
|
|||||||
(export! output-stream))))]
|
(export! output-stream))))]
|
||||||
|
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(yrs/response 200 body {"content-type" "application/octet-stream"}))))
|
{::yrs/status 200
|
||||||
|
::yrs/body body
|
||||||
|
::yrs/headers {"content-type" "application/octet-stream"}})))
|
||||||
|
|
||||||
(s/def ::file ::media/upload)
|
(s/def ::file ::media/upload)
|
||||||
(s/def ::import-binfile
|
(s/def ::import-binfile
|
||||||
|
|||||||
@ -101,7 +101,7 @@
|
|||||||
(sv/defmethod ::get-comment-threads
|
(sv/defmethod ::get-comment-threads
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(get-comment-threads conn profile-id file-id)))
|
(get-comment-threads conn profile-id file-id)))
|
||||||
|
|
||||||
@ -144,7 +144,7 @@
|
|||||||
(sv/defmethod ::get-unread-comment-threads
|
(sv/defmethod ::get-unread-comment-threads
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
(get-unread-comment-threads conn profile-id team-id)))
|
(get-unread-comment-threads conn profile-id team-id)))
|
||||||
|
|
||||||
@ -191,7 +191,7 @@
|
|||||||
(sv/defmethod ::get-comment-thread
|
(sv/defmethod ::get-comment-thread
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(let [sql (str "with threads as (" sql:comment-threads ")"
|
(let [sql (str "with threads as (" sql:comment-threads ")"
|
||||||
"select * from threads where id = ?")]
|
"select * from threads where id = ?")]
|
||||||
@ -211,7 +211,7 @@
|
|||||||
(sv/defmethod ::get-comments
|
(sv/defmethod ::get-comments
|
||||||
{::doc/added "1.15"}
|
{::doc/added "1.15"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(get-comments conn thread-id))))
|
(get-comments conn thread-id))))
|
||||||
@ -263,7 +263,7 @@
|
|||||||
{::doc/added "1.15"
|
{::doc/added "1.15"
|
||||||
::doc/changes ["1.15" "Imported from queries and renamed."]}
|
::doc/changes ["1.15" "Imported from queries and renamed."]}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||||
(get-file-comments-users conn file-id profile-id)))
|
(get-file-comments-users conn file-id profile-id)))
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.auth :as auth]
|
[app.rpc.commands.auth :as auth]
|
||||||
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
@ -48,7 +49,7 @@
|
|||||||
:fullname fullname
|
:fullname fullname
|
||||||
:is-active true
|
:is-active true
|
||||||
:deleted-at (dt/in-future cf/deletion-delay)
|
:deleted-at (dt/in-future cf/deletion-delay)
|
||||||
:password password
|
:password (profile/derive-password cfg password)
|
||||||
:props {}}]
|
:props {}}]
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
|
|||||||
@ -277,7 +277,7 @@
|
|||||||
::cond/get-object #(get-minimal-file %1 (:id %2))
|
::cond/get-object #(get-minimal-file %1 (:id %2))
|
||||||
::cond/key-fn get-file-etag}
|
::cond/key-fn get-file-etag}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(let [perms (get-permissions conn profile-id id)]
|
(let [perms (get-permissions conn profile-id id)]
|
||||||
(check-read-permissions! perms)
|
(check-read-permissions! perms)
|
||||||
(let [file (-> (get-file conn id features)
|
(let [file (-> (get-file conn id features)
|
||||||
@ -305,7 +305,7 @@
|
|||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::rpc/:auth false}
|
::rpc/:auth false}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(let [perms (get-permissions conn profile-id file-id share-id)]
|
(let [perms (get-permissions conn profile-id file-id share-id)]
|
||||||
(check-read-permissions! perms)
|
(check-read-permissions! perms)
|
||||||
(-> (get-file-fragment conn file-id fragment-id)
|
(-> (get-file-fragment conn file-id fragment-id)
|
||||||
@ -341,7 +341,7 @@
|
|||||||
::cond/reuse-key? true
|
::cond/reuse-key? true
|
||||||
::cond/key-fn get-file-etag}
|
::cond/key-fn get-file-etag}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-object-thumbnails conn file-id)))
|
(get-object-thumbnails conn file-id)))
|
||||||
|
|
||||||
@ -372,7 +372,7 @@
|
|||||||
"Get all files for the specified project."
|
"Get all files for the specified project."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(projects/check-read-permissions! conn profile-id project-id)
|
(projects/check-read-permissions! conn profile-id project-id)
|
||||||
(get-project-files conn project-id)))
|
(get-project-files conn project-id)))
|
||||||
|
|
||||||
@ -391,7 +391,7 @@
|
|||||||
"Checks if the file has libraries. Returns a boolean"
|
"Checks if the file has libraries. Returns a boolean"
|
||||||
{::doc/added "1.15.1"}
|
{::doc/added "1.15.1"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! pool profile-id file-id)
|
(check-read-permissions! pool profile-id file-id)
|
||||||
(get-has-file-libraries conn file-id)))
|
(get-has-file-libraries conn file-id)))
|
||||||
|
|
||||||
@ -458,7 +458,7 @@
|
|||||||
Mainly used for rendering purposes."
|
Mainly used for rendering purposes."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-page conn params)))
|
(get-page conn params)))
|
||||||
|
|
||||||
@ -511,7 +511,7 @@
|
|||||||
"Get all file (libraries) for the specified team."
|
"Get all file (libraries) for the specified team."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
(get-team-shared-files conn team-id)))
|
(get-team-shared-files conn team-id)))
|
||||||
|
|
||||||
@ -565,7 +565,7 @@
|
|||||||
"Get libraries used by the specified file."
|
"Get libraries used by the specified file."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-file-libraries conn file-id features)))
|
(get-file-libraries conn file-id features)))
|
||||||
|
|
||||||
@ -591,7 +591,7 @@
|
|||||||
"Returns all the file references that use specified file (library) id."
|
"Returns all the file references that use specified file (library) id."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(get-library-file-references conn file-id)))
|
(get-library-file-references conn file-id)))
|
||||||
|
|
||||||
@ -628,7 +628,7 @@
|
|||||||
(sv/defmethod ::get-team-recent-files
|
(sv/defmethod ::get-team-recent-files
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
(get-team-recent-files conn team-id)))
|
(get-team-recent-files conn team-id)))
|
||||||
|
|
||||||
@ -662,7 +662,7 @@
|
|||||||
(sv/defmethod ::get-file-thumbnail
|
(sv/defmethod ::get-file-thumbnail
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
(-> (get-file-thumbnail conn file-id revn)
|
(-> (get-file-thumbnail conn file-id revn)
|
||||||
(rph/with-http-cache long-cache-duration))))
|
(rph/with-http-cache long-cache-duration))))
|
||||||
@ -758,7 +758,7 @@
|
|||||||
mainly for render thumbnails on dashboard."
|
mainly for render thumbnails on dashboard."
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id file-id)
|
(check-read-permissions! conn profile-id file-id)
|
||||||
;; NOTE: we force here the "storage/pointer-map" feature, because
|
;; NOTE: we force here the "storage/pointer-map" feature, because
|
||||||
;; it used internally only and is independent if user supports it
|
;; it used internally only and is independent if user supports it
|
||||||
|
|||||||
@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
(defn- wrap-with-pointer-map-context
|
(defn- wrap-with-pointer-map-context
|
||||||
[f]
|
[f]
|
||||||
(fn [{:keys [conn] :as cfg} {:keys [id] :as file}]
|
(fn [{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||||
(binding [pmap/*tracked* (atom {})
|
(binding [pmap/*tracked* (atom {})
|
||||||
pmap/*load-fn* (partial files/load-pointer conn id)
|
pmap/*load-fn* (partial files/load-pointer conn id)
|
||||||
ffeat/*wrap-with-pointer-map-fn* pmap/wrap]
|
ffeat/*wrap-with-pointer-map-fn* pmap/wrap]
|
||||||
@ -126,7 +126,7 @@
|
|||||||
;; database.
|
;; database.
|
||||||
|
|
||||||
(sv/defmethod ::update-file
|
(sv/defmethod ::update-file
|
||||||
{::climit/queue :update-file
|
{::climit/id :update-file-by-id
|
||||||
::climit/key-fn :id
|
::climit/key-fn :id
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::webhooks/batch-timeout (dt/duration "2m")
|
::webhooks/batch-timeout (dt/duration "2m")
|
||||||
@ -136,8 +136,7 @@
|
|||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id id)
|
(files/check-edition-permissions! conn profile-id id)
|
||||||
(db/xact-lock! conn id)
|
(db/xact-lock! conn id)
|
||||||
|
(let [cfg (assoc cfg ::db/conn conn)
|
||||||
(let [cfg (assoc cfg :conn conn)
|
|
||||||
params (assoc params :profile-id profile-id)
|
params (assoc params :profile-id profile-id)
|
||||||
tpoint (dt/tpoint)]
|
tpoint (dt/tpoint)]
|
||||||
(-> (update-file cfg params)
|
(-> (update-file cfg params)
|
||||||
@ -145,7 +144,7 @@
|
|||||||
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
|
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
|
||||||
|
|
||||||
(defn update-file
|
(defn update-file
|
||||||
[{:keys [conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
|
[{:keys [::db/conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
|
||||||
(let [file (get-file conn id)
|
(let [file (get-file conn id)
|
||||||
features (->> (concat (:features file)
|
features (->> (concat (:features file)
|
||||||
(:features params))
|
(:features params))
|
||||||
@ -197,24 +196,34 @@
|
|||||||
:project-id (:project-id file)
|
:project-id (:project-id file)
|
||||||
:team-id (:team-id file)}))))))
|
:team-id (:team-id file)}))))))
|
||||||
|
|
||||||
|
(defn- update-file-data
|
||||||
|
[file changes]
|
||||||
|
(-> file
|
||||||
|
(update :revn inc)
|
||||||
|
(update :data (fn [data]
|
||||||
|
(cond-> data
|
||||||
|
:always
|
||||||
|
(-> (blob/decode)
|
||||||
|
(assoc :id (:id file))
|
||||||
|
(pmg/migrate-data))
|
||||||
|
|
||||||
|
(and (contains? ffeat/*current* "components/v2")
|
||||||
|
(not (contains? ffeat/*previous* "components/v2")))
|
||||||
|
(ctf/migrate-to-components-v2)
|
||||||
|
|
||||||
|
:always
|
||||||
|
(-> (cp/process-changes changes)
|
||||||
|
(blob/encode)))))))
|
||||||
|
|
||||||
|
|
||||||
(defn- update-file*
|
(defn- update-file*
|
||||||
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
|
||||||
(let [file (-> file
|
(let [;; Process the file data in the CLIMIT context; scheduling it
|
||||||
(update :revn inc)
|
;; to be executed on a separated executor for avoid to do the
|
||||||
(update :data (fn [data]
|
;; CPU intensive operation on vthread.
|
||||||
(cond-> data
|
file (-> (climit/configure cfg :update-file)
|
||||||
:always
|
(climit/submit! (partial update-file-data file changes)))]
|
||||||
(-> (blob/decode)
|
|
||||||
(assoc :id (:id file))
|
|
||||||
(pmg/migrate-data))
|
|
||||||
|
|
||||||
(and (contains? ffeat/*current* "components/v2")
|
|
||||||
(not (contains? ffeat/*previous* "components/v2")))
|
|
||||||
(ctf/migrate-to-components-v2)
|
|
||||||
|
|
||||||
:always
|
|
||||||
(-> (cp/process-changes changes)
|
|
||||||
(blob/encode))))))]
|
|
||||||
(db/insert! conn :file-change
|
(db/insert! conn :file-change
|
||||||
{:id (uuid/next)
|
{:id (uuid/next)
|
||||||
:session-id session-id
|
:session-id session-id
|
||||||
@ -273,11 +282,10 @@
|
|||||||
(vec)))
|
(vec)))
|
||||||
|
|
||||||
(defn- send-notifications!
|
(defn- send-notifications!
|
||||||
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [file changes session-id] :as params}]
|
||||||
(let [lchanges (filter library-change? changes)
|
(let [lchanges (filter library-change? changes)
|
||||||
msgbus (::mbus/msgbus cfg)]
|
msgbus (::mbus/msgbus cfg)]
|
||||||
|
|
||||||
;; Asynchronously publish message to the msgbus
|
|
||||||
(mbus/pub! msgbus
|
(mbus/pub! msgbus
|
||||||
:topic (:id file)
|
:topic (:id file)
|
||||||
:message {:type :file-change
|
:message {:type :file-change
|
||||||
@ -290,7 +298,6 @@
|
|||||||
(when (and (:is-shared file) (seq lchanges))
|
(when (and (:is-shared file) (seq lchanges))
|
||||||
(let [team-id (or (:team-id file)
|
(let [team-id (or (:team-id file)
|
||||||
(files/get-team-id conn (:project-id file)))]
|
(files/get-team-id conn (:project-id file)))]
|
||||||
;; Asynchronously publish message to the msgbus
|
|
||||||
(mbus/pub! msgbus
|
(mbus/pub! msgbus
|
||||||
:topic team-id
|
:topic team-id
|
||||||
:message {:type :library-change
|
:message {:type :library-change
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
(ns app.rpc.commands.fonts
|
(ns app.rpc.commands.fonts
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
@ -15,7 +15,7 @@
|
|||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.climit :as-alias climit]
|
[app.rpc.climit :as climit]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.commands.projects :as projects]
|
[app.rpc.commands.projects :as projects]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
@ -25,10 +25,7 @@
|
|||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as-alias wrk]
|
[clojure.spec.alpha :as s]))
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
||||||
(def valid-style #{"normal" "italic"})
|
(def valid-style #{"normal" "italic"})
|
||||||
@ -59,7 +56,7 @@
|
|||||||
(sv/defmethod ::get-font-variants
|
(sv/defmethod ::get-font-variants
|
||||||
{::doc/added "1.18"}
|
{::doc/added "1.18"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(cond
|
(cond
|
||||||
(uuid? team-id)
|
(uuid? team-id)
|
||||||
(do
|
(do
|
||||||
@ -107,50 +104,45 @@
|
|||||||
(create-font-variant cfg (assoc params :profile-id profile-id))))
|
(create-font-variant cfg (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
(defn create-font-variant
|
(defn create-font-variant
|
||||||
[{:keys [::sto/storage ::db/pool ::wrk/executor ::rpc/climit]} {:keys [data] :as params}]
|
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [data] :as params}]
|
||||||
(letfn [(generate-fonts [data]
|
(letfn [(generate-missing! [data]
|
||||||
(climit/with-dispatch (:process-font climit)
|
(let [data (media/run {:cmd :generate-fonts :input data})]
|
||||||
(media/run {:cmd :generate-fonts :input data})))
|
(when (and (not (contains? data "font/otf"))
|
||||||
|
(not (contains? data "font/ttf"))
|
||||||
|
(not (contains? data "font/woff"))
|
||||||
|
(not (contains? data "font/woff2")))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :invalid-font-upload
|
||||||
|
:hint "invalid font upload, unable to generate missing font assets"))
|
||||||
|
data))
|
||||||
|
|
||||||
;; Function responsible of calculating cryptographyc hash of
|
(prepare-font [data mtype]
|
||||||
;; the provided data.
|
|
||||||
(calculate-hash [data]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(sto/calculate-hash data)))
|
|
||||||
|
|
||||||
(validate-data [data]
|
|
||||||
(when (and (not (contains? data "font/otf"))
|
|
||||||
(not (contains? data "font/ttf"))
|
|
||||||
(not (contains? data "font/woff"))
|
|
||||||
(not (contains? data "font/woff2")))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :invalid-font-upload))
|
|
||||||
data)
|
|
||||||
|
|
||||||
(persist-font-object [data mtype]
|
|
||||||
(when-let [resource (get data mtype)]
|
(when-let [resource (get data mtype)]
|
||||||
(p/let [hash (calculate-hash resource)
|
(let [hash (sto/calculate-hash resource)
|
||||||
content (-> (sto/content resource)
|
content (-> (sto/content resource)
|
||||||
(sto/wrap-with-hash hash))]
|
(sto/wrap-with-hash hash))]
|
||||||
(sto/put-object! storage {::sto/content content
|
{::sto/content content
|
||||||
::sto/touched-at (dt/now)
|
::sto/touched-at (dt/now)
|
||||||
::sto/deduplicate? true
|
::sto/deduplicate? true
|
||||||
:content-type mtype
|
:content-type mtype
|
||||||
:bucket "team-font-variant"}))))
|
:bucket "team-font-variant"})))
|
||||||
|
|
||||||
(persist-fonts [data]
|
(persist-fonts-files! [data]
|
||||||
(p/let [otf (persist-font-object data "font/otf")
|
(let [otf-params (prepare-font data "font/otf")
|
||||||
ttf (persist-font-object data "font/ttf")
|
ttf-params (prepare-font data "font/ttf")
|
||||||
woff1 (persist-font-object data "font/woff")
|
wf1-params (prepare-font data "font/woff")
|
||||||
woff2 (persist-font-object data "font/woff2")]
|
wf2-params (prepare-font data "font/woff2")]
|
||||||
|
(cond-> {}
|
||||||
|
(some? otf-params)
|
||||||
|
(assoc :otf (sto/put-object! storage otf-params))
|
||||||
|
(some? ttf-params)
|
||||||
|
(assoc :ttf (sto/put-object! storage ttf-params))
|
||||||
|
(some? wf1-params)
|
||||||
|
(assoc :woff1 (sto/put-object! storage wf1-params))
|
||||||
|
(some? wf2-params)
|
||||||
|
(assoc :woff2 (sto/put-object! storage wf2-params)))))
|
||||||
|
|
||||||
(d/without-nils
|
(insert-font-variant! [{:keys [woff1 woff2 otf ttf]}]
|
||||||
{:otf otf
|
|
||||||
:ttf ttf
|
|
||||||
:woff1 woff1
|
|
||||||
:woff2 woff2})))
|
|
||||||
|
|
||||||
(insert-into-db [{:keys [woff1 woff2 otf ttf]}]
|
|
||||||
(db/insert! pool :team-font-variant
|
(db/insert! pool :team-font-variant
|
||||||
{:id (uuid/next)
|
{:id (uuid/next)
|
||||||
:team-id (:team-id params)
|
:team-id (:team-id params)
|
||||||
@ -164,13 +156,11 @@
|
|||||||
:ttf-file-id (:id ttf)}))
|
:ttf-file-id (:id ttf)}))
|
||||||
]
|
]
|
||||||
|
|
||||||
(->> (generate-fonts data)
|
(let [data (-> (climit/configure cfg :process-font)
|
||||||
(p/fmap validate-data)
|
(climit/submit! (partial generate-missing! data)))
|
||||||
(p/mcat executor persist-fonts)
|
assets (persist-fonts-files! data)
|
||||||
(p/fmap executor insert-into-db)
|
result (insert-font-variant! assets)]
|
||||||
(p/fmap (fn [result]
|
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
|
||||||
(let [params (update params :data (comp vec keys))]
|
|
||||||
(rph/with-meta result {::audit/replace-props params})))))))
|
|
||||||
|
|
||||||
;; --- UPDATE FONT FAMILY
|
;; --- UPDATE FONT FAMILY
|
||||||
|
|
||||||
|
|||||||
@ -22,13 +22,9 @@
|
|||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.storage.tmp :as tmp]
|
[app.storage.tmp :as tmp]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
|
||||||
[app.worker :as-alias wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[datoteka.io :as io]
|
[datoteka.io :as io]))
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
(def default-max-file-size
|
(def default-max-file-size
|
||||||
(* 1024 1024 10)) ; 10 MiB
|
(* 1024 1024 10)) ; 10 MiB
|
||||||
@ -110,71 +106,62 @@
|
|||||||
;; witch holds the reference to storage object (it some kind of
|
;; witch holds the reference to storage object (it some kind of
|
||||||
;; inverse, soft referential integrity).
|
;; inverse, soft referential integrity).
|
||||||
|
|
||||||
|
(defn- process-main-image
|
||||||
|
[info]
|
||||||
|
(let [hash (sto/calculate-hash (:path info))
|
||||||
|
data (-> (sto/content (:path info))
|
||||||
|
(sto/wrap-with-hash hash))]
|
||||||
|
{::sto/content data
|
||||||
|
::sto/deduplicate? true
|
||||||
|
::sto/touched-at (:ts info)
|
||||||
|
:content-type (:mtype info)
|
||||||
|
:bucket "file-media-object"}))
|
||||||
|
|
||||||
|
(defn- process-thumb-image
|
||||||
|
[info]
|
||||||
|
(let [thumb (-> thumbnail-options
|
||||||
|
(assoc :cmd :generic-thumbnail)
|
||||||
|
(assoc :input info)
|
||||||
|
(media/run))
|
||||||
|
hash (sto/calculate-hash (:data thumb))
|
||||||
|
data (-> (sto/content (:data thumb) (:size thumb))
|
||||||
|
(sto/wrap-with-hash hash))]
|
||||||
|
{::sto/content data
|
||||||
|
::sto/deduplicate? true
|
||||||
|
::sto/touched-at (:ts info)
|
||||||
|
:content-type (:mtype thumb)
|
||||||
|
:bucket "file-media-object"}))
|
||||||
|
|
||||||
|
(defn- process-image
|
||||||
|
[content]
|
||||||
|
(let [info (media/run {:cmd :info :input content})]
|
||||||
|
(cond-> info
|
||||||
|
(and (not (svg-image? info))
|
||||||
|
(big-enough-for-thumbnail? info))
|
||||||
|
(assoc ::thumb (process-thumb-image info))
|
||||||
|
|
||||||
|
:always
|
||||||
|
(assoc ::image (process-main-image info)))))
|
||||||
|
|
||||||
(defn create-file-media-object
|
(defn create-file-media-object
|
||||||
[{:keys [::sto/storage ::db/pool climit ::wrk/executor]}
|
[{:keys [::sto/storage ::db/pool] :as cfg}
|
||||||
{:keys [id file-id is-local name content]}]
|
{:keys [id file-id is-local name content]}]
|
||||||
(letfn [;; Function responsible to retrieve the file information, as
|
|
||||||
;; it is synchronous operation it should be wrapped into
|
|
||||||
;; with-dispatch macro.
|
|
||||||
(get-info [content]
|
|
||||||
(climit/with-dispatch (:process-image climit)
|
|
||||||
(media/run {:cmd :info :input content})))
|
|
||||||
|
|
||||||
;; Function responsible of calculating cryptographyc hash of
|
(let [result (-> (climit/configure cfg :process-image)
|
||||||
;; the provided data.
|
(climit/submit! (partial process-image content)))
|
||||||
(calculate-hash [data]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(sto/calculate-hash data)))
|
|
||||||
|
|
||||||
;; Function responsible of generating thumnail. As it is synchronous
|
image (sto/put-object! storage (::image result))
|
||||||
;; opetation, it should be wrapped into with-dispatch macro
|
thumb (when-let [params (::thumb result)]
|
||||||
(generate-thumbnail [info]
|
(sto/put-object! storage params))]
|
||||||
(climit/with-dispatch (:process-image climit)
|
|
||||||
(media/run (assoc thumbnail-options
|
|
||||||
:cmd :generic-thumbnail
|
|
||||||
:input info))))
|
|
||||||
|
|
||||||
(create-thumbnail [info]
|
(db/exec-one! pool [sql:create-file-media-object
|
||||||
(when (and (not (svg-image? info))
|
(or id (uuid/next))
|
||||||
(big-enough-for-thumbnail? info))
|
file-id is-local name
|
||||||
(p/let [thumb (generate-thumbnail info)
|
(:id image)
|
||||||
hash (calculate-hash (:data thumb))
|
(:id thumb)
|
||||||
content (-> (sto/content (:data thumb) (:size thumb))
|
(:width result)
|
||||||
(sto/wrap-with-hash hash))]
|
(:height result)
|
||||||
(sto/put-object! storage
|
(:mtype result)])))
|
||||||
{::sto/content content
|
|
||||||
::sto/deduplicate? true
|
|
||||||
::sto/touched-at (dt/now)
|
|
||||||
:content-type (:mtype thumb)
|
|
||||||
:bucket "file-media-object"}))))
|
|
||||||
|
|
||||||
(create-image [info]
|
|
||||||
(p/let [data (:path info)
|
|
||||||
hash (calculate-hash data)
|
|
||||||
content (-> (sto/content data)
|
|
||||||
(sto/wrap-with-hash hash))]
|
|
||||||
(sto/put-object! storage
|
|
||||||
{::sto/content content
|
|
||||||
::sto/deduplicate? true
|
|
||||||
::sto/touched-at (dt/now)
|
|
||||||
:content-type (:mtype info)
|
|
||||||
:bucket "file-media-object"})))
|
|
||||||
|
|
||||||
(insert-into-database [info image thumb]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(db/exec-one! pool [sql:create-file-media-object
|
|
||||||
(or id (uuid/next))
|
|
||||||
file-id is-local name
|
|
||||||
(:id image)
|
|
||||||
(:id thumb)
|
|
||||||
(:width info)
|
|
||||||
(:height info)
|
|
||||||
(:mtype info)])))]
|
|
||||||
|
|
||||||
(p/let [info (get-info content)
|
|
||||||
thumb (create-thumbnail info)
|
|
||||||
image (create-image info)]
|
|
||||||
(insert-into-database info image thumb))))
|
|
||||||
|
|
||||||
;; --- Create File Media Object (from URL)
|
;; --- Create File Media Object (from URL)
|
||||||
|
|
||||||
@ -192,9 +179,9 @@
|
|||||||
(files/check-edition-permissions! pool profile-id file-id)
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
(create-file-media-object-from-url cfg params)))
|
(create-file-media-object-from-url cfg params)))
|
||||||
|
|
||||||
(defn- create-file-media-object-from-url
|
(defn- download-image
|
||||||
[cfg {:keys [url name] :as params}]
|
[{:keys [::http/client]} uri]
|
||||||
(letfn [(parse-and-validate-size [headers]
|
(letfn [(parse-and-validate [{:keys [headers] :as response}]
|
||||||
(let [size (some-> (get headers "content-length") d/parse-integer)
|
(let [size (some-> (get headers "content-length") d/parse-integer)
|
||||||
mtype (get headers "content-type")
|
mtype (get headers "content-type")
|
||||||
format (cm/mtype->format mtype)
|
format (cm/mtype->format mtype)
|
||||||
@ -217,32 +204,34 @@
|
|||||||
:code :media-type-not-allowed
|
:code :media-type-not-allowed
|
||||||
:hint "seems like the url points to an invalid media object"))
|
:hint "seems like the url points to an invalid media object"))
|
||||||
|
|
||||||
{:size size
|
{:size size :mtype mtype :format format}))]
|
||||||
:mtype mtype
|
|
||||||
:format format}))
|
|
||||||
|
|
||||||
(download-media [uri]
|
(let [{:keys [body] :as response} (http/req! client
|
||||||
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
|
{:method :get :uri uri}
|
||||||
(p/then process-response)))
|
{:response-type :input-stream :sync? true})
|
||||||
|
{:keys [size mtype]} (parse-and-validate response)
|
||||||
|
|
||||||
(process-response [{:keys [body headers] :as response}]
|
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||||
(let [{:keys [size mtype]} (parse-and-validate-size headers)
|
written (io/write-to-file! body path :size size)]
|
||||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
|
||||||
written (io/write-to-file! body path :size size)]
|
|
||||||
|
|
||||||
(when (not= written size)
|
(when (not= written size)
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
:code :mismatch-write-size
|
:code :mismatch-write-size
|
||||||
:hint "unexpected state: unable to write to file"))
|
:hint "unexpected state: unable to write to file"))
|
||||||
|
|
||||||
{:filename "tempfile"
|
{:filename "tempfile"
|
||||||
:size size
|
:size size
|
||||||
:path path
|
:path path
|
||||||
:mtype mtype}))]
|
:mtype mtype})))
|
||||||
|
|
||||||
(p/let [content (download-media url)]
|
|
||||||
(->> (merge params {:content content :name (or name (:filename content))})
|
(defn- create-file-media-object-from-url
|
||||||
(create-file-media-object cfg)))))
|
[cfg {:keys [url name] :as params}]
|
||||||
|
(let [content (download-image cfg url)
|
||||||
|
params (-> params
|
||||||
|
(assoc :content content)
|
||||||
|
(assoc :name (or name (:filename content))))]
|
||||||
|
(create-file-media-object cfg params)))
|
||||||
|
|
||||||
;; --- Clone File Media object (Upload and create from url)
|
;; --- Clone File Media object (Upload and create from url)
|
||||||
|
|
||||||
|
|||||||
@ -26,17 +26,16 @@
|
|||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as-alias wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]))
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
|
(declare check-profile-existence!)
|
||||||
(declare decode-row)
|
(declare decode-row)
|
||||||
|
(declare derive-password)
|
||||||
|
(declare filter-props)
|
||||||
(declare get-profile)
|
(declare get-profile)
|
||||||
(declare strip-private-attrs)
|
(declare strip-private-attrs)
|
||||||
(declare filter-props)
|
(declare verify-password)
|
||||||
(declare check-profile-existence!)
|
|
||||||
|
|
||||||
;; --- QUERY: Get profile (own)
|
;; --- QUERY: Get profile (own)
|
||||||
|
|
||||||
@ -50,6 +49,7 @@
|
|||||||
;; We need to return the anonymous profile object in two cases, when
|
;; We need to return the anonymous profile object in two cases, when
|
||||||
;; no profile-id is in session, and when db call raises not found. In all other
|
;; no profile-id is in session, and when db call raises not found. In all other
|
||||||
;; cases we need to reraise the exception.
|
;; cases we need to reraise the exception.
|
||||||
|
|
||||||
(try
|
(try
|
||||||
(-> (get-profile pool profile-id)
|
(-> (get-profile pool profile-id)
|
||||||
(strip-private-attrs)
|
(strip-private-attrs)
|
||||||
@ -120,10 +120,10 @@
|
|||||||
:req-un [::password ::old-password]))
|
:req-un [::password ::old-password]))
|
||||||
|
|
||||||
(sv/defmethod ::update-profile-password
|
(sv/defmethod ::update-profile-password
|
||||||
{::climit/queue :auth}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [profile (validate-password! conn (assoc params :profile-id profile-id))
|
(let [cfg (assoc cfg ::db/conn conn)
|
||||||
|
profile (validate-password! cfg (assoc params :profile-id profile-id))
|
||||||
session-id (::session/id params)]
|
session-id (::session/id params)]
|
||||||
|
|
||||||
(when (= (str/lower (:email profile))
|
(when (= (str/lower (:email profile))
|
||||||
@ -132,29 +132,30 @@
|
|||||||
:code :email-as-password
|
:code :email-as-password
|
||||||
:hint "you can't use your email as password"))
|
:hint "you can't use your email as password"))
|
||||||
|
|
||||||
(update-profile-password! conn (assoc profile :password password))
|
(update-profile-password! cfg (assoc profile :password password))
|
||||||
(invalidate-profile-session! conn profile-id session-id)
|
(invalidate-profile-session! cfg profile-id session-id)
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
(defn- invalidate-profile-session!
|
(defn- invalidate-profile-session!
|
||||||
"Removes all sessions except the current one."
|
"Removes all sessions except the current one."
|
||||||
[conn profile-id session-id]
|
[{:keys [::db/conn]} profile-id session-id]
|
||||||
(let [sql "delete from http_session where profile_id = ? and id != ?"]
|
(let [sql "delete from http_session where profile_id = ? and id != ?"]
|
||||||
(:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id]))))
|
(:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id]))))
|
||||||
|
|
||||||
(defn- validate-password!
|
(defn- validate-password!
|
||||||
[conn {:keys [profile-id old-password] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
|
||||||
(let [profile (db/get-by-id conn :profile profile-id ::db/for-update? true)]
|
(let [profile (db/get-by-id conn :profile profile-id ::db/for-update? true)]
|
||||||
(when-not (:valid (auth/verify-password old-password (:password profile)))
|
(when-not (:valid (verify-password cfg old-password (:password profile)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :old-password-not-match))
|
:code :old-password-not-match))
|
||||||
profile))
|
profile))
|
||||||
|
|
||||||
(defn update-profile-password!
|
(defn update-profile-password!
|
||||||
[conn {:keys [id password] :as profile}]
|
[{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}]
|
||||||
(db/update! conn :profile
|
(let [password (derive-password cfg password)]
|
||||||
{:password (auth/derive-password password)}
|
(db/update! conn :profile
|
||||||
{:id id}))
|
{:password password}
|
||||||
|
{:id id})))
|
||||||
|
|
||||||
;; --- MUTATION: Update Photo
|
;; --- MUTATION: Update Photo
|
||||||
|
|
||||||
@ -173,61 +174,49 @@
|
|||||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||||
(update-profile-photo cfg (assoc params :profile-id profile-id))))
|
(update-profile-photo cfg (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
;; TODO: reimplement it without p/let
|
|
||||||
|
|
||||||
(defn update-profile-photo
|
(defn update-profile-photo
|
||||||
[{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id file] :as params}]
|
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}]
|
||||||
(letfn [(on-uploaded [photo]
|
(let [photo (upload-photo cfg params)
|
||||||
(let [profile (db/get-by-id pool :profile profile-id ::db/for-update? true)]
|
profile (db/get-by-id pool :profile profile-id ::db/for-update? true)]
|
||||||
|
|
||||||
;; Schedule deletion of old photo
|
;; Schedule deletion of old photo
|
||||||
(when-let [id (:photo-id profile)]
|
(when-let [id (:photo-id profile)]
|
||||||
(sto/touch-object! storage id))
|
(sto/touch-object! storage id))
|
||||||
|
|
||||||
;; Save new photo
|
;; Save new photo
|
||||||
(db/update! pool :profile
|
(db/update! pool :profile
|
||||||
{:photo-id (:id photo)}
|
{:photo-id (:id photo)}
|
||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
|
|
||||||
(-> (rph/wrap)
|
(-> (rph/wrap)
|
||||||
(rph/with-meta {::audit/replace-props
|
(rph/with-meta {::audit/replace-props
|
||||||
{:file-name (:filename file)
|
{:file-name (:filename file)
|
||||||
:file-size (:size file)
|
:file-size (:size file)
|
||||||
:file-path (str (:path file))
|
:file-path (str (:path file))
|
||||||
:file-mtype (:mtype file)}}))))]
|
:file-mtype (:mtype file)}}))))
|
||||||
(->> (upload-photo cfg params)
|
|
||||||
(p/fmap executor on-uploaded))))
|
(defn- generate-thumbnail!
|
||||||
|
[file]
|
||||||
|
(let [input (media/run {:cmd :info :input file})
|
||||||
|
thumb (media/run {:cmd :profile-thumbnail
|
||||||
|
:format :jpeg
|
||||||
|
:quality 85
|
||||||
|
:width 256
|
||||||
|
:height 256
|
||||||
|
:input input})
|
||||||
|
hash (sto/calculate-hash (:data thumb))
|
||||||
|
content (-> (sto/content (:data thumb) (:size thumb))
|
||||||
|
(sto/wrap-with-hash hash))]
|
||||||
|
{::sto/content content
|
||||||
|
::sto/deduplicate? true
|
||||||
|
:bucket "profile"
|
||||||
|
:content-type (:mtype thumb)}))
|
||||||
|
|
||||||
(defn upload-photo
|
(defn upload-photo
|
||||||
[{:keys [::sto/storage ::wrk/executor climit] :as cfg} {:keys [file]}]
|
[{:keys [::sto/storage] :as cfg} {:keys [file]}]
|
||||||
(letfn [(get-info [content]
|
(let [params (-> (climit/configure cfg :process-image)
|
||||||
(climit/with-dispatch (:process-image climit)
|
(climit/submit! (partial generate-thumbnail! file)))]
|
||||||
(media/run {:cmd :info :input content})))
|
(sto/put-object! storage params)))
|
||||||
|
|
||||||
(generate-thumbnail [info]
|
|
||||||
(climit/with-dispatch (:process-image climit)
|
|
||||||
(media/run {:cmd :profile-thumbnail
|
|
||||||
:format :jpeg
|
|
||||||
:quality 85
|
|
||||||
:width 256
|
|
||||||
:height 256
|
|
||||||
:input info})))
|
|
||||||
|
|
||||||
;; Function responsible of calculating cryptographyc hash of
|
|
||||||
;; the provided data.
|
|
||||||
(calculate-hash [data]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(sto/calculate-hash data)))]
|
|
||||||
|
|
||||||
(p/let [info (get-info file)
|
|
||||||
thumb (generate-thumbnail info)
|
|
||||||
hash (calculate-hash (:data thumb))
|
|
||||||
content (-> (sto/content (:data thumb) (:size thumb))
|
|
||||||
(sto/wrap-with-hash hash))]
|
|
||||||
(sto/put-object! storage {::sto/content content
|
|
||||||
::sto/deduplicate? true
|
|
||||||
:bucket "profile"
|
|
||||||
:content-type (:mtype thumb)}))))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- MUTATION: Request Email Change
|
;; --- MUTATION: Request Email Change
|
||||||
@ -417,6 +406,17 @@
|
|||||||
[props]
|
[props]
|
||||||
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
|
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
|
||||||
|
|
||||||
|
(defn derive-password
|
||||||
|
[cfg password]
|
||||||
|
(when password
|
||||||
|
(-> (climit/configure cfg :derive-password)
|
||||||
|
(climit/submit! (partial auth/derive-password password)))))
|
||||||
|
|
||||||
|
(defn verify-password
|
||||||
|
[cfg password password-data]
|
||||||
|
(-> (climit/configure cfg :derive-password)
|
||||||
|
(climit/submit! (partial auth/verify-password password password-data))))
|
||||||
|
|
||||||
(defn decode-row
|
(defn decode-row
|
||||||
[{:keys [props] :as row}]
|
[{:keys [props] :as row}]
|
||||||
(cond-> row
|
(cond-> row
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(ns app.rpc.commands.projects
|
(ns app.rpc.commands.projects
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.loggers.audit :as-alias audit]
|
[app.loggers.audit :as-alias audit]
|
||||||
@ -79,7 +80,7 @@
|
|||||||
(sv/defmethod ::get-projects
|
(sv/defmethod ::get-projects
|
||||||
{::doc/added "1.18"}
|
{::doc/added "1.18"}
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
(get-projects conn profile-id team-id)))
|
(get-projects conn profile-id team-id)))
|
||||||
|
|
||||||
@ -114,7 +115,7 @@
|
|||||||
(sv/defmethod ::get-all-projects
|
(sv/defmethod ::get-all-projects
|
||||||
{::doc/added "1.18"}
|
{::doc/added "1.18"}
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(get-all-projects conn profile-id)))
|
(get-all-projects conn profile-id)))
|
||||||
|
|
||||||
(def sql:all-projects
|
(def sql:all-projects
|
||||||
@ -157,7 +158,7 @@
|
|||||||
(sv/defmethod ::get-project
|
(sv/defmethod ::get-project
|
||||||
{::doc/added "1.18"}
|
{::doc/added "1.18"}
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(let [project (db/get-by-id conn :project id)]
|
(let [project (db/get-by-id conn :project id)]
|
||||||
(check-read-permissions! conn profile-id id)
|
(check-read-permissions! conn profile-id id)
|
||||||
project)))
|
project)))
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
(ns app.rpc.commands.teams
|
(ns app.rpc.commands.teams
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
@ -27,11 +28,8 @@
|
|||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as-alias wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]))
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
;; --- Helpers & Specs
|
;; --- Helpers & Specs
|
||||||
|
|
||||||
@ -78,13 +76,15 @@
|
|||||||
|
|
||||||
(declare retrieve-teams)
|
(declare retrieve-teams)
|
||||||
|
|
||||||
|
(def counter (volatile! 0))
|
||||||
|
|
||||||
(s/def ::get-teams
|
(s/def ::get-teams
|
||||||
(s/keys :req [::rpc/profile-id]))
|
(s/keys :req [::rpc/profile-id]))
|
||||||
|
|
||||||
(sv/defmethod ::get-teams
|
(sv/defmethod ::get-teams
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(retrieve-teams conn profile-id)))
|
(retrieve-teams conn profile-id)))
|
||||||
|
|
||||||
(def sql:teams
|
(def sql:teams
|
||||||
@ -129,7 +129,7 @@
|
|||||||
(sv/defmethod ::get-team
|
(sv/defmethod ::get-team
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(retrieve-team conn profile-id id)))
|
(retrieve-team conn profile-id id)))
|
||||||
|
|
||||||
(defn retrieve-team
|
(defn retrieve-team
|
||||||
@ -170,7 +170,7 @@
|
|||||||
(sv/defmethod ::get-team-members
|
(sv/defmethod ::get-team-members
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id team-id)
|
(check-read-permissions! conn profile-id team-id)
|
||||||
(retrieve-team-members conn team-id)))
|
(retrieve-team-members conn team-id)))
|
||||||
|
|
||||||
@ -188,7 +188,7 @@
|
|||||||
(sv/defmethod ::get-team-users
|
(sv/defmethod ::get-team-users
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(if team-id
|
(if team-id
|
||||||
(do
|
(do
|
||||||
(check-read-permissions! conn profile-id team-id)
|
(check-read-permissions! conn profile-id team-id)
|
||||||
@ -246,7 +246,7 @@
|
|||||||
(sv/defmethod ::get-team-stats
|
(sv/defmethod ::get-team-stats
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id team-id)
|
(check-read-permissions! conn profile-id team-id)
|
||||||
(retrieve-team-stats conn team-id)))
|
(retrieve-team-stats conn team-id)))
|
||||||
|
|
||||||
@ -277,7 +277,7 @@
|
|||||||
(sv/defmethod ::get-team-invitations
|
(sv/defmethod ::get-team-invitations
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id team-id)
|
(check-read-permissions! conn profile-id team-id)
|
||||||
(get-team-invitations conn team-id)))
|
(get-team-invitations conn team-id)))
|
||||||
|
|
||||||
@ -588,10 +588,9 @@
|
|||||||
(update-team-photo cfg (assoc params :profile-id profile-id))))
|
(update-team-photo cfg (assoc params :profile-id profile-id))))
|
||||||
|
|
||||||
(defn update-team-photo
|
(defn update-team-photo
|
||||||
[{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id team-id] :as params}]
|
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||||
(p/let [team (px/with-dispatch executor
|
(let [team (retrieve-team pool profile-id team-id)
|
||||||
(retrieve-team pool profile-id team-id))
|
photo (profile/upload-photo cfg params)]
|
||||||
photo (profile/upload-photo cfg params)]
|
|
||||||
|
|
||||||
;; Mark object as touched for make it ellegible for tentative
|
;; Mark object as touched for make it ellegible for tentative
|
||||||
;; garbage collection.
|
;; garbage collection.
|
||||||
@ -694,13 +693,13 @@
|
|||||||
(l/info :hint "invitation token" :token itoken))
|
(l/info :hint "invitation token" :token itoken))
|
||||||
|
|
||||||
(audit/submit! cfg
|
(audit/submit! cfg
|
||||||
{:type "action"
|
{::audit/type "action"
|
||||||
:name (if updated?
|
::audit/name (if updated?
|
||||||
"update-team-invitation"
|
"update-team-invitation"
|
||||||
"create-team-invitation")
|
"create-team-invitation")
|
||||||
:profile-id (:id profile)
|
::audit/profile-id (:id profile)
|
||||||
:props (-> (dissoc tprops :profile-id)
|
::audit/props (-> (dissoc tprops :profile-id)
|
||||||
(d/without-nils))})
|
(d/without-nils))})
|
||||||
|
|
||||||
(eml/send! {::eml/conn conn
|
(eml/send! {::eml/conn conn
|
||||||
::eml/factory eml/invite-to-team
|
::eml/factory eml/invite-to-team
|
||||||
@ -802,13 +801,13 @@
|
|||||||
::quotes/incr (count emails)}))
|
::quotes/incr (count emails)}))
|
||||||
|
|
||||||
(audit/submit! cfg
|
(audit/submit! cfg
|
||||||
{:type "command"
|
{::audit/type "command"
|
||||||
:name "create-team-invitations"
|
::audit/name "create-team-invitations"
|
||||||
:profile-id profile-id
|
::audit/profile-id profile-id
|
||||||
:props {:emails emails
|
::audit/props {:emails emails
|
||||||
:role role
|
:role role
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:invitations (count emails)}})
|
:invitations (count emails)}})
|
||||||
|
|
||||||
(vary-meta team assoc ::audit/props {:invitations (count emails)}))))
|
(vary-meta team assoc ::audit/props {:invitations (count emails)}))))
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(ns app.rpc.commands.viewer
|
(ns app.rpc.commands.viewer
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
@ -85,5 +86,5 @@
|
|||||||
::cond/reuse-key? true
|
::cond/reuse-key? true
|
||||||
::doc/added "1.17"}
|
::doc/added "1.17"}
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(get-view-only-bundle conn (assoc params :profile-id profile-id))))
|
(get-view-only-bundle conn (assoc params :profile-id profile-id))))
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(ns app.rpc.commands.webhooks
|
(ns app.rpc.commands.webhooks
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
@ -18,10 +19,8 @@
|
|||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as-alias wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]))
|
||||||
[promesa.core :as p]))
|
|
||||||
|
|
||||||
(defn decode-row
|
(defn decode-row
|
||||||
[{:keys [uri] :as row}]
|
[{:keys [uri] :as row}]
|
||||||
@ -48,30 +47,26 @@
|
|||||||
|
|
||||||
(defn- validate-webhook!
|
(defn- validate-webhook!
|
||||||
[cfg whook params]
|
[cfg whook params]
|
||||||
(letfn [(handle-exception [exception]
|
(when (not= (:uri whook) (:uri params))
|
||||||
(if-let [hint (webhooks/interpret-exception exception)]
|
(try
|
||||||
(ex/raise :type :validation
|
(let [response (http/req! cfg
|
||||||
:code :webhook-validation
|
{:method :head
|
||||||
:hint hint)
|
:uri (str (:uri params))
|
||||||
(ex/raise :type :internal
|
:timeout (dt/duration "3s")}
|
||||||
:code :webhook-validation
|
{:sync? true})]
|
||||||
:cause exception)))
|
(when-let [hint (webhooks/interpret-response response)]
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :webhook-validation
|
||||||
|
:hint hint)))
|
||||||
|
|
||||||
(handle-response [response]
|
(catch Throwable cause
|
||||||
(when-let [hint (webhooks/interpret-response response)]
|
(if-let [hint (webhooks/interpret-exception cause)]
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :webhook-validation
|
:code :webhook-validation
|
||||||
:hint hint)))]
|
:hint hint)
|
||||||
|
(ex/raise :type :internal
|
||||||
(if (not= (:uri whook) (:uri params))
|
:code :webhook-validation
|
||||||
(->> (http/req! cfg {:method :head
|
:cause cause))))))
|
||||||
:uri (str (:uri params))
|
|
||||||
:timeout (dt/duration "3s")})
|
|
||||||
(p/hmap (fn [response exception]
|
|
||||||
(if exception
|
|
||||||
(handle-exception exception)
|
|
||||||
(handle-response response)))))
|
|
||||||
(p/resolved nil))))
|
|
||||||
|
|
||||||
(defn- validate-quotes!
|
(defn- validate-quotes!
|
||||||
[{:keys [::db/pool]} {:keys [team-id]}]
|
[{:keys [::db/pool]} {:keys [team-id]}]
|
||||||
@ -106,22 +101,22 @@
|
|||||||
|
|
||||||
(sv/defmethod ::create-webhook
|
(sv/defmethod ::create-webhook
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||||
(check-edition-permissions! pool profile-id team-id)
|
(check-edition-permissions! pool profile-id team-id)
|
||||||
(validate-quotes! cfg params)
|
(validate-quotes! cfg params)
|
||||||
(->> (validate-webhook! cfg nil params)
|
(validate-webhook! cfg nil params)
|
||||||
(p/fmap executor (fn [_] (insert-webhook! cfg params)))))
|
(insert-webhook! cfg params))
|
||||||
|
|
||||||
(s/def ::update-webhook
|
(s/def ::update-webhook
|
||||||
(s/keys :req-un [::id ::uri ::mtype ::is-active]))
|
(s/keys :req-un [::id ::uri ::mtype ::is-active]))
|
||||||
|
|
||||||
(sv/defmethod ::update-webhook
|
(sv/defmethod ::update-webhook
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
|
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
|
||||||
(check-edition-permissions! pool profile-id (:team-id whook))
|
(check-edition-permissions! pool profile-id (:team-id whook))
|
||||||
(->> (validate-webhook! cfg whook params)
|
(validate-webhook! cfg whook params)
|
||||||
(p/fmap executor (fn [_] (update-webhook! cfg whook params))))))
|
(update-webhook! cfg whook params)))
|
||||||
|
|
||||||
(s/def ::delete-webhook
|
(s/def ::delete-webhook
|
||||||
(s/keys :req [::rpc/profile-id]
|
(s/keys :req [::rpc/profile-id]
|
||||||
@ -149,7 +144,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-webhooks
|
(sv/defmethod ::get-webhooks
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(check-read-permissions! conn profile-id team-id)
|
(check-read-permissions! conn profile-id team-id)
|
||||||
(->> (db/exec! conn [sql:get-webhooks team-id])
|
(->> (db/exec! conn [sql:get-webhooks team-id])
|
||||||
(mapv decode-row))))
|
(mapv decode-row))))
|
||||||
|
|||||||
@ -27,8 +27,6 @@
|
|||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.util.services :as-alias sv]
|
[app.util.services :as-alias sv]
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]
|
|
||||||
[yetti.response :as yrs]))
|
[yetti.response :as yrs]))
|
||||||
|
|
||||||
(def
|
(def
|
||||||
@ -38,30 +36,24 @@
|
|||||||
|
|
||||||
(defn- fmt-key
|
(defn- fmt-key
|
||||||
[s]
|
[s]
|
||||||
(when s
|
(str "W/\"" s "\""))
|
||||||
(str "W/\"" s "\"")))
|
|
||||||
|
|
||||||
(defn wrap
|
(defn wrap
|
||||||
[{:keys [executor]} f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}]
|
[_ f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}]
|
||||||
(if (and (ifn? get-object) (ifn? key-fn))
|
(if (and (ifn? get-object) (ifn? key-fn))
|
||||||
(do
|
(do
|
||||||
(l/debug :hint "instrumenting method" :service (::sv/name mdata))
|
(l/debug :hint "instrumenting method" :service (::sv/name mdata))
|
||||||
(fn [cfg {:keys [::key] :as params}]
|
(fn [cfg {:keys [::key] :as params}]
|
||||||
(if *enabled*
|
(if *enabled*
|
||||||
(->> (if (or key reuse-key?)
|
(let [key' (when (or key reuse-key?)
|
||||||
(->> (px/submit! executor (partial get-object cfg params))
|
(some-> (get-object cfg params) key-fn fmt-key))]
|
||||||
(p/map key-fn)
|
(if (and (some? key)
|
||||||
(p/map fmt-key))
|
(= key key'))
|
||||||
(p/resolved nil))
|
(fn [_] {::yrs/status 304})
|
||||||
(p/mapcat (fn [key']
|
(let [result (f cfg params)
|
||||||
(if (and (some? key)
|
etag (or (and reuse-key? key')
|
||||||
(= key key'))
|
(some-> result meta ::key fmt-key)
|
||||||
(p/resolved (fn [_] (yrs/response 304)))
|
(some-> result key-fn fmt-key))]
|
||||||
(->> (f cfg params)
|
(rph/with-header result "etag" etag))))
|
||||||
(p/map (fn [result]
|
|
||||||
(->> (or (and reuse-key? key')
|
|
||||||
(-> result meta ::key fmt-key)
|
|
||||||
(-> result key-fn fmt-key))
|
|
||||||
(rph/with-header result "etag")))))))))
|
|
||||||
(f cfg params))))
|
(f cfg params))))
|
||||||
f))
|
f))
|
||||||
|
|||||||
@ -30,32 +30,34 @@
|
|||||||
|
|
||||||
(defn- prepare-context
|
(defn- prepare-context
|
||||||
[methods]
|
[methods]
|
||||||
(letfn [(gen-doc [type [name f]]
|
(letfn [(gen-doc [type [{:keys [::sv/name] :as mdata} _f]]
|
||||||
(let [mdata (meta f)]
|
{:type (d/name type)
|
||||||
{:type (d/name type)
|
:name (d/name name)
|
||||||
:name (d/name name)
|
:module (-> (:ns mdata) (str/split ".") last)
|
||||||
:module (-> (:ns mdata) (str/split ".") last)
|
:auth (:auth mdata true)
|
||||||
:auth (:auth mdata true)
|
:webhook (::webhooks/event? mdata false)
|
||||||
:webhook (::webhooks/event? mdata false)
|
:docs (::sv/docstring mdata)
|
||||||
:docs (::sv/docstring mdata)
|
:deprecated (::deprecated mdata)
|
||||||
:deprecated (::deprecated mdata)
|
:added (::added mdata)
|
||||||
:added (::added mdata)
|
:changes (some->> (::changes mdata) (partition-all 2) (map vec))
|
||||||
:changes (some->> (::changes mdata) (partition-all 2) (map vec))
|
:spec (get-spec-str (::sv/spec mdata))})]
|
||||||
:spec (get-spec-str (::sv/spec mdata))}))]
|
|
||||||
|
|
||||||
{:version (:main cf/version)
|
{:version (:main cf/version)
|
||||||
:command-methods
|
:command-methods
|
||||||
(->> (:commands methods)
|
(->> (:commands methods)
|
||||||
|
(map val)
|
||||||
(map (partial gen-doc :command))
|
(map (partial gen-doc :command))
|
||||||
(sort-by (juxt :module :name)))
|
(sort-by (juxt :module :name)))
|
||||||
|
|
||||||
:query-methods
|
:query-methods
|
||||||
(->> (:queries methods)
|
(->> (:queries methods)
|
||||||
|
(map val)
|
||||||
(map (partial gen-doc :query))
|
(map (partial gen-doc :query))
|
||||||
(sort-by (juxt :module :name)))
|
(sort-by (juxt :module :name)))
|
||||||
|
|
||||||
:mutation-methods
|
:mutation-methods
|
||||||
(->> (:mutations methods)
|
(->> (:mutations methods)
|
||||||
|
(map val)
|
||||||
(map (partial gen-doc :query))
|
(map (partial gen-doc :query))
|
||||||
(sort-by (juxt :module :name)))}))
|
(sort-by (juxt :module :name)))}))
|
||||||
|
|
||||||
@ -64,11 +66,11 @@
|
|||||||
(if (contains? cf/flags :backend-api-doc)
|
(if (contains? cf/flags :backend-api-doc)
|
||||||
(let [context (prepare-context methods)]
|
(let [context (prepare-context methods)]
|
||||||
(fn [_ respond _]
|
(fn [_ respond _]
|
||||||
(respond (yrs/response 200 (-> (io/resource "app/templates/api-doc.tmpl")
|
(respond {::yrs/status 200
|
||||||
(tmpl/render context))))))
|
::yrs/body (-> (io/resource "app/templates/api-doc.tmpl")
|
||||||
|
(tmpl/render context))})))
|
||||||
(fn [_ respond _]
|
(fn [_ respond _]
|
||||||
(respond (yrs/response 404)))))
|
(respond {::yrs/status 404}))))
|
||||||
|
|
||||||
|
|
||||||
(s/def ::routes vector?)
|
(s/def ::routes vector?)
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.http :as-alias http]
|
[app.http :as-alias http]
|
||||||
[app.rpc :as-alias rpc]))
|
[app.rpc :as-alias rpc]
|
||||||
|
[yetti.response :as-alias yrs]))
|
||||||
|
|
||||||
;; A utilty wrapper object for wrap service responses that does not
|
;; A utilty wrapper object for wrap service responses that does not
|
||||||
;; implements the IObj interface that make possible attach metadata to
|
;; implements the IObj interface that make possible attach metadata to
|
||||||
@ -35,7 +36,9 @@
|
|||||||
o
|
o
|
||||||
(MetadataWrapper. o {})))
|
(MetadataWrapper. o {})))
|
||||||
([o m]
|
([o m]
|
||||||
(MetadataWrapper. o m)))
|
(if (instance? clojure.lang.IObj o)
|
||||||
|
(vary-meta o merge m)
|
||||||
|
(MetadataWrapper. o m))))
|
||||||
|
|
||||||
(defn wrapped?
|
(defn wrapped?
|
||||||
[o]
|
[o]
|
||||||
@ -74,4 +77,4 @@
|
|||||||
(fn [_ response]
|
(fn [_ response]
|
||||||
(let [exp (if (integer? max-age) max-age (inst-ms max-age))
|
(let [exp (if (integer? max-age) max-age (inst-ms max-age))
|
||||||
val (dm/fmt "max-age=%" (int (/ exp 1000.0)))]
|
val (dm/fmt "max-age=%" (int (/ exp 1000.0)))]
|
||||||
(update response :headers assoc "cache-control" val)))))
|
(update response ::yrs/headers assoc "cache-control" val)))))
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc.climit :as-alias climit]
|
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
@ -78,20 +77,20 @@
|
|||||||
(s/keys :req-un [::profile-id ::password ::old-password]))
|
(s/keys :req-un [::profile-id ::password ::old-password]))
|
||||||
|
|
||||||
(sv/defmethod ::update-profile-password
|
(sv/defmethod ::update-profile-password
|
||||||
{::climit/queue :auth
|
{::doc/added "1.0"
|
||||||
::doc/added "1.0"
|
|
||||||
::doc/deprecated "1.18"}
|
::doc/deprecated "1.18"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [password] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [password] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [profile (#'profile/validate-password! conn params)
|
(let [cfg (assoc cfg ::db/conn conn)
|
||||||
|
profile (#'profile/validate-password! cfg params)
|
||||||
session-id (::session/id params)]
|
session-id (::session/id params)]
|
||||||
(when (= (str/lower (:email profile))
|
(when (= (str/lower (:email profile))
|
||||||
(str/lower (:password params)))
|
(str/lower (:password params)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :email-as-password
|
:code :email-as-password
|
||||||
:hint "you can't use your email as password"))
|
:hint "you can't use your email as password"))
|
||||||
(profile/update-profile-password! conn (assoc profile :password password))
|
(profile/update-profile-password! cfg (assoc profile :password password))
|
||||||
(#'profile/invalidate-profile-session! conn (:id profile) session-id)
|
(#'profile/invalidate-profile-session! cfg (:id profile) session-id)
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -10,8 +10,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.util.retry :refer [conflict-exception?]]
|
[app.util.retry :refer [conflict-exception?]]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]))
|
||||||
[promesa.core :as p]))
|
|
||||||
|
|
||||||
(defn conflict-db-insert?
|
(defn conflict-db-insert?
|
||||||
"Check if exception matches a insertion conflict on postgresql."
|
"Check if exception matches a insertion conflict on postgresql."
|
||||||
@ -28,18 +27,16 @@
|
|||||||
|
|
||||||
(if-let [max-retries (::max-retries mdata)]
|
(if-let [max-retries (::max-retries mdata)]
|
||||||
(fn [cfg params]
|
(fn [cfg params]
|
||||||
(letfn [(run [retry]
|
((fn run [retry]
|
||||||
(->> (f cfg params)
|
(try
|
||||||
(p/merr (partial handle-error retry))))
|
(f cfg params)
|
||||||
|
(catch Throwable cause
|
||||||
(handle-error [retry cause]
|
(if (matches cause)
|
||||||
(if (matches cause)
|
(let [current-retry (inc retry)]
|
||||||
(let [current-retry (inc retry)]
|
(l/trace :hint "running retry algorithm" :retry current-retry)
|
||||||
(l/trace :hint "running retry algorithm" :retry current-retry)
|
(if (<= current-retry max-retries)
|
||||||
(if (<= current-retry max-retries)
|
(run current-retry)
|
||||||
(run current-retry)
|
(throw cause)))
|
||||||
(throw cause)))
|
(throw cause))))) 1))
|
||||||
(throw cause)))]
|
|
||||||
(run 1)))
|
|
||||||
f))
|
f))
|
||||||
|
|
||||||
|
|||||||
@ -55,6 +55,7 @@
|
|||||||
[app.redis :as rds]
|
[app.redis :as rds]
|
||||||
[app.redis.script :as-alias rscript]
|
[app.redis.script :as-alias rscript]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.rlimit.result :as-alias lresult]
|
[app.rpc.rlimit.result :as-alias lresult]
|
||||||
[app.util.services :as-alias sv]
|
[app.util.services :as-alias sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
@ -64,7 +65,6 @@
|
|||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
(def ^:private default-timeout
|
(def ^:private default-timeout
|
||||||
@ -82,7 +82,7 @@
|
|||||||
{::rscript/name ::window-rate-limit
|
{::rscript/name ::window-rate-limit
|
||||||
::rscript/path "app/rpc/rlimit/window.lua"})
|
::rscript/path "app/rpc/rlimit/window.lua"})
|
||||||
|
|
||||||
(def enabled?
|
(def enabled
|
||||||
"Allows on runtime completely disable rate limiting."
|
"Allows on runtime completely disable rate limiting."
|
||||||
(atom true))
|
(atom true))
|
||||||
|
|
||||||
@ -119,116 +119,97 @@
|
|||||||
(defmethod parse-limit :bucket
|
(defmethod parse-limit :bucket
|
||||||
[[name strategy opts :as vlimit]]
|
[[name strategy opts :as vlimit]]
|
||||||
(us/assert! ::limit-tuple vlimit)
|
(us/assert! ::limit-tuple vlimit)
|
||||||
(merge
|
(if-let [[_ capacity rate interval] (re-find bucket-opts-re opts)]
|
||||||
{::name name
|
(let [interval (dt/duration interval)
|
||||||
::strategy strategy}
|
rate (parse-long rate)
|
||||||
(if-let [[_ capacity rate interval] (re-find bucket-opts-re opts)]
|
capacity (parse-long capacity)]
|
||||||
(let [interval (dt/duration interval)
|
{::name name
|
||||||
rate (parse-long rate)
|
::strategy strategy
|
||||||
capacity (parse-long capacity)]
|
::capacity capacity
|
||||||
{::capacity capacity
|
::rate rate
|
||||||
::rate rate
|
::interval interval
|
||||||
::interval interval
|
::opts opts
|
||||||
::opts opts
|
::params [(dt/->seconds interval) rate capacity]
|
||||||
::params [(dt/->seconds interval) rate capacity]
|
::key (str "ratelimit.bucket." (d/name name))})
|
||||||
::key (str "ratelimit.bucket." (d/name name))})
|
(ex/raise :type :validation
|
||||||
(ex/raise :type :validation
|
:code :invalid-bucket-limit-opts
|
||||||
:code :invalid-bucket-limit-opts
|
:hint (str/ffmt "looks like '%' does not have a valid format" opts))))
|
||||||
:hint (str/ffmt "looks like '%' does not have a valid format" opts)))))
|
|
||||||
|
|
||||||
(defmethod process-limit :bucket
|
(defmethod process-limit :bucket
|
||||||
[redis user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
|
[redis user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
|
||||||
(let [script (-> bucket-rate-limit-script
|
(let [script (-> bucket-rate-limit-script
|
||||||
(assoc ::rscript/keys [(str key "." service "." user-id)])
|
(assoc ::rscript/keys [(str key "." service "." user-id)])
|
||||||
(assoc ::rscript/vals (conj params (dt/->seconds now))))]
|
(assoc ::rscript/vals (conj params (dt/->seconds now))))
|
||||||
(->> (rds/eval! redis script)
|
result (rds/eval! redis script)
|
||||||
(p/fmap (fn [result]
|
allowed? (boolean (nth result 0))
|
||||||
(let [allowed? (boolean (nth result 0))
|
remaining (nth result 1)
|
||||||
remaining (nth result 1)
|
reset (* (/ (inst-ms interval) rate)
|
||||||
reset (* (/ (inst-ms interval) rate)
|
(- capacity remaining))]
|
||||||
(- capacity remaining))]
|
(l/trace :hint "limit processed"
|
||||||
(l/trace :hint "limit processed"
|
:service service
|
||||||
:service service
|
:limit (name (::name limit))
|
||||||
:limit (name (::name limit))
|
:strategy (name (::strategy limit))
|
||||||
:strategy (name (::strategy limit))
|
:opts (::opts limit)
|
||||||
:opts (::opts limit)
|
:allowed allowed?
|
||||||
:allowed? allowed?
|
:remaining remaining)
|
||||||
:remaining remaining)
|
(-> limit
|
||||||
(-> limit
|
(assoc ::lresult/allowed allowed?)
|
||||||
(assoc ::lresult/allowed? allowed?)
|
(assoc ::lresult/reset (dt/plus now reset))
|
||||||
(assoc ::lresult/reset (dt/plus now reset))
|
(assoc ::lresult/remaining remaining))))
|
||||||
(assoc ::lresult/remaining remaining))))))))
|
|
||||||
|
|
||||||
(defmethod process-limit :window
|
(defmethod process-limit :window
|
||||||
[redis user-id now {:keys [::nreq ::unit ::key ::service] :as limit}]
|
[redis user-id now {:keys [::nreq ::unit ::key ::service] :as limit}]
|
||||||
(let [ts (dt/truncate now unit)
|
(let [ts (dt/truncate now unit)
|
||||||
ttl (dt/diff now (dt/plus ts {unit 1}))
|
ttl (dt/diff now (dt/plus ts {unit 1}))
|
||||||
script (-> window-rate-limit-script
|
script (-> window-rate-limit-script
|
||||||
(assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))])
|
(assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))])
|
||||||
(assoc ::rscript/vals [nreq (dt/->seconds ttl)]))]
|
(assoc ::rscript/vals [nreq (dt/->seconds ttl)]))
|
||||||
(->> (rds/eval! redis script)
|
result (rds/eval! redis script)
|
||||||
(p/fmap (fn [result]
|
allowed? (boolean (nth result 0))
|
||||||
(let [allowed? (boolean (nth result 0))
|
remaining (nth result 1)]
|
||||||
remaining (nth result 1)]
|
(l/trace :hint "limit processed"
|
||||||
(l/trace :hint "limit processed"
|
:service service
|
||||||
:service service
|
:limit (name (::name limit))
|
||||||
:limit (name (::name limit))
|
:strategy (name (::strategy limit))
|
||||||
:strategy (name (::strategy limit))
|
:opts (::opts limit)
|
||||||
:opts (::opts limit)
|
:allowed allowed?
|
||||||
:allowed? allowed?
|
:remaining remaining)
|
||||||
:remaining remaining)
|
(-> limit
|
||||||
(-> limit
|
(assoc ::lresult/allowed allowed?)
|
||||||
(assoc ::lresult/allowed? allowed?)
|
(assoc ::lresult/remaining remaining)
|
||||||
(assoc ::lresult/remaining remaining)
|
(assoc ::lresult/reset (dt/plus ts {unit 1})))))
|
||||||
(assoc ::lresult/reset (dt/plus ts {unit 1})))))))))
|
|
||||||
|
|
||||||
(defn- process-limits!
|
(defn- process-limits!
|
||||||
[redis user-id limits now]
|
[redis user-id limits now]
|
||||||
(->> (p/all (map (partial process-limit redis user-id now) limits))
|
(let [results (into [] (map (partial process-limit redis user-id now)) limits)
|
||||||
(p/fmap (fn [results]
|
remaining (->> results
|
||||||
(let [remaining (->> results
|
(d/index-by ::name ::lresult/remaining)
|
||||||
(d/index-by ::name ::lresult/remaining)
|
(uri/map->query-string))
|
||||||
(uri/map->query-string))
|
reset (->> results
|
||||||
reset (->> results
|
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
|
||||||
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
|
(uri/map->query-string))
|
||||||
(uri/map->query-string))
|
|
||||||
rejected (->> results
|
|
||||||
(filter (complement ::lresult/allowed?))
|
|
||||||
(first))]
|
|
||||||
|
|
||||||
(when rejected
|
rejected (d/seek (complement ::lresult/allowed) results)]
|
||||||
(l/warn :hint "rejected rate limit"
|
|
||||||
:user-id (str user-id)
|
|
||||||
:limit-service (-> rejected ::service name)
|
|
||||||
:limit-name (-> rejected ::name name)
|
|
||||||
:limit-strategy (-> rejected ::strategy name)))
|
|
||||||
|
|
||||||
{:enabled? true
|
(when rejected
|
||||||
:allowed? (not (some? rejected))
|
(l/warn :hint "rejected rate limit"
|
||||||
:headers {"x-rate-limit-remaining" remaining
|
:user-id (str user-id)
|
||||||
"x-rate-limit-reset" reset}})))))
|
:limit-service (-> rejected ::service name)
|
||||||
|
:limit-name (-> rejected ::name name)
|
||||||
|
:limit-strategy (-> rejected ::strategy name)))
|
||||||
|
|
||||||
(defn- handle-response
|
{::enabled true
|
||||||
[f cfg params result]
|
::allowed (not (some? rejected))
|
||||||
(if (:enabled? result)
|
::remaingin remaining
|
||||||
(let [headers (:headers result)]
|
::reset reset
|
||||||
(if (:allowed? result)
|
::headers {"x-rate-limit-remaining" remaining
|
||||||
(->> (f cfg params)
|
"x-rate-limit-reset" reset}}))
|
||||||
(p/fmap (fn [response]
|
|
||||||
(vary-meta response update ::http/headers merge headers))))
|
|
||||||
(p/rejected
|
|
||||||
(ex/error :type :rate-limit
|
|
||||||
:code :request-blocked
|
|
||||||
:hint "rate limit reached"
|
|
||||||
::http/headers headers))))
|
|
||||||
(f cfg params)))
|
|
||||||
|
|
||||||
(defn- get-limits
|
(defn- get-limits
|
||||||
[state skey sname]
|
[state skey sname]
|
||||||
(some->> (or (get-in @state [::limits skey])
|
(when-let [limits (or (get-in @state [::limits skey])
|
||||||
(get-in @state [::limits :default]))
|
(get-in @state [::limits :default]))]
|
||||||
(map #(assoc % ::service sname))
|
(into [] (map #(assoc % ::service sname)) limits)))
|
||||||
(seq)))
|
|
||||||
|
|
||||||
(defn- get-uid
|
(defn- get-uid
|
||||||
[{:keys [::http/request] :as params}]
|
[{:keys [::http/request] :as params}]
|
||||||
@ -236,6 +217,31 @@
|
|||||||
(some-> request parse-client-ip)
|
(some-> request parse-client-ip)
|
||||||
uuid/zero))
|
uuid/zero))
|
||||||
|
|
||||||
|
(defn process-request!
|
||||||
|
[{:keys [::rpc/rlimit ::rds/redis ::skey ::sname] :as cfg} params]
|
||||||
|
(when-let [limits (get-limits rlimit skey sname)]
|
||||||
|
(let [redis (rds/get-or-connect redis ::rpc/rlimit default-options)
|
||||||
|
uid (get-uid params)
|
||||||
|
;; FIXME: why not clasic try/catch?
|
||||||
|
result (ex/try! (process-limits! redis uid limits (dt/now)))]
|
||||||
|
|
||||||
|
(l/trc :hint "process-limits"
|
||||||
|
:service sname
|
||||||
|
:remaining (::remaingin result)
|
||||||
|
:reset (::reset result))
|
||||||
|
|
||||||
|
(cond
|
||||||
|
(ex/exception? result)
|
||||||
|
(do
|
||||||
|
(l/error :hint "error on processing rate-limit" :cause result)
|
||||||
|
{::enabled false})
|
||||||
|
|
||||||
|
(contains? cf/flags :soft-rpc-rlimit)
|
||||||
|
{::enabled false}
|
||||||
|
|
||||||
|
:else
|
||||||
|
result))))
|
||||||
|
|
||||||
(defn wrap
|
(defn wrap
|
||||||
[{:keys [::rpc/rlimit ::rds/redis] :as cfg} f mdata]
|
[{:keys [::rpc/rlimit ::rds/redis] :as cfg} f mdata]
|
||||||
(us/assert! ::rpc/rlimit rlimit)
|
(us/assert! ::rpc/rlimit rlimit)
|
||||||
@ -243,36 +249,25 @@
|
|||||||
|
|
||||||
(if rlimit
|
(if rlimit
|
||||||
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
|
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
|
||||||
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))]
|
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))
|
||||||
|
cfg (-> cfg
|
||||||
|
(assoc ::skey skey)
|
||||||
|
(assoc ::sname sname))]
|
||||||
|
|
||||||
(fn [cfg params]
|
(fn [hcfg params]
|
||||||
(if @enabled?
|
(if @enabled
|
||||||
(try
|
(let [result (process-request! cfg params)]
|
||||||
(let [uid (get-uid params)
|
(if (::enabled result)
|
||||||
rsp (when-let [limits (get-limits rlimit skey sname)]
|
(if (::allowed result)
|
||||||
(let [redis (rds/get-or-connect redis ::rpc/rlimit default-options)
|
(-> (f hcfg params)
|
||||||
rsp (->> (process-limits! redis uid limits (dt/now))
|
(rph/wrap)
|
||||||
(p/merr (fn [cause]
|
(vary-meta update ::http/headers merge (::headers result)))
|
||||||
;; If we have an error on processing the rate-limit we just skip
|
(ex/raise :type :rate-limit
|
||||||
;; it for do not cause service interruption because of redis
|
:code :request-blocked
|
||||||
;; downtime or similar situation.
|
:hint "rate limit reached"
|
||||||
(l/error :hint "error on processing rate-limit" :cause cause)
|
::http/headers (::headers result)))
|
||||||
(p/resolved {:enabled? false}))))]
|
(f hcfg params)))
|
||||||
|
(f hcfg params))))
|
||||||
;; If soft rate are enabled, we process the rate-limit but return unprotected
|
|
||||||
;; response.
|
|
||||||
(if (contains? cf/flags :soft-rpc-rlimit)
|
|
||||||
{:enabled? false}
|
|
||||||
rsp)))]
|
|
||||||
|
|
||||||
(->> (p/promise rsp)
|
|
||||||
(p/fmap #(or % {:enabled? false}))
|
|
||||||
(p/mcat #(handle-response f cfg params %))))
|
|
||||||
|
|
||||||
(catch Throwable cause
|
|
||||||
(p/rejected cause)))
|
|
||||||
|
|
||||||
(f cfg params))))
|
|
||||||
f))
|
f))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -352,7 +347,7 @@
|
|||||||
::limits limits}))))
|
::limits limits}))))
|
||||||
|
|
||||||
(defn- refresh-config
|
(defn- refresh-config
|
||||||
[{:keys [::state ::path ::wrk/executor ::wrk/scheduled-executor] :as cfg}]
|
[{:keys [::state ::path ::wrk/executor] :as cfg}]
|
||||||
(letfn [(update-config [{:keys [::updated-at] :as state}]
|
(letfn [(update-config [{:keys [::updated-at] :as state}]
|
||||||
(let [updated-at' (fs/last-modified-time path)]
|
(let [updated-at' (fs/last-modified-time path)]
|
||||||
(merge state
|
(merge state
|
||||||
@ -367,8 +362,7 @@
|
|||||||
state)))))
|
state)))))
|
||||||
|
|
||||||
(schedule-next [state]
|
(schedule-next [state]
|
||||||
(px/schedule! scheduled-executor
|
(px/schedule! (inst-ms (::refresh state))
|
||||||
(inst-ms (::refresh state))
|
|
||||||
(partial refresh-config cfg))
|
(partial refresh-config cfg))
|
||||||
state)]
|
state)]
|
||||||
|
|
||||||
@ -391,8 +385,7 @@
|
|||||||
(and (fs/exists? path) (fs/regular-file? path) path)))
|
(and (fs/exists? path) (fs/regular-file? path) path)))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec :app.rpc/rlimit [_]
|
(defmethod ig/pre-init-spec :app.rpc/rlimit [_]
|
||||||
(s/keys :req [::wrk/executor
|
(s/keys :req [::wrk/executor]))
|
||||||
::wrk/scheduled-executor]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::rpc/rlimit
|
(defmethod ig/init-key ::rpc/rlimit
|
||||||
[_ {:keys [::wrk/executor] :as cfg}]
|
[_ {:keys [::wrk/executor] :as cfg}]
|
||||||
|
|||||||
@ -22,8 +22,7 @@
|
|||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]))
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Storage Module State
|
;; Storage Module State
|
||||||
@ -79,42 +78,40 @@
|
|||||||
(update :metadata db/decode-transit-pgobject))))
|
(update :metadata db/decode-transit-pgobject))))
|
||||||
|
|
||||||
(defn- create-database-object
|
(defn- create-database-object
|
||||||
[{:keys [::backend ::wrk/executor ::db/pool-or-conn]} {:keys [::content ::expired-at ::touched-at] :as params}]
|
[{:keys [::backend ::db/pool-or-conn]} {:keys [::content ::expired-at ::touched-at] :as params}]
|
||||||
(px/with-dispatch executor
|
(let [id (uuid/random)
|
||||||
(let [id (uuid/random)
|
mdata (cond-> (get-metadata params)
|
||||||
|
(satisfies? impl/IContentHash content)
|
||||||
|
(assoc :hash (impl/get-hash content)))
|
||||||
|
|
||||||
mdata (cond-> (get-metadata params)
|
;; NOTE: for now we don't reuse the deleted objects, but in
|
||||||
(satisfies? impl/IContentHash content)
|
;; futute we can consider reusing deleted objects if we
|
||||||
(assoc :hash (impl/get-hash content)))
|
;; found a duplicated one and is marked for deletion but
|
||||||
|
;; still not deleted.
|
||||||
|
result (when (and (::deduplicate? params)
|
||||||
|
(:hash mdata)
|
||||||
|
(:bucket mdata))
|
||||||
|
(get-database-object-by-hash pool-or-conn backend (:bucket mdata) (:hash mdata)))
|
||||||
|
|
||||||
;; NOTE: for now we don't reuse the deleted objects, but in
|
result (or result
|
||||||
;; futute we can consider reusing deleted objects if we
|
(-> (db/insert! pool-or-conn :storage-object
|
||||||
;; found a duplicated one and is marked for deletion but
|
{:id id
|
||||||
;; still not deleted.
|
:size (impl/get-size content)
|
||||||
result (when (and (::deduplicate? params)
|
:backend (name backend)
|
||||||
(:hash mdata)
|
:metadata (db/tjson mdata)
|
||||||
(:bucket mdata))
|
:deleted-at expired-at
|
||||||
(get-database-object-by-hash pool-or-conn backend (:bucket mdata) (:hash mdata)))
|
:touched-at touched-at})
|
||||||
|
(update :metadata db/decode-transit-pgobject)
|
||||||
|
(update :metadata assoc ::created? true)))]
|
||||||
|
|
||||||
result (or result
|
(impl/storage-object
|
||||||
(-> (db/insert! pool-or-conn :storage-object
|
(:id result)
|
||||||
{:id id
|
(:size result)
|
||||||
:size (impl/get-size content)
|
(:created-at result)
|
||||||
:backend (name backend)
|
(:deleted-at result)
|
||||||
:metadata (db/tjson mdata)
|
(:touched-at result)
|
||||||
:deleted-at expired-at
|
backend
|
||||||
:touched-at touched-at})
|
(:metadata result))))
|
||||||
(update :metadata db/decode-transit-pgobject)
|
|
||||||
(update :metadata assoc ::created? true)))]
|
|
||||||
|
|
||||||
(impl/storage-object
|
|
||||||
(:id result)
|
|
||||||
(:size result)
|
|
||||||
(:created-at result)
|
|
||||||
(:deleted-at result)
|
|
||||||
(:touched-at result)
|
|
||||||
backend
|
|
||||||
(:metadata result)))))
|
|
||||||
|
|
||||||
(def ^:private sql:retrieve-storage-object
|
(def ^:private sql:retrieve-storage-object
|
||||||
"select * from storage_object where id = ? and (deleted_at is null or deleted_at > now())")
|
"select * from storage_object where id = ? and (deleted_at is null or deleted_at > now())")
|
||||||
@ -153,45 +150,41 @@
|
|||||||
(dm/export impl/object?)
|
(dm/export impl/object?)
|
||||||
|
|
||||||
(defn get-object
|
(defn get-object
|
||||||
[{:keys [::db/pool-or-conn ::wrk/executor] :as storage} id]
|
[{:keys [::db/pool-or-conn] :as storage} id]
|
||||||
(us/assert! ::storage storage)
|
(us/assert! ::storage storage)
|
||||||
(px/with-dispatch executor
|
(retrieve-database-object pool-or-conn id))
|
||||||
(retrieve-database-object pool-or-conn id)))
|
|
||||||
|
|
||||||
(defn put-object!
|
(defn put-object!
|
||||||
"Creates a new object with the provided content."
|
"Creates a new object with the provided content."
|
||||||
[{:keys [::backend] :as storage} {:keys [::content] :as params}]
|
[{:keys [::backend] :as storage} {:keys [::content] :as params}]
|
||||||
(us/assert! ::storage-with-backend storage)
|
(us/assert! ::storage-with-backend storage)
|
||||||
(us/assert! ::impl/content content)
|
(us/assert! ::impl/content content)
|
||||||
(->> (create-database-object storage params)
|
(let [object (create-database-object storage params)]
|
||||||
(p/mcat (fn [object]
|
(if (::created? (meta object))
|
||||||
(if (::created? (meta object))
|
;; Store the data finally on the underlying storage subsystem.
|
||||||
;; Store the data finally on the underlying storage subsystem.
|
(-> (impl/resolve-backend storage backend)
|
||||||
(-> (impl/resolve-backend storage backend)
|
(impl/put-object object content))
|
||||||
(impl/put-object object content))
|
object)))
|
||||||
(p/resolved object))))))
|
|
||||||
|
|
||||||
(defn touch-object!
|
(defn touch-object!
|
||||||
"Mark object as touched."
|
"Mark object as touched."
|
||||||
[{:keys [::db/pool-or-conn ::wrk/executor] :as storage} object-or-id]
|
[{:keys [::db/pool-or-conn] :as storage} object-or-id]
|
||||||
(us/assert! ::storage storage)
|
(us/assert! ::storage storage)
|
||||||
(px/with-dispatch executor
|
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
|
||||||
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
|
rs (db/update! pool-or-conn :storage-object
|
||||||
rs (db/update! pool-or-conn :storage-object
|
{:touched-at (dt/now)}
|
||||||
{:touched-at (dt/now)}
|
{:id id}
|
||||||
{:id id}
|
{::db/return-keys? false})]
|
||||||
{::db/return-keys? false})]
|
(pos? (db/get-update-count rs))))
|
||||||
(pos? (db/get-update-count rs)))))
|
|
||||||
|
|
||||||
(defn get-object-data
|
(defn get-object-data
|
||||||
"Return an input stream instance of the object content."
|
"Return an input stream instance of the object content."
|
||||||
[storage object]
|
[storage object]
|
||||||
(us/assert! ::storage storage)
|
(us/assert! ::storage storage)
|
||||||
(if (or (nil? (:expired-at object))
|
(when (or (nil? (:expired-at object))
|
||||||
(dt/is-after? (:expired-at object) (dt/now)))
|
(dt/is-after? (:expired-at object) (dt/now)))
|
||||||
(-> (impl/resolve-backend storage (:backend object))
|
(-> (impl/resolve-backend storage (:backend object))
|
||||||
(impl/get-object-data object))
|
(impl/get-object-data object))))
|
||||||
(p/resolved nil)))
|
|
||||||
|
|
||||||
(defn get-object-bytes
|
(defn get-object-bytes
|
||||||
"Returns a byte array of object content."
|
"Returns a byte array of object content."
|
||||||
@ -208,11 +201,10 @@
|
|||||||
(get-object-url storage object nil))
|
(get-object-url storage object nil))
|
||||||
([storage object options]
|
([storage object options]
|
||||||
(us/assert! ::storage storage)
|
(us/assert! ::storage storage)
|
||||||
(if (or (nil? (:expired-at object))
|
(when (or (nil? (:expired-at object))
|
||||||
(dt/is-after? (:expired-at object) (dt/now)))
|
(dt/is-after? (:expired-at object) (dt/now)))
|
||||||
(-> (impl/resolve-backend storage (:backend object))
|
(-> (impl/resolve-backend storage (:backend object))
|
||||||
(impl/get-object-url object options))
|
(impl/get-object-url object options)))))
|
||||||
(p/resolved nil))))
|
|
||||||
|
|
||||||
(defn get-object-path
|
(defn get-object-path
|
||||||
"Get the Path to the object. Only works with `:fs` type of
|
"Get the Path to the object. Only works with `:fs` type of
|
||||||
@ -220,24 +212,20 @@
|
|||||||
[storage object]
|
[storage object]
|
||||||
(us/assert! ::storage storage)
|
(us/assert! ::storage storage)
|
||||||
(let [backend (impl/resolve-backend storage (:backend object))]
|
(let [backend (impl/resolve-backend storage (:backend object))]
|
||||||
(if (not= :fs (::type backend))
|
(when (and (= :fs (::type backend))
|
||||||
(p/resolved nil)
|
(or (nil? (:expired-at object))
|
||||||
(if (or (nil? (:expired-at object))
|
(dt/is-after? (:expired-at object) (dt/now))))
|
||||||
(dt/is-after? (:expired-at object) (dt/now)))
|
(-> (impl/get-object-url backend object nil) file-url->path))))
|
||||||
(->> (impl/get-object-url backend object nil)
|
|
||||||
(p/fmap file-url->path))
|
|
||||||
(p/resolved nil)))))
|
|
||||||
|
|
||||||
(defn del-object!
|
(defn del-object!
|
||||||
[{:keys [::db/pool-or-conn ::wrk/executor] :as storage} object-or-id]
|
[{:keys [::db/pool-or-conn] :as storage} object-or-id]
|
||||||
(us/assert! ::storage storage)
|
(us/assert! ::storage storage)
|
||||||
(px/with-dispatch executor
|
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
|
||||||
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
|
res (db/update! pool-or-conn :storage-object
|
||||||
res (db/update! pool-or-conn :storage-object
|
{:deleted-at (dt/now)}
|
||||||
{:deleted-at (dt/now)}
|
{:id id}
|
||||||
{:id id}
|
{::db/return-keys? false})]
|
||||||
{::db/return-keys? false})]
|
(pos? (db/get-update-count res))))
|
||||||
(pos? (db/get-update-count res)))))
|
|
||||||
|
|
||||||
(dm/export impl/resolve-backend)
|
(dm/export impl/resolve-backend)
|
||||||
(dm/export impl/calculate-hash)
|
(dm/export impl/calculate-hash)
|
||||||
@ -281,7 +269,7 @@
|
|||||||
(doseq [id ids]
|
(doseq [id ids]
|
||||||
(l/debug :hint "gc-deleted: permanently delete storage object" :backend backend-id :id id))
|
(l/debug :hint "gc-deleted: permanently delete storage object" :backend backend-id :id id))
|
||||||
|
|
||||||
@(impl/del-objects-in-bulk backend ids)))]
|
(impl/del-objects-in-bulk backend ids)))]
|
||||||
|
|
||||||
(fn [params]
|
(fn [params]
|
||||||
(let [min-age (or (:min-age params) min-age)]
|
(let [min-age (or (:min-age params) min-age)]
|
||||||
@ -422,8 +410,8 @@
|
|||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
:code :unexpected-unknown-reference
|
:code :unexpected-unknown-reference
|
||||||
:hint (dm/fmt "unknown reference %" bucket)))]
|
:hint (dm/fmt "unknown reference %" bucket)))]
|
||||||
(recur (+ to-freeze f)
|
(recur (+ to-freeze (long f))
|
||||||
(+ to-delete d)
|
(+ to-delete (long d))
|
||||||
(rest groups)))
|
(rest groups)))
|
||||||
(do
|
(do
|
||||||
(l/info :hint "gc-touched: task finished" :to-freeze to-freeze :to-delete to-delete)
|
(l/info :hint "gc-touched: task finished" :to-freeze to-freeze :to-delete to-delete)
|
||||||
|
|||||||
@ -6,22 +6,18 @@
|
|||||||
|
|
||||||
(ns app.storage.fs
|
(ns app.storage.fs
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
[app.storage :as-alias sto]
|
[app.storage :as-alias sto]
|
||||||
[app.storage.impl :as impl]
|
[app.storage.impl :as impl]
|
||||||
[app.worker :as-alias wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
[datoteka.io :as io]
|
[datoteka.io :as io]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig])
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px])
|
|
||||||
(:import
|
(:import
|
||||||
java.io.InputStream
|
|
||||||
java.io.OutputStream
|
|
||||||
java.nio.file.Path
|
java.nio.file.Path
|
||||||
java.nio.file.Files))
|
java.nio.file.Files))
|
||||||
|
|
||||||
@ -48,74 +44,66 @@
|
|||||||
(s/keys :req [::directory
|
(s/keys :req [::directory
|
||||||
::uri]
|
::uri]
|
||||||
:opt [::sto/type
|
:opt [::sto/type
|
||||||
::sto/id
|
::sto/id]))
|
||||||
::wrk/executor]))
|
|
||||||
|
|
||||||
;; --- API IMPL
|
;; --- API IMPL
|
||||||
|
|
||||||
(defmethod impl/put-object :fs
|
(defmethod impl/put-object :fs
|
||||||
[{:keys [::wrk/executor] :as backend} {:keys [id] :as object} content]
|
[backend {:keys [id] :as object} content]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
(px/with-dispatch executor
|
(let [base (fs/path (::directory backend))
|
||||||
(let [base (fs/path (::directory backend))
|
path (fs/path (impl/id->path id))
|
||||||
path (fs/path (impl/id->path id))
|
full (fs/normalize (fs/join base path))]
|
||||||
full (fs/normalize (fs/join base path))]
|
|
||||||
(when-not (fs/exists? (fs/parent full))
|
|
||||||
(fs/create-dir (fs/parent full)))
|
|
||||||
(with-open [^InputStream src (io/input-stream content)
|
|
||||||
^OutputStream dst (io/output-stream full)]
|
|
||||||
(io/copy! src dst))
|
|
||||||
|
|
||||||
object)))
|
(when-not (fs/exists? (fs/parent full))
|
||||||
|
(fs/create-dir (fs/parent full)))
|
||||||
|
|
||||||
|
(dm/with-open [src (io/input-stream content)
|
||||||
|
dst (io/output-stream full)]
|
||||||
|
(io/copy! src dst))
|
||||||
|
|
||||||
|
object))
|
||||||
|
|
||||||
(defmethod impl/get-object-data :fs
|
(defmethod impl/get-object-data :fs
|
||||||
[{:keys [::wrk/executor] :as backend} {:keys [id] :as object}]
|
[backend {:keys [id] :as object}]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
(px/with-dispatch executor
|
(let [^Path base (fs/path (::directory backend))
|
||||||
(let [^Path base (fs/path (::directory backend))
|
^Path path (fs/path (impl/id->path id))
|
||||||
^Path path (fs/path (impl/id->path id))
|
^Path full (fs/normalize (fs/join base path))]
|
||||||
^Path full (fs/normalize (fs/join base path))]
|
(when-not (fs/exists? full)
|
||||||
(when-not (fs/exists? full)
|
(ex/raise :type :internal
|
||||||
(ex/raise :type :internal
|
:code :filesystem-object-does-not-exists
|
||||||
:code :filesystem-object-does-not-exists
|
:path (str full)))
|
||||||
:path (str full)))
|
(io/input-stream full)))
|
||||||
(io/input-stream full))))
|
|
||||||
|
|
||||||
(defmethod impl/get-object-bytes :fs
|
(defmethod impl/get-object-bytes :fs
|
||||||
[backend object]
|
[backend object]
|
||||||
(->> (impl/get-object-data backend object)
|
(dm/with-open [input (impl/get-object-data backend object)]
|
||||||
(p/fmap (fn [input]
|
(io/read-as-bytes input)))
|
||||||
(try
|
|
||||||
(io/read-as-bytes input)
|
|
||||||
(finally
|
|
||||||
(io/close! input)))))))
|
|
||||||
|
|
||||||
(defmethod impl/get-object-url :fs
|
(defmethod impl/get-object-url :fs
|
||||||
[{:keys [::uri] :as backend} {:keys [id] :as object} _]
|
[{:keys [::uri] :as backend} {:keys [id] :as object} _]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
(p/resolved
|
(update uri :path
|
||||||
(update uri :path
|
(fn [existing]
|
||||||
(fn [existing]
|
(if (str/ends-with? existing "/")
|
||||||
(if (str/ends-with? existing "/")
|
(str existing (impl/id->path id))
|
||||||
(str existing (impl/id->path id))
|
(str existing "/" (impl/id->path id))))))
|
||||||
(str existing "/" (impl/id->path id)))))))
|
|
||||||
|
|
||||||
(defmethod impl/del-object :fs
|
(defmethod impl/del-object :fs
|
||||||
[{:keys [::wrk/executor] :as backend} {:keys [id] :as object}]
|
[backend {:keys [id] :as object}]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
(px/with-dispatch executor
|
(let [base (fs/path (::directory backend))
|
||||||
(let [base (fs/path (::directory backend))
|
path (fs/path (impl/id->path id))
|
||||||
path (fs/path (impl/id->path id))
|
path (fs/join base path)]
|
||||||
path (fs/join base path)]
|
(Files/deleteIfExists ^Path path)))
|
||||||
(Files/deleteIfExists ^Path path))))
|
|
||||||
|
|
||||||
(defmethod impl/del-objects-in-bulk :fs
|
(defmethod impl/del-objects-in-bulk :fs
|
||||||
[{:keys [::wrk/executor] :as backend} ids]
|
[backend ids]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
(px/with-dispatch executor
|
(let [base (fs/path (::directory backend))]
|
||||||
(let [base (fs/path (::directory backend))]
|
(doseq [id ids]
|
||||||
(doseq [id ids]
|
(let [path (fs/path (impl/id->path id))
|
||||||
(let [path (fs/path (impl/id->path id))
|
path (fs/join base path)]
|
||||||
path (fs/join base path)]
|
(Files/deleteIfExists ^Path path)))))
|
||||||
(Files/deleteIfExists ^Path path))))))
|
|
||||||
|
|
||||||
|
|||||||
@ -153,8 +153,8 @@
|
|||||||
(content (.toPath ^java.io.File data) size)
|
(content (.toPath ^java.io.File data) size)
|
||||||
|
|
||||||
(instance? String data)
|
(instance? String data)
|
||||||
(let [data (.getBytes data "UTF-8")]
|
(let [data (.getBytes ^String data "UTF-8")]
|
||||||
(bytes->content data (alength data)))
|
(bytes->content data (alength ^bytes data)))
|
||||||
|
|
||||||
(bytes? data)
|
(bytes? data)
|
||||||
(bytes->content data (or size (alength ^bytes data)))
|
(bytes->content data (or size (alength ^bytes data)))
|
||||||
@ -195,7 +195,7 @@
|
|||||||
|
|
||||||
(defn calculate-hash
|
(defn calculate-hash
|
||||||
[resource]
|
[resource]
|
||||||
(let [result (with-open [input (io/input-stream resource)]
|
(let [result (dm/with-open [input (io/input-stream resource)]
|
||||||
(-> (bh/blake2b-256 input)
|
(-> (bh/blake2b-256 input)
|
||||||
(bc/bytes->hex)))]
|
(bc/bytes->hex)))]
|
||||||
(str "blake2b:" result)))
|
(str "blake2b:" result)))
|
||||||
|
|||||||
@ -45,6 +45,7 @@
|
|||||||
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
|
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
|
||||||
software.amazon.awssdk.regions.Region
|
software.amazon.awssdk.regions.Region
|
||||||
software.amazon.awssdk.services.s3.S3AsyncClient
|
software.amazon.awssdk.services.s3.S3AsyncClient
|
||||||
|
software.amazon.awssdk.services.s3.S3AsyncClientBuilder
|
||||||
software.amazon.awssdk.services.s3.S3Configuration
|
software.amazon.awssdk.services.s3.S3Configuration
|
||||||
software.amazon.awssdk.services.s3.model.Delete
|
software.amazon.awssdk.services.s3.model.Delete
|
||||||
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
|
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
|
||||||
@ -121,7 +122,7 @@
|
|||||||
(defmethod impl/put-object :s3
|
(defmethod impl/put-object :s3
|
||||||
[backend object content]
|
[backend object content]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
(put-object backend object content))
|
(p/await! (put-object backend object content)))
|
||||||
|
|
||||||
(defmethod impl/get-object-data :s3
|
(defmethod impl/get-object-data :s3
|
||||||
[backend object]
|
[backend object]
|
||||||
@ -135,12 +136,13 @@
|
|||||||
:cause cause))]
|
:cause cause))]
|
||||||
|
|
||||||
(-> (get-object-data backend object)
|
(-> (get-object-data backend object)
|
||||||
(p/catch no-such-key? handle-not-found))))
|
(p/catch no-such-key? handle-not-found)
|
||||||
|
(p/await!))))
|
||||||
|
|
||||||
(defmethod impl/get-object-bytes :s3
|
(defmethod impl/get-object-bytes :s3
|
||||||
[backend object]
|
[backend object]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
(get-object-bytes backend object))
|
(p/await! (get-object-bytes backend object)))
|
||||||
|
|
||||||
(defmethod impl/get-object-url :s3
|
(defmethod impl/get-object-url :s3
|
||||||
[backend object options]
|
[backend object options]
|
||||||
@ -150,12 +152,12 @@
|
|||||||
(defmethod impl/del-object :s3
|
(defmethod impl/del-object :s3
|
||||||
[backend object]
|
[backend object]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
(del-object backend object))
|
(p/await! (del-object backend object)))
|
||||||
|
|
||||||
(defmethod impl/del-objects-in-bulk :s3
|
(defmethod impl/del-objects-in-bulk :s3
|
||||||
[backend ids]
|
[backend ids]
|
||||||
(us/assert! ::backend backend)
|
(us/assert! ::backend backend)
|
||||||
(del-object-in-bulk backend ids))
|
(p/await! (del-object-in-bulk backend ids)))
|
||||||
|
|
||||||
;; --- HELPERS
|
;; --- HELPERS
|
||||||
|
|
||||||
@ -187,13 +189,17 @@
|
|||||||
(.writeTimeout default-timeout)
|
(.writeTimeout default-timeout)
|
||||||
(.build))
|
(.build))
|
||||||
|
|
||||||
client (-> (S3AsyncClient/builder)
|
client (let [builder (S3AsyncClient/builder)
|
||||||
(.serviceConfiguration ^S3Configuration sconfig)
|
builder (.serviceConfiguration ^S3AsyncClientBuilder builder ^S3Configuration sconfig)
|
||||||
(.asyncConfiguration ^ClientAsyncConfiguration aconfig)
|
builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig)
|
||||||
(.httpClient ^NettyNioAsyncHttpClient hclient)
|
builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient)
|
||||||
(.region (lookup-region region))
|
builder (.region ^S3AsyncClientBuilder builder (lookup-region region))
|
||||||
(cond-> (some? endpoint) (.endpointOverride (URI. endpoint)))
|
builder (cond-> ^S3AsyncClientBuilder builder
|
||||||
(.build))]
|
(some? endpoint)
|
||||||
|
(.endpointOverride (URI. endpoint)))]
|
||||||
|
(.build ^S3AsyncClientBuilder builder))
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
(reify
|
(reify
|
||||||
clojure.lang.IDeref
|
clojure.lang.IDeref
|
||||||
@ -288,6 +294,7 @@
|
|||||||
^AsyncRequestBody rbody)
|
^AsyncRequestBody rbody)
|
||||||
(p/fmap (constantly object)))))
|
(p/fmap (constantly object)))))
|
||||||
|
|
||||||
|
;; FIXME: research how to avoid reflection on close method
|
||||||
(defn- path->stream
|
(defn- path->stream
|
||||||
[path]
|
[path]
|
||||||
(proxy [FilterInputStream] [(io/input-stream path)]
|
(proxy [FilterInputStream] [(io/input-stream path)]
|
||||||
@ -347,8 +354,7 @@
|
|||||||
(getObjectRequest ^GetObjectRequest gor)
|
(getObjectRequest ^GetObjectRequest gor)
|
||||||
(build))
|
(build))
|
||||||
pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)]
|
pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)]
|
||||||
(p/resolved
|
(u/uri (str (.url ^PresignedGetObjectRequest pgor)))))
|
||||||
(u/uri (str (.url ^PresignedGetObjectRequest pgor))))))
|
|
||||||
|
|
||||||
(defn- del-object
|
(defn- del-object
|
||||||
[{:keys [::bucket ::client ::prefix]} {:keys [id] :as obj}]
|
[{:keys [::bucket ::client ::prefix]} {:keys [id] :as obj}]
|
||||||
|
|||||||
@ -10,57 +10,59 @@
|
|||||||
the operating system cleaning task should be responsible of
|
the operating system cleaning task should be responsible of
|
||||||
permanently delete these files (look at systemd-tempfiles)."
|
permanently delete these files (look at systemd-tempfiles)."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.storage :as-alias sto]
|
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.core.async :as a]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.exec :as px]))
|
[promesa.exec :as px]
|
||||||
|
[promesa.exec.csp :as sp]))
|
||||||
|
|
||||||
(declare remove-temp-file)
|
(declare ^:private remove-temp-file)
|
||||||
(defonce queue (a/chan 128))
|
(declare ^:private io-loop)
|
||||||
|
|
||||||
|
(defonce queue (sp/chan :buf 128))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::cleaner [_]
|
(defmethod ig/pre-init-spec ::cleaner [_]
|
||||||
(s/keys :req [::sto/min-age ::wrk/scheduled-executor]))
|
(s/keys :req [::wrk/executor]))
|
||||||
|
|
||||||
(defmethod ig/prep-key ::cleaner
|
(defmethod ig/prep-key ::cleaner
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(merge {::sto/min-age (dt/duration "30m")}
|
(assoc cfg ::min-age (dt/duration "30m")))
|
||||||
(d/without-nils cfg)))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::cleaner
|
(defmethod ig/init-key ::cleaner
|
||||||
[_ {:keys [::sto/min-age ::wrk/scheduled-executor] :as cfg}]
|
[_ cfg]
|
||||||
(px/thread
|
(px/fn->thread (partial io-loop cfg)
|
||||||
{:name "penpot/storage-tmp-cleaner"}
|
{:name "penpot/storage/tmp-cleaner" :virtual true}))
|
||||||
(try
|
|
||||||
(l/info :hint "started tmp file cleaner")
|
|
||||||
(loop []
|
|
||||||
(when-let [path (a/<!! queue)]
|
|
||||||
(l/trace :hint "schedule tempfile deletion" :path path
|
|
||||||
:expires-at (dt/plus (dt/now) min-age))
|
|
||||||
(px/schedule! scheduled-executor
|
|
||||||
(inst-ms min-age)
|
|
||||||
(partial remove-temp-file path))
|
|
||||||
(recur)))
|
|
||||||
(catch InterruptedException _
|
|
||||||
(l/debug :hint "interrupted"))
|
|
||||||
(finally
|
|
||||||
(l/info :hint "terminated tmp file cleaner")))))
|
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::cleaner
|
(defmethod ig/halt-key! ::cleaner
|
||||||
[_ thread]
|
[_ thread]
|
||||||
(px/interrupt! thread))
|
(px/interrupt! thread))
|
||||||
|
|
||||||
|
(defn- io-loop
|
||||||
|
[{:keys [::min-age] :as cfg}]
|
||||||
|
(l/info :hint "started tmp file cleaner")
|
||||||
|
(try
|
||||||
|
(loop []
|
||||||
|
(when-let [path (sp/take! queue)]
|
||||||
|
(l/debug :hint "schedule tempfile deletion" :path path
|
||||||
|
:expires-at (dt/plus (dt/now) min-age))
|
||||||
|
(px/schedule! (inst-ms min-age) (partial remove-temp-file cfg path))
|
||||||
|
(recur)))
|
||||||
|
(catch InterruptedException _
|
||||||
|
(l/trace :hint "cleaner interrupted"))
|
||||||
|
(finally
|
||||||
|
(l/info :hint "cleaner terminated"))))
|
||||||
|
|
||||||
(defn- remove-temp-file
|
(defn- remove-temp-file
|
||||||
"Permanently delete tempfile"
|
"Permanently delete tempfile"
|
||||||
[path]
|
[{:keys [::wrk/executor path]}]
|
||||||
(l/trace :hint "permanently delete tempfile" :path path)
|
|
||||||
(when (fs/exists? path)
|
(when (fs/exists? path)
|
||||||
(fs/delete path)))
|
(px/run! executor
|
||||||
|
(fn []
|
||||||
|
(l/debug :hint "permanently delete tempfile" :path path)
|
||||||
|
(fs/delete path)))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; API
|
;; API
|
||||||
@ -72,7 +74,7 @@
|
|||||||
:or {prefix "penpot."
|
:or {prefix "penpot."
|
||||||
suffix ".tmp"}}]
|
suffix ".tmp"}}]
|
||||||
(let [candidate (fs/tempfile :suffix suffix :prefix prefix)]
|
(let [candidate (fs/tempfile :suffix suffix :prefix prefix)]
|
||||||
(a/offer! queue candidate)
|
(sp/offer! queue candidate)
|
||||||
candidate))
|
candidate))
|
||||||
|
|
||||||
(defn create-tempfile
|
(defn create-tempfile
|
||||||
@ -80,5 +82,5 @@
|
|||||||
:or {prefix "penpot."
|
:or {prefix "penpot."
|
||||||
suffix ".tmp"}}]
|
suffix ".tmp"}}]
|
||||||
(let [path (fs/create-tempfile :suffix suffix :prefix prefix)]
|
(let [path (fs/create-tempfile :suffix suffix :prefix prefix)]
|
||||||
(a/offer! queue path)
|
(sp/offer! queue path)
|
||||||
path))
|
path))
|
||||||
|
|||||||
@ -85,7 +85,7 @@
|
|||||||
|
|
||||||
;; Mark as deleted the storage object related with the
|
;; Mark as deleted the storage object related with the
|
||||||
;; photo-id field.
|
;; photo-id field.
|
||||||
(some->> photo-id (sto/touch-object! storage) deref)
|
(some->> photo-id (sto/touch-object! storage))
|
||||||
|
|
||||||
;; And finally, permanently delete the profile.
|
;; And finally, permanently delete the profile.
|
||||||
(db/delete! conn :profile {:id id})
|
(db/delete! conn :profile {:id id})
|
||||||
@ -117,7 +117,7 @@
|
|||||||
|
|
||||||
;; Mark as deleted the storage object related with the
|
;; Mark as deleted the storage object related with the
|
||||||
;; photo-id field.
|
;; photo-id field.
|
||||||
(some->> photo-id (sto/touch-object! storage) deref)
|
(some->> photo-id (sto/touch-object! storage))
|
||||||
|
|
||||||
;; And finally, permanently delete the team.
|
;; And finally, permanently delete the team.
|
||||||
(db/delete! conn :team {:id id})
|
(db/delete! conn :team {:id id})
|
||||||
@ -184,10 +184,10 @@
|
|||||||
(l/debug :hint "permanently delete font variant" :id (str id))
|
(l/debug :hint "permanently delete font variant" :id (str id))
|
||||||
|
|
||||||
;; Mark as deleted the all related storage objects
|
;; Mark as deleted the all related storage objects
|
||||||
(some->> (:woff1-file-id font) (sto/touch-object! storage) deref)
|
(some->> (:woff1-file-id font) (sto/touch-object! storage))
|
||||||
(some->> (:woff2-file-id font) (sto/touch-object! storage) deref)
|
(some->> (:woff2-file-id font) (sto/touch-object! storage))
|
||||||
(some->> (:otf-file-id font) (sto/touch-object! storage) deref)
|
(some->> (:otf-file-id font) (sto/touch-object! storage))
|
||||||
(some->> (:ttf-file-id font) (sto/touch-object! storage) deref)
|
(some->> (:ttf-file-id font) (sto/touch-object! storage))
|
||||||
|
|
||||||
;; And finally, permanently delete the team font variant
|
;; And finally, permanently delete the team font variant
|
||||||
(db/delete! conn :team-font-variant {:id id})
|
(db/delete! conn :team-font-variant {:id id})
|
||||||
|
|||||||
69
backend/src/app/util/cache.clj
Normal file
69
backend/src/app/util/cache.clj
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
;; 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) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.util.cache
|
||||||
|
"In-memory cache backed by Caffeine"
|
||||||
|
(:refer-clojure :exclude [get])
|
||||||
|
(:require
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[promesa.core :as p]
|
||||||
|
[promesa.exec :as px])
|
||||||
|
(:import
|
||||||
|
com.github.benmanes.caffeine.cache.AsyncCache
|
||||||
|
com.github.benmanes.caffeine.cache.AsyncLoadingCache
|
||||||
|
com.github.benmanes.caffeine.cache.CacheLoader
|
||||||
|
com.github.benmanes.caffeine.cache.Caffeine
|
||||||
|
com.github.benmanes.caffeine.cache.RemovalListener
|
||||||
|
java.time.Duration
|
||||||
|
java.util.concurrent.Executor
|
||||||
|
java.util.function.Function))
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
(defn create-listener
|
||||||
|
[f]
|
||||||
|
(reify RemovalListener
|
||||||
|
(onRemoval [_ key val cause]
|
||||||
|
(when val
|
||||||
|
(f key val cause)))))
|
||||||
|
|
||||||
|
(defn create-loader
|
||||||
|
[f]
|
||||||
|
(reify CacheLoader
|
||||||
|
(load [_ key]
|
||||||
|
(f key))))
|
||||||
|
|
||||||
|
(defn create
|
||||||
|
[& {:keys [executor on-remove load-fn keepalive]}]
|
||||||
|
(as-> (Caffeine/newBuilder) builder
|
||||||
|
(if on-remove (.removalListener builder (create-listener on-remove)) builder)
|
||||||
|
(if executor (.executor builder ^Executor (px/resolve-executor executor)) builder)
|
||||||
|
(if keepalive (.expireAfterAccess builder ^Duration (dt/duration keepalive)) builder)
|
||||||
|
(if load-fn
|
||||||
|
(.buildAsync builder ^CacheLoader (create-loader load-fn))
|
||||||
|
(.buildAsync builder))))
|
||||||
|
|
||||||
|
(defn invalidate-all!
|
||||||
|
[^AsyncCache cache]
|
||||||
|
(.invalidateAll (.synchronous cache)))
|
||||||
|
|
||||||
|
(defn get
|
||||||
|
([cache key]
|
||||||
|
(assert (instance? AsyncLoadingCache cache) "should be AsyncLoadingCache instance")
|
||||||
|
(p/await! (.get ^AsyncLoadingCache cache ^Object key)))
|
||||||
|
([cache key not-found-fn]
|
||||||
|
(assert (instance? AsyncCache cache) "should be AsyncCache instance")
|
||||||
|
(p/await! (.get ^AsyncCache cache
|
||||||
|
^Object key
|
||||||
|
^Function (reify
|
||||||
|
Function
|
||||||
|
(apply [_ key]
|
||||||
|
(not-found-fn key)))))))
|
||||||
|
|
||||||
|
(defn cache?
|
||||||
|
[o]
|
||||||
|
(or (instance? AsyncCache o)
|
||||||
|
(instance? AsyncLoadingCache o)))
|
||||||
@ -45,9 +45,9 @@
|
|||||||
(map second)
|
(map second)
|
||||||
(filter #(::spec (meta %)))
|
(filter #(::spec (meta %)))
|
||||||
(map (fn [fvar]
|
(map (fn [fvar]
|
||||||
(with-meta (deref fvar)
|
[(deref fvar)
|
||||||
(-> (meta fvar)
|
(-> (meta fvar)
|
||||||
(assoc :ns (-> ns ns-name str)))))))))))
|
(assoc :ns (-> ns ns-name str)))])))))))
|
||||||
|
|
||||||
(defn scan-ns
|
(defn scan-ns
|
||||||
[& nsyms]
|
[& nsyms]
|
||||||
|
|||||||
@ -6,27 +6,30 @@
|
|||||||
|
|
||||||
(ns app.util.svg
|
(ns app.util.svg
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[clojure.xml :as xml]
|
[clojure.xml :as xml]
|
||||||
[cuerdas.core :as str])
|
[cuerdas.core :as str])
|
||||||
(:import
|
(:import
|
||||||
javax.xml.XMLConstants
|
javax.xml.XMLConstants
|
||||||
|
java.io.InputStream
|
||||||
javax.xml.parsers.SAXParserFactory
|
javax.xml.parsers.SAXParserFactory
|
||||||
|
clojure.lang.XMLHandler
|
||||||
org.apache.commons.io.IOUtils))
|
org.apache.commons.io.IOUtils))
|
||||||
|
|
||||||
(defn- secure-parser-factory
|
(defn- secure-parser-factory
|
||||||
[s ch]
|
[^InputStream input ^XMLHandler handler]
|
||||||
(.. (doto (SAXParserFactory/newInstance)
|
(.. (doto (SAXParserFactory/newInstance)
|
||||||
(.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
|
(.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
|
||||||
(.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
|
(.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
|
||||||
(newSAXParser)
|
(newSAXParser)
|
||||||
(parse s ch)))
|
(parse input handler)))
|
||||||
|
|
||||||
(defn parse
|
(defn parse
|
||||||
[data]
|
[^String data]
|
||||||
(try
|
(try
|
||||||
(with-open [istream (IOUtils/toInputStream data "UTF-8")]
|
(dm/with-open [istream (IOUtils/toInputStream data "UTF-8")]
|
||||||
(xml/parse istream secure-parser-factory))
|
(xml/parse istream secure-parser-factory))
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
(l/warn :hint "error on processing svg"
|
(l/warn :hint "error on processing svg"
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
java.time.ZonedDateTime
|
java.time.ZonedDateTime
|
||||||
java.time.format.DateTimeFormatter
|
java.time.format.DateTimeFormatter
|
||||||
java.time.temporal.ChronoUnit
|
java.time.temporal.ChronoUnit
|
||||||
|
java.time.temporal.Temporal
|
||||||
java.time.temporal.TemporalAmount
|
java.time.temporal.TemporalAmount
|
||||||
java.time.temporal.TemporalUnit
|
java.time.temporal.TemporalUnit
|
||||||
java.util.Date
|
java.util.Date
|
||||||
@ -160,11 +161,29 @@
|
|||||||
|
|
||||||
(defn plus
|
(defn plus
|
||||||
[d ta]
|
[d ta]
|
||||||
(.plus d ^TemporalAmount (duration ta)))
|
(let [^TemporalAmount ta (duration ta)]
|
||||||
|
(cond
|
||||||
|
(instance? Duration d)
|
||||||
|
(.plus ^Duration d ta)
|
||||||
|
|
||||||
|
(instance? Temporal d)
|
||||||
|
(.plus ^Temporal d ta)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(throw (UnsupportedOperationException. "unsupported type")))))
|
||||||
|
|
||||||
(defn minus
|
(defn minus
|
||||||
[d ta]
|
[d ta]
|
||||||
(.minus d ^TemporalAmount (duration ta)))
|
(let [^TemporalAmount ta (duration ta)]
|
||||||
|
(cond
|
||||||
|
(instance? Duration d)
|
||||||
|
(.minus ^Duration d ta)
|
||||||
|
|
||||||
|
(instance? Temporal d)
|
||||||
|
(.minus ^Temporal d ta)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(throw (UnsupportedOperationException. "unsupported type")))))
|
||||||
|
|
||||||
(defn now
|
(defn now
|
||||||
[]
|
[]
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns app.util.websocket
|
(ns app.util.websocket
|
||||||
"A general protocol implementation on top of websockets."
|
"A general protocol implementation on top of websockets using vthreads."
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
@ -13,22 +13,42 @@
|
|||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.loggers.audit :refer [parse-client-ip]]
|
[app.loggers.audit :refer [parse-client-ip]]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.core.async :as a]
|
[promesa.exec :as px]
|
||||||
|
[promesa.exec.csp :as sp]
|
||||||
[yetti.request :as yr]
|
[yetti.request :as yr]
|
||||||
[yetti.util :as yu]
|
[yetti.util :as yu]
|
||||||
[yetti.websocket :as yws])
|
[yetti.websocket :as yws])
|
||||||
(:import
|
(:import
|
||||||
java.nio.ByteBuffer))
|
java.nio.ByteBuffer))
|
||||||
|
|
||||||
(declare decode-beat)
|
|
||||||
(declare encode-beat)
|
|
||||||
(declare start-io-loop)
|
|
||||||
(declare ws-ping!)
|
|
||||||
(declare ws-send!)
|
|
||||||
(declare filter-options)
|
|
||||||
|
|
||||||
(def noop (constantly nil))
|
(def noop (constantly nil))
|
||||||
(def identity-3 (fn [_ _ o] o))
|
(def identity-3 (fn [_ _ o] o))
|
||||||
|
(def max-missed-heartbeats 3)
|
||||||
|
(def heartbeat-interval 5000)
|
||||||
|
|
||||||
|
(defn- encode-beat
|
||||||
|
[n]
|
||||||
|
(doto (ByteBuffer/allocate 8)
|
||||||
|
(.putLong n)
|
||||||
|
(.rewind)))
|
||||||
|
|
||||||
|
(defn- decode-beat
|
||||||
|
[^ByteBuffer buffer]
|
||||||
|
(when (= 8 (.capacity buffer))
|
||||||
|
(.rewind buffer)
|
||||||
|
(.getLong buffer)))
|
||||||
|
|
||||||
|
(defn- wrap-handler
|
||||||
|
[handler]
|
||||||
|
(fn [wsp message]
|
||||||
|
(try
|
||||||
|
(handler wsp message)
|
||||||
|
(catch Throwable cause
|
||||||
|
(if (ex/error? cause)
|
||||||
|
{:type :error :error (ex-data cause)}
|
||||||
|
{:type :error :error {:message (ex-message cause)}})))))
|
||||||
|
|
||||||
|
(declare start-io-loop!)
|
||||||
|
|
||||||
(defn handler
|
(defn handler
|
||||||
"A WebSocket upgrade handler factory. Returns a handler that can be
|
"A WebSocket upgrade handler factory. Returns a handler that can be
|
||||||
@ -46,12 +66,11 @@
|
|||||||
::on-connect
|
::on-connect
|
||||||
::input-buff-size
|
::input-buff-size
|
||||||
::output-buff-size
|
::output-buff-size
|
||||||
::handler
|
|
||||||
::idle-timeout]
|
::idle-timeout]
|
||||||
:or {input-buff-size 64
|
:or {input-buff-size 64
|
||||||
output-buff-size 64
|
output-buff-size 64
|
||||||
idle-timeout 60000
|
idle-timeout 60000
|
||||||
on-connect noop
|
on-connect identity
|
||||||
on-snd-message identity-3
|
on-snd-message identity-3
|
||||||
on-rcv-message identity-3}
|
on-rcv-message identity-3}
|
||||||
:as options}]
|
:as options}]
|
||||||
@ -61,91 +80,65 @@
|
|||||||
(assert (fn? on-connect) "'on-connect' should be a function")
|
(assert (fn? on-connect) "'on-connect' should be a function")
|
||||||
|
|
||||||
(fn [{:keys [::yws/channel] :as request}]
|
(fn [{:keys [::yws/channel] :as request}]
|
||||||
(let [input-ch (a/chan input-buff-size)
|
(let [input-ch (sp/chan :buf input-buff-size)
|
||||||
output-ch (a/chan output-buff-size)
|
output-ch (sp/chan :buf output-buff-size)
|
||||||
hbeat-ch (a/chan (a/sliding-buffer 6))
|
hbeat-ch (sp/chan :buf (sp/sliding-buffer 6))
|
||||||
close-ch (a/chan)
|
close-ch (sp/chan)
|
||||||
stop-ch (a/chan)
|
|
||||||
|
|
||||||
ip-addr (parse-client-ip request)
|
ip-addr (parse-client-ip request)
|
||||||
uagent (yr/get-header request "user-agent")
|
uagent (yr/get-header request "user-agent")
|
||||||
id (uuid/next)
|
id (uuid/next)
|
||||||
|
state (atom {})
|
||||||
|
beats (atom #{})
|
||||||
|
|
||||||
options (-> (filter-options options)
|
options (-> options
|
||||||
(merge {::id id
|
(update ::handler wrap-handler)
|
||||||
::created-at (dt/now)
|
(assoc ::id id)
|
||||||
::input-ch input-ch
|
(assoc ::state state)
|
||||||
::heartbeat-ch hbeat-ch
|
(assoc ::beats beats)
|
||||||
::output-ch output-ch
|
(assoc ::created-at (dt/now))
|
||||||
::close-ch close-ch
|
(assoc ::input-ch input-ch)
|
||||||
::stop-ch stop-ch
|
(assoc ::heartbeat-ch hbeat-ch)
|
||||||
::channel channel
|
(assoc ::output-ch output-ch)
|
||||||
::remote-addr ip-addr
|
(assoc ::close-ch close-ch)
|
||||||
::user-agent uagent})
|
(assoc ::channel channel)
|
||||||
(atom))
|
(assoc ::remote-addr ip-addr)
|
||||||
|
(assoc ::user-agent uagent)
|
||||||
;; call the on-connect hook and memoize the on-terminate instance
|
(on-connect))
|
||||||
on-terminate (on-connect options)
|
|
||||||
|
|
||||||
on-ws-open
|
on-ws-open
|
||||||
(fn [channel]
|
(fn [channel]
|
||||||
(l/trace :fn "on-ws-open" :conn-id id)
|
(l/trace :fn "on-ws-open" :conn-id id)
|
||||||
(yws/idle-timeout! channel (dt/duration idle-timeout)))
|
(let [timeout (dt/duration idle-timeout)
|
||||||
|
name (str "penpot/websocket/io-loop/" id)]
|
||||||
|
(yws/idle-timeout! channel timeout)
|
||||||
|
(px/fn->thread (partial start-io-loop! options)
|
||||||
|
{:name name :virtual true})))
|
||||||
|
|
||||||
on-ws-terminate
|
on-ws-terminate
|
||||||
(fn [_ code reason]
|
(fn [_ code reason]
|
||||||
(l/trace :fn "on-ws-terminate" :conn-id id :code code :reason reason)
|
(l/trace :fn "on-ws-terminate"
|
||||||
(a/close! close-ch))
|
:conn-id id
|
||||||
|
:code code
|
||||||
|
:reason reason)
|
||||||
|
(sp/close! close-ch))
|
||||||
|
|
||||||
on-ws-error
|
on-ws-error
|
||||||
(fn [_ error]
|
(fn [_ cause]
|
||||||
(when-not (or (instance? java.nio.channels.ClosedChannelException error)
|
(sp/close! close-ch cause))
|
||||||
(instance? java.net.SocketException error)
|
|
||||||
(instance? java.io.IOException error))
|
|
||||||
(l/error :fn "on-ws-error" :conn-id id
|
|
||||||
:hint (ex-message error)
|
|
||||||
:cause error))
|
|
||||||
(on-ws-terminate nil 8801 "close after error"))
|
|
||||||
|
|
||||||
on-ws-message
|
on-ws-message
|
||||||
(fn [_ message]
|
(fn [_ message]
|
||||||
(try
|
(sp/offer! input-ch message)
|
||||||
(let [message (on-rcv-message options message)
|
(swap! state assoc ::last-activity-at (dt/now)))
|
||||||
message (t/decode-str message)]
|
|
||||||
(a/offer! input-ch message)
|
|
||||||
(swap! options assoc ::last-activity-at (dt/now)))
|
|
||||||
(catch Throwable e
|
|
||||||
(l/warn :hint "error on decoding incoming message from websocket"
|
|
||||||
:wsmsg (pr-str message)
|
|
||||||
:cause e)
|
|
||||||
(a/>! close-ch [8802 "decode error"])
|
|
||||||
(a/close! close-ch))))
|
|
||||||
|
|
||||||
on-ws-pong
|
on-ws-pong
|
||||||
(fn [_ buffers]
|
(fn [_ buffers]
|
||||||
(a/>!! hbeat-ch (yu/copy-many buffers)))]
|
;; (l/trace :fn "on-ws-pong" :buffers (pr-str buffers))
|
||||||
|
(sp/put! hbeat-ch (yu/copy-many buffers)))]
|
||||||
|
|
||||||
;; Wait a close signal
|
(yws/on-close! channel (fn [_]
|
||||||
(a/go
|
(sp/close! close-ch)))
|
||||||
(let [[code reason] (a/<! close-ch)]
|
|
||||||
(a/close! stop-ch)
|
|
||||||
(a/close! hbeat-ch)
|
|
||||||
(a/close! output-ch)
|
|
||||||
(a/close! input-ch)
|
|
||||||
|
|
||||||
(when (and code reason)
|
|
||||||
(l/trace :hint "close channel condition" :code code :reason reason)
|
|
||||||
(yws/close! channel code reason))
|
|
||||||
|
|
||||||
(when (fn? on-terminate)
|
|
||||||
(on-terminate))
|
|
||||||
|
|
||||||
(l/trace :hint "connection terminated")))
|
|
||||||
|
|
||||||
;; React on messages received from the client
|
|
||||||
(a/go
|
|
||||||
(a/<! (start-io-loop options handler on-snd-message on-ws-terminate))
|
|
||||||
(l/trace :hint "io loop terminated"))
|
|
||||||
|
|
||||||
{:on-open on-ws-open
|
{:on-open on-ws-open
|
||||||
:on-error on-ws-error
|
:on-error on-ws-error
|
||||||
@ -153,118 +146,81 @@
|
|||||||
:on-text on-ws-message
|
:on-text on-ws-message
|
||||||
:on-pong on-ws-pong})))
|
:on-pong on-ws-pong})))
|
||||||
|
|
||||||
(defn- ws-send!
|
(defn- handle-ping!
|
||||||
[channel s]
|
[{:keys [::id ::beats ::channel] :as wsp} beat-id]
|
||||||
(let [ch (a/chan 1)]
|
(l/trace :hint "ping" :beat beat-id :conn-id id)
|
||||||
|
(yws/ping! channel (encode-beat beat-id))
|
||||||
|
(let [issued (swap! beats conj (long beat-id))]
|
||||||
|
(not (>= (count issued) max-missed-heartbeats))))
|
||||||
|
|
||||||
|
(defn- start-io-loop!
|
||||||
|
[{:keys [::id ::close-ch ::input-ch ::output-ch ::heartbeat-ch ::channel ::handler ::beats ::on-rcv-message ::on-snd-message] :as wsp}]
|
||||||
|
(px/thread
|
||||||
|
{:name (str "penpot/websocket/io-loop/" id)
|
||||||
|
:virtual true}
|
||||||
(try
|
(try
|
||||||
(yws/send! channel s (fn [e]
|
(handler wsp {:type :open})
|
||||||
(when e (a/offer! ch e))
|
(loop [i 0]
|
||||||
(a/close! ch)))
|
(let [ping-ch (sp/timeout-chan heartbeat-interval)
|
||||||
|
[msg p] (sp/alts! [close-ch input-ch output-ch heartbeat-ch ping-ch])]
|
||||||
|
(when (yws/connected? channel)
|
||||||
|
(cond
|
||||||
|
(identical? p ping-ch)
|
||||||
|
(if (handle-ping! wsp i)
|
||||||
|
(recur (inc i))
|
||||||
|
(yws/close! channel 8802 "missing to many pings"))
|
||||||
|
|
||||||
|
(or (identical? p close-ch) (nil? msg))
|
||||||
|
(do :nothing)
|
||||||
|
|
||||||
|
(identical? p heartbeat-ch)
|
||||||
|
(let [beat (decode-beat msg)]
|
||||||
|
;; (l/trace :hint "pong" :beat beat :conn-id id)
|
||||||
|
(swap! beats disj beat)
|
||||||
|
(recur i))
|
||||||
|
|
||||||
|
(identical? p input-ch)
|
||||||
|
(let [message (t/decode-str msg)
|
||||||
|
message (on-rcv-message message)
|
||||||
|
{:keys [request-id] :as response} (handler wsp message)]
|
||||||
|
(when (map? response)
|
||||||
|
(sp/put! output-ch
|
||||||
|
(cond-> response
|
||||||
|
(some? request-id)
|
||||||
|
(assoc :request-id request-id))))
|
||||||
|
(recur i))
|
||||||
|
|
||||||
|
(identical? p output-ch)
|
||||||
|
(let [message (on-snd-message msg)
|
||||||
|
message (t/encode-str message {:type :json-verbose})]
|
||||||
|
;; (l/trace :hint "writing message to output" :message msg)
|
||||||
|
(yws/send! channel message)
|
||||||
|
(recur i))))))
|
||||||
|
|
||||||
|
(catch java.nio.channels.ClosedChannelException _)
|
||||||
|
(catch java.net.SocketException _)
|
||||||
|
(catch java.io.IOException _)
|
||||||
|
|
||||||
|
(catch InterruptedException _
|
||||||
|
(l/debug :hint "websocket thread interrumpted" :conn-id id))
|
||||||
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(a/offer! ch cause)
|
(l/error :hint "unhandled exception on websocket thread"
|
||||||
(a/close! ch)))
|
:conn-id id
|
||||||
ch))
|
:cause cause))
|
||||||
|
|
||||||
(defn- ws-ping!
|
(finally
|
||||||
[channel s]
|
(handler wsp {:type :close})
|
||||||
(let [ch (a/chan 1)]
|
|
||||||
(try
|
|
||||||
(yws/ping! channel s (fn [e]
|
|
||||||
(when e (a/offer! ch e))
|
|
||||||
(a/close! ch)))
|
|
||||||
(catch Throwable cause
|
|
||||||
(a/offer! ch cause)
|
|
||||||
(a/close! ch)))
|
|
||||||
ch))
|
|
||||||
|
|
||||||
(defn- encode-beat
|
(when (yws/connected? channel)
|
||||||
[n]
|
;; NOTE: we need to ignore all exceptions here because
|
||||||
(doto (ByteBuffer/allocate 8)
|
;; there can be a race condition that first returns that
|
||||||
(.putLong n)
|
;; channel is connected but on closing, will raise that
|
||||||
(.rewind)))
|
;; channel is already closed.
|
||||||
|
(ex/ignoring
|
||||||
|
(yws/close! channel 8899 "terminated")))
|
||||||
|
|
||||||
(defn- decode-beat
|
(when-let [on-disconnect (::on-disconnect wsp)]
|
||||||
[^ByteBuffer buffer]
|
(on-disconnect))
|
||||||
(when (= 8 (.capacity buffer))
|
|
||||||
(.rewind buffer)
|
|
||||||
(.getLong buffer)))
|
|
||||||
|
|
||||||
(defn- wrap-handler
|
(l/trace :hint "websocket thread terminated" :conn-id id)))))
|
||||||
[handler]
|
|
||||||
(fn [wsp message]
|
|
||||||
(locking wsp
|
|
||||||
(handler wsp message))))
|
|
||||||
|
|
||||||
(def max-missed-heartbeats 3)
|
|
||||||
(def heartbeat-interval 5000)
|
|
||||||
|
|
||||||
(defn- start-io-loop
|
|
||||||
[wsp handler on-snd-message on-ws-terminate]
|
|
||||||
(let [input-ch (::input-ch @wsp)
|
|
||||||
output-ch (::output-ch @wsp)
|
|
||||||
stop-ch (::stop-ch @wsp)
|
|
||||||
hbeat-pong-ch (::heartbeat-ch @wsp)
|
|
||||||
channel (::channel @wsp)
|
|
||||||
conn-id (::id @wsp)
|
|
||||||
handler (wrap-handler handler)
|
|
||||||
beats (atom #{})
|
|
||||||
choices [stop-ch
|
|
||||||
input-ch
|
|
||||||
output-ch
|
|
||||||
hbeat-pong-ch]]
|
|
||||||
|
|
||||||
;; Start IO loop
|
|
||||||
(a/go
|
|
||||||
(a/<! (handler wsp {:type :connect}))
|
|
||||||
(a/<! (a/go-loop [i 0]
|
|
||||||
(let [hbeat-ping-ch (a/timeout heartbeat-interval)
|
|
||||||
[v p] (a/alts! (conj choices hbeat-ping-ch))]
|
|
||||||
(cond
|
|
||||||
(not (yws/connected? channel))
|
|
||||||
(on-ws-terminate nil 8800 "channel disconnected")
|
|
||||||
|
|
||||||
(= p hbeat-ping-ch)
|
|
||||||
(do
|
|
||||||
(l/trace :hint "ping" :beat i :conn-id conn-id)
|
|
||||||
(a/<! (ws-ping! channel (encode-beat i)))
|
|
||||||
(let [issued (swap! beats conj (long i))]
|
|
||||||
(if (>= (count issued) max-missed-heartbeats)
|
|
||||||
(on-ws-terminate nil 8802 "heartbeat: timeout")
|
|
||||||
(recur (inc i)))))
|
|
||||||
|
|
||||||
(= p hbeat-pong-ch)
|
|
||||||
(let [beat (decode-beat v)]
|
|
||||||
(l/trace :hint "pong" :beat beat :conn-id conn-id)
|
|
||||||
(swap! beats disj beat)
|
|
||||||
(recur i))
|
|
||||||
|
|
||||||
(= p input-ch)
|
|
||||||
(let [result (a/<! (handler wsp v))]
|
|
||||||
;; (l/trace :hint "message received" :message v)
|
|
||||||
(cond
|
|
||||||
(ex/error? result)
|
|
||||||
(a/>! output-ch {:type :error :error (ex-data result)})
|
|
||||||
|
|
||||||
(ex/exception? result)
|
|
||||||
(a/>! output-ch {:type :error :error {:message (ex-message result)}})
|
|
||||||
|
|
||||||
(map? result)
|
|
||||||
(a/>! output-ch (cond-> result (:request-id v) (assoc :request-id (:request-id v)))))
|
|
||||||
(recur i))
|
|
||||||
|
|
||||||
(= p output-ch)
|
|
||||||
(let [v (on-snd-message wsp v)]
|
|
||||||
;; (l/trace :hint "writing message to output" :message v)
|
|
||||||
(a/<! (ws-send! channel (t/encode-str v)))
|
|
||||||
(recur i))))))
|
|
||||||
|
|
||||||
(a/<! (handler wsp {:type :disconnect})))))
|
|
||||||
|
|
||||||
(defn- filter-options
|
|
||||||
"Remove from options all namespace qualified keys that matches the
|
|
||||||
current namespace."
|
|
||||||
[options]
|
|
||||||
(into {}
|
|
||||||
(remove (fn [[key]]
|
|
||||||
(= (namespace key) "app.util.websocket")))
|
|
||||||
options))
|
|
||||||
|
|||||||
@ -22,17 +22,16 @@
|
|||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
|
[promesa.core :as p]
|
||||||
[promesa.exec :as px])
|
[promesa.exec :as px])
|
||||||
(:import
|
(:import
|
||||||
java.util.concurrent.ExecutorService
|
java.util.concurrent.ExecutorService
|
||||||
java.util.concurrent.ForkJoinPool
|
java.util.concurrent.ForkJoinPool
|
||||||
java.util.concurrent.Future
|
java.util.concurrent.Future))
|
||||||
java.util.concurrent.ScheduledExecutorService))
|
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(s/def ::executor #(instance? ExecutorService %))
|
(s/def ::executor #(instance? ExecutorService %))
|
||||||
(s/def ::scheduled-executor #(instance? ScheduledExecutorService %))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Executor
|
;; Executor
|
||||||
@ -47,29 +46,23 @@
|
|||||||
[skey {:keys [::parallelism]}]
|
[skey {:keys [::parallelism]}]
|
||||||
(let [prefix (if (vector? skey) (-> skey first name) "default")
|
(let [prefix (if (vector? skey) (-> skey first name) "default")
|
||||||
tname (str "penpot/" prefix "/%s")
|
tname (str "penpot/" prefix "/%s")
|
||||||
factory (px/forkjoin-thread-factory :name tname)]
|
ttype (cf/get :worker-executor-type :fjoin)]
|
||||||
(px/forkjoin-executor
|
(case ttype
|
||||||
:factory factory
|
:fjoin
|
||||||
:parallelism parallelism
|
(let [factory (px/forkjoin-thread-factory :name tname)]
|
||||||
:async? true)))
|
(px/forkjoin-executor {:factory factory
|
||||||
|
:core-size (px/get-available-processors)
|
||||||
|
:parallelism parallelism
|
||||||
|
:async true}))
|
||||||
|
|
||||||
|
:cached
|
||||||
|
(let [factory (px/thread-factory :name tname)]
|
||||||
|
(px/cached-executor :factory factory)))))
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::executor
|
(defmethod ig/halt-key! ::executor
|
||||||
[_ instance]
|
[_ instance]
|
||||||
(px/shutdown! instance))
|
(px/shutdown! instance))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::scheduled-executor [_]
|
|
||||||
(s/keys :req [::parallelism]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::scheduled-executor
|
|
||||||
[_ {:keys [::parallelism]}]
|
|
||||||
(px/scheduled-executor
|
|
||||||
:parallelism parallelism
|
|
||||||
:factory (px/thread-factory :name "penpot/scheduled-executor/%s")))
|
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::scheduled-executor
|
|
||||||
[_ instance]
|
|
||||||
(px/shutdown! instance))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; TASKS REGISTRY
|
;; TASKS REGISTRY
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -147,7 +140,7 @@
|
|||||||
steals))]
|
steals))]
|
||||||
|
|
||||||
(px/thread
|
(px/thread
|
||||||
{:name "penpot/executors-monitor"}
|
{:name "penpot/executors-monitor" :virtual true}
|
||||||
(l/info :hint "monitor: started" :name name)
|
(l/info :hint "monitor: started" :name name)
|
||||||
(try
|
(try
|
||||||
(loop [steals 0]
|
(loop [steals 0]
|
||||||
@ -220,53 +213,48 @@
|
|||||||
:queued res)))
|
:queued res)))
|
||||||
|
|
||||||
(run-batch! [rconn]
|
(run-batch! [rconn]
|
||||||
(db/with-atomic [conn pool]
|
(try
|
||||||
(when-let [tasks (get-tasks conn)]
|
(db/with-atomic [conn pool]
|
||||||
(->> (group-by :queue tasks)
|
(if-let [tasks (get-tasks conn)]
|
||||||
(run! (partial push-tasks! conn rconn)))
|
(->> (group-by :queue tasks)
|
||||||
true)))]
|
(run! (partial push-tasks! conn rconn)))
|
||||||
|
(px/sleep (::wait-duration cfg))))
|
||||||
|
(catch InterruptedException cause
|
||||||
|
(throw cause))
|
||||||
|
(catch Exception cause
|
||||||
|
(cond
|
||||||
|
(rds/exception? cause)
|
||||||
|
(do
|
||||||
|
(l/warn :hint "dispatcher: redis exception (will retry in an instant)" :cause cause)
|
||||||
|
(px/sleep (::rds/timeout rconn)))
|
||||||
|
|
||||||
|
(db/sql-exception? cause)
|
||||||
|
(do
|
||||||
|
(l/warn :hint "dispatcher: database exception (will retry in an instant)" :cause cause)
|
||||||
|
(px/sleep (::rds/timeout rconn)))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(do
|
||||||
|
(l/error :hint "dispatcher: unhandled exception (will retry in an instant)" :cause cause)
|
||||||
|
(px/sleep (::rds/timeout rconn)))))))
|
||||||
|
|
||||||
|
(dispatcher []
|
||||||
|
(l/info :hint "dispatcher: started")
|
||||||
|
(try
|
||||||
|
(dm/with-open [rconn (rds/connect redis)]
|
||||||
|
(loop []
|
||||||
|
(run-batch! rconn)
|
||||||
|
(recur)))
|
||||||
|
(catch InterruptedException _
|
||||||
|
(l/trace :hint "dispatcher: interrupted"))
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/error :hint "dispatcher: unexpected exception" :cause cause))
|
||||||
|
(finally
|
||||||
|
(l/info :hint "dispatcher: terminated"))))]
|
||||||
|
|
||||||
(if (db/read-only? pool)
|
(if (db/read-only? pool)
|
||||||
(l/warn :hint "dispatcher: not started (db is read-only)")
|
(l/warn :hint "dispatcher: not started (db is read-only)")
|
||||||
(px/thread
|
(px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual true))))
|
||||||
{:name "penpot/worker-dispatcher"}
|
|
||||||
(l/info :hint "dispatcher: started")
|
|
||||||
(try
|
|
||||||
(dm/with-open [rconn (rds/connect redis)]
|
|
||||||
(loop []
|
|
||||||
(when (px/interrupted?)
|
|
||||||
(throw (InterruptedException. "interrumpted")))
|
|
||||||
|
|
||||||
(try
|
|
||||||
(when-not (run-batch! rconn)
|
|
||||||
(px/sleep (::wait-duration cfg)))
|
|
||||||
(catch InterruptedException cause
|
|
||||||
(throw cause))
|
|
||||||
(catch Exception cause
|
|
||||||
(cond
|
|
||||||
(rds/exception? cause)
|
|
||||||
(do
|
|
||||||
(l/warn :hint "dispatcher: redis exception (will retry in an instant)" :cause cause)
|
|
||||||
(px/sleep (::rds/timeout rconn)))
|
|
||||||
|
|
||||||
(db/sql-exception? cause)
|
|
||||||
(do
|
|
||||||
(l/warn :hint "dispatcher: database exception (will retry in an instant)" :cause cause)
|
|
||||||
(px/sleep (::rds/timeout rconn)))
|
|
||||||
|
|
||||||
:else
|
|
||||||
(do
|
|
||||||
(l/error :hint "dispatcher: unhandled exception (will retry in an instant)" :cause cause)
|
|
||||||
(px/sleep (::rds/timeout rconn))))))
|
|
||||||
|
|
||||||
(recur)))
|
|
||||||
|
|
||||||
(catch InterruptedException _
|
|
||||||
(l/debug :hint "dispatcher: interrupted"))
|
|
||||||
(catch Throwable cause
|
|
||||||
(l/error :hint "dispatcher: unexpected exception" :cause cause))
|
|
||||||
(finally
|
|
||||||
(l/info :hint "dispatcher: terminated")))))))
|
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::dispatcher
|
(defmethod ig/halt-key! ::dispatcher
|
||||||
[_ thread]
|
[_ thread]
|
||||||
@ -311,7 +299,7 @@
|
|||||||
(defn- start-worker!
|
(defn- start-worker!
|
||||||
[{:keys [::rds/redis ::worker-id ::queue] :as cfg}]
|
[{:keys [::rds/redis ::worker-id ::queue] :as cfg}]
|
||||||
(px/thread
|
(px/thread
|
||||||
{:name (format "penpot/worker/%s" worker-id)}
|
{:name (format "penpot/worker/runner:%s" worker-id)}
|
||||||
(l/info :hint "worker: started" :worker-id worker-id :queue queue)
|
(l/info :hint "worker: started" :worker-id worker-id :queue queue)
|
||||||
(try
|
(try
|
||||||
(dm/with-open [rconn (rds/connect redis)]
|
(dm/with-open [rconn (rds/connect redis)]
|
||||||
@ -462,7 +450,8 @@
|
|||||||
(case status
|
(case status
|
||||||
:retry (handle-task-retry result)
|
:retry (handle-task-retry result)
|
||||||
:failed (handle-task-failure result)
|
:failed (handle-task-failure result)
|
||||||
:completed (handle-task-completion result))))
|
:completed (handle-task-completion result)
|
||||||
|
nil)))
|
||||||
|
|
||||||
(run-task-loop [task-id]
|
(run-task-loop [task-id]
|
||||||
(loop [result (run-task task-id)]
|
(loop [result (run-task task-id)]
|
||||||
@ -531,7 +520,7 @@
|
|||||||
(s/def ::entries (s/coll-of (s/nilable ::cron-task)))
|
(s/def ::entries (s/coll-of (s/nilable ::cron-task)))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::cron [_]
|
(defmethod ig/pre-init-spec ::cron [_]
|
||||||
(s/keys :req [::scheduled-executor ::db/pool ::entries ::registry]))
|
(s/keys :req [::db/pool ::entries ::registry]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::cron
|
(defmethod ig/init-key ::cron
|
||||||
[_ {:keys [::entries ::registry ::db/pool] :as cfg}]
|
[_ {:keys [::entries ::registry ::db/pool] :as cfg}]
|
||||||
@ -598,22 +587,23 @@
|
|||||||
|
|
||||||
(defn- execute-cron-task
|
(defn- execute-cron-task
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [id] :as task}]
|
[{:keys [::db/pool] :as cfg} {:keys [id] :as task}]
|
||||||
(try
|
(px/thread
|
||||||
(db/with-atomic [conn pool]
|
{:name (str "penpot/cront-task/" id)}
|
||||||
(when (db/exec-one! conn [sql:lock-cron-task (d/name id)])
|
(try
|
||||||
(l/trace :hint "cron: execute task" :task-id id)
|
(db/with-atomic [conn pool]
|
||||||
((:fn task) task)))
|
(when (db/exec-one! conn [sql:lock-cron-task (d/name id)])
|
||||||
(catch InterruptedException _
|
(l/trace :hint "cron: execute task" :task-id id)
|
||||||
(px/interrupt! (px/current-thread))
|
((:fn task) task)))
|
||||||
(l/debug :hint "cron: task interrupted" :task-id id))
|
(catch InterruptedException _
|
||||||
(catch Throwable cause
|
(l/debug :hint "cron: task interrupted" :task-id id))
|
||||||
(l/error :hint "cron: unhandled exception on running task"
|
(catch Throwable cause
|
||||||
::l/context (get-error-context cause task)
|
(l/error :hint "cron: unhandled exception on running task"
|
||||||
:task-id id
|
::l/context (get-error-context cause task)
|
||||||
:cause cause))
|
:task-id id
|
||||||
(finally
|
:cause cause))
|
||||||
(when-not (px/interrupted? :current)
|
(finally
|
||||||
(schedule-cron-task cfg task)))))
|
(when-not (px/interrupted? :current)
|
||||||
|
(schedule-cron-task cfg task))))))
|
||||||
|
|
||||||
(defn- ms-until-valid
|
(defn- ms-until-valid
|
||||||
[cron]
|
[cron]
|
||||||
@ -622,16 +612,11 @@
|
|||||||
next (dt/next-valid-instant-from cron now)]
|
next (dt/next-valid-instant-from cron now)]
|
||||||
(inst-ms (dt/diff now next))))
|
(inst-ms (dt/diff now next))))
|
||||||
|
|
||||||
(def ^:private
|
|
||||||
xf-without-done
|
|
||||||
(remove #(.isDone ^Future %)))
|
|
||||||
|
|
||||||
(defn- schedule-cron-task
|
(defn- schedule-cron-task
|
||||||
[{:keys [::scheduled-executor ::running] :as cfg} {:keys [cron] :as task}]
|
[{:keys [::running] :as cfg} {:keys [cron] :as task}]
|
||||||
(let [ft (px/schedule! scheduled-executor
|
(let [ft (px/schedule! (ms-until-valid cron)
|
||||||
(ms-until-valid cron)
|
|
||||||
(partial execute-cron-task cfg task))]
|
(partial execute-cron-task cfg task))]
|
||||||
(swap! running #(into #{ft} xf-without-done %))))
|
(swap! running #(into #{ft} (filter p/pending?) %))))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.auth]
|
[app.auth]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.flags :as flags]
|
[app.common.flags :as flags]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
@ -65,6 +66,47 @@
|
|||||||
:enable-smtp
|
:enable-smtp
|
||||||
:enable-quotes])
|
:enable-quotes])
|
||||||
|
|
||||||
|
(def test-init-sql
|
||||||
|
["alter table project_profile_rel set unlogged;\n"
|
||||||
|
"alter table file_profile_rel set unlogged;\n"
|
||||||
|
"alter table presence set unlogged;\n"
|
||||||
|
"alter table presence set unlogged;\n"
|
||||||
|
"alter table http_session set unlogged;\n"
|
||||||
|
"alter table team_profile_rel set unlogged;\n"
|
||||||
|
"alter table team_project_profile_rel set unlogged;\n"
|
||||||
|
"alter table comment_thread_status set unlogged;\n"
|
||||||
|
"alter table comment set unlogged;\n"
|
||||||
|
"alter table comment_thread set unlogged;\n"
|
||||||
|
"alter table profile_complaint_report set unlogged;\n"
|
||||||
|
"alter table file_change set unlogged;\n"
|
||||||
|
"alter table team_font_variant set unlogged;\n"
|
||||||
|
"alter table share_link set unlogged;\n"
|
||||||
|
"alter table usage_quote set unlogged;\n"
|
||||||
|
"alter table access_token set unlogged;\n"
|
||||||
|
"alter table profile set unlogged;\n"
|
||||||
|
"alter table file_library_rel set unlogged;\n"
|
||||||
|
"alter table file_thumbnail set unlogged;\n"
|
||||||
|
"alter table file_object_thumbnail set unlogged;\n"
|
||||||
|
"alter table file_media_object set unlogged;\n"
|
||||||
|
"alter table file_data_fragment set unlogged;\n"
|
||||||
|
"alter table file set unlogged;\n"
|
||||||
|
"alter table project set unlogged;\n"
|
||||||
|
"alter table team_invitation set unlogged;\n"
|
||||||
|
"alter table webhook_delivery set unlogged;\n"
|
||||||
|
"alter table webhook set unlogged;\n"
|
||||||
|
"alter table team set unlogged;\n"
|
||||||
|
;; For some reason, modifying the task realted tables is very very
|
||||||
|
;; slow (5s); so we just don't alter them
|
||||||
|
;; "alter table task set unlogged;\n"
|
||||||
|
;; "alter table task_default set unlogged;\n"
|
||||||
|
;; "alter table task_completed set unlogged;\n"
|
||||||
|
"alter table audit_log_default set unlogged ;\n"
|
||||||
|
"alter table storage_object set unlogged;\n"
|
||||||
|
"alter table server_error_report set unlogged;\n"
|
||||||
|
"alter table server_prop set unlogged;\n"
|
||||||
|
"alter table global_complaint_report set unlogged;\n"
|
||||||
|
])
|
||||||
|
|
||||||
(defn state-init
|
(defn state-init
|
||||||
[next]
|
[next]
|
||||||
(with-redefs [app.config/flags (flags/parse flags/default default-flags)
|
(with-redefs [app.config/flags (flags/parse flags/default default-flags)
|
||||||
@ -97,9 +139,7 @@
|
|||||||
:app.http.oauth/handler
|
:app.http.oauth/handler
|
||||||
:app.notifications/handler
|
:app.notifications/handler
|
||||||
:app.loggers.mattermost/reporter
|
:app.loggers.mattermost/reporter
|
||||||
:app.loggers.loki/reporter
|
|
||||||
:app.loggers.database/reporter
|
:app.loggers.database/reporter
|
||||||
:app.loggers.zmq/receiver
|
|
||||||
:app.worker/cron
|
:app.worker/cron
|
||||||
:app.worker/worker))
|
:app.worker/worker))
|
||||||
_ (ig/load-namespaces system)
|
_ (ig/load-namespaces system)
|
||||||
@ -108,6 +148,9 @@
|
|||||||
(try
|
(try
|
||||||
(binding [*system* system
|
(binding [*system* system
|
||||||
*pool* (:app.db/pool system)]
|
*pool* (:app.db/pool system)]
|
||||||
|
(db/with-atomic [conn *pool*]
|
||||||
|
(doseq [sql test-init-sql]
|
||||||
|
(db/exec! conn [sql])))
|
||||||
(next))
|
(next))
|
||||||
(finally
|
(finally
|
||||||
(ig/halt! system))))))
|
(ig/halt! system))))))
|
||||||
@ -120,11 +163,15 @@
|
|||||||
" AND table_name != 'migrations';")]
|
" AND table_name != 'migrations';")]
|
||||||
(db/with-atomic [conn *pool*]
|
(db/with-atomic [conn *pool*]
|
||||||
(let [result (->> (db/exec! conn [sql])
|
(let [result (->> (db/exec! conn [sql])
|
||||||
(map :table-name))]
|
(map :table-name)
|
||||||
(db/exec! conn [(str "TRUNCATE "
|
(remove #(= "task" %)))
|
||||||
(apply str (interpose ", " result))
|
sql (str "TRUNCATE "
|
||||||
" CASCADE;")]))))
|
(apply str (interpose ", " result))
|
||||||
(next))
|
" CASCADE;")]
|
||||||
|
(doseq [table result]
|
||||||
|
(db/exec! conn [(str "delete from " table ";")]))))
|
||||||
|
|
||||||
|
(next)))
|
||||||
|
|
||||||
(defn clean-storage
|
(defn clean-storage
|
||||||
[next]
|
[next]
|
||||||
@ -162,7 +209,7 @@
|
|||||||
:password "123123"
|
:password "123123"
|
||||||
:is-demo false}
|
:is-demo false}
|
||||||
params)]
|
params)]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(->> params
|
(->> params
|
||||||
(cmd.auth/create-profile! conn)
|
(cmd.auth/create-profile! conn)
|
||||||
(cmd.auth/create-profile-rels! conn))))))
|
(cmd.auth/create-profile-rels! conn))))))
|
||||||
@ -172,7 +219,7 @@
|
|||||||
([pool i {:keys [profile-id team-id] :as params}]
|
([pool i {:keys [profile-id team-id] :as params}]
|
||||||
(us/assert uuid? profile-id)
|
(us/assert uuid? profile-id)
|
||||||
(us/assert uuid? team-id)
|
(us/assert uuid? team-id)
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(->> (merge {:id (mk-uuid "project" i)
|
(->> (merge {:id (mk-uuid "project" i)
|
||||||
:name (str "project" i)}
|
:name (str "project" i)}
|
||||||
params)
|
params)
|
||||||
@ -184,7 +231,7 @@
|
|||||||
([pool i {:keys [profile-id project-id] :as params}]
|
([pool i {:keys [profile-id project-id] :as params}]
|
||||||
(us/assert uuid? profile-id)
|
(us/assert uuid? profile-id)
|
||||||
(us/assert uuid? project-id)
|
(us/assert uuid? project-id)
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(files.create/create-file conn
|
(files.create/create-file conn
|
||||||
(merge {:id (mk-uuid "file" i)
|
(merge {:id (mk-uuid "file" i)
|
||||||
:name (str "file" i)
|
:name (str "file" i)
|
||||||
@ -200,7 +247,7 @@
|
|||||||
([i params] (create-team* *pool* i params))
|
([i params] (create-team* *pool* i params))
|
||||||
([pool i {:keys [profile-id] :as params}]
|
([pool i {:keys [profile-id] :as params}]
|
||||||
(us/assert uuid? profile-id)
|
(us/assert uuid? profile-id)
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(let [id (mk-uuid "team" i)]
|
(let [id (mk-uuid "team" i)]
|
||||||
(teams/create-team conn {:id id
|
(teams/create-team conn {:id id
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
@ -211,7 +258,7 @@
|
|||||||
([pool {:keys [name width height mtype file-id is-local media-id]
|
([pool {:keys [name width height mtype file-id is-local media-id]
|
||||||
:or {name "sample" width 100 height 100 mtype "image/svg+xml" is-local true}}]
|
:or {name "sample" width 100 height 100 mtype "image/svg+xml" is-local true}}]
|
||||||
|
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(db/insert! conn :file-media-object
|
(db/insert! conn :file-media-object
|
||||||
{:id (uuid/next)
|
{:id (uuid/next)
|
||||||
:file-id file-id
|
:file-id file-id
|
||||||
@ -225,12 +272,12 @@
|
|||||||
(defn link-file-to-library*
|
(defn link-file-to-library*
|
||||||
([params] (link-file-to-library* *pool* params))
|
([params] (link-file-to-library* *pool* params))
|
||||||
([pool {:keys [file-id library-id] :as params}]
|
([pool {:keys [file-id library-id] :as params}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(#'files/link-file-to-library conn {:file-id file-id :library-id library-id}))))
|
(#'files/link-file-to-library conn {:file-id file-id :library-id library-id}))))
|
||||||
|
|
||||||
(defn create-complaint-for
|
(defn create-complaint-for
|
||||||
[pool {:keys [id created-at type]}]
|
[pool {:keys [id created-at type]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(db/insert! conn :profile-complaint-report
|
(db/insert! conn :profile-complaint-report
|
||||||
{:profile-id id
|
{:profile-id id
|
||||||
:created-at (or created-at (dt/now))
|
:created-at (or created-at (dt/now))
|
||||||
@ -239,7 +286,7 @@
|
|||||||
|
|
||||||
(defn create-global-complaint-for
|
(defn create-global-complaint-for
|
||||||
[pool {:keys [email type created-at]}]
|
[pool {:keys [email type created-at]}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(db/insert! conn :global-complaint-report
|
(db/insert! conn :global-complaint-report
|
||||||
{:email email
|
{:email email
|
||||||
:type (name type)
|
:type (name type)
|
||||||
@ -249,7 +296,7 @@
|
|||||||
(defn create-team-role*
|
(defn create-team-role*
|
||||||
([params] (create-team-role* *pool* params))
|
([params] (create-team-role* *pool* params))
|
||||||
([pool {:keys [team-id profile-id role] :or {role :owner}}]
|
([pool {:keys [team-id profile-id role] :or {role :owner}}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(#'teams/create-team-role conn {:team-id team-id
|
(#'teams/create-team-role conn {:team-id team-id
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:role role}))))
|
:role role}))))
|
||||||
@ -257,7 +304,7 @@
|
|||||||
(defn create-project-role*
|
(defn create-project-role*
|
||||||
([params] (create-project-role* *pool* params))
|
([params] (create-project-role* *pool* params))
|
||||||
([pool {:keys [project-id profile-id role] :or {role :owner}}]
|
([pool {:keys [project-id profile-id role] :or {role :owner}}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(#'teams/create-project-role conn {:project-id project-id
|
(#'teams/create-project-role conn {:project-id project-id
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:role role}))))
|
:role role}))))
|
||||||
@ -265,7 +312,7 @@
|
|||||||
(defn create-file-role*
|
(defn create-file-role*
|
||||||
([params] (create-file-role* *pool* params))
|
([params] (create-file-role* *pool* params))
|
||||||
([pool {:keys [file-id profile-id role] :or {role :owner}}]
|
([pool {:keys [file-id profile-id role] :or {role :owner}}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(files.create/create-file-role! conn {:file-id file-id
|
(files.create/create-file-role! conn {:file-id file-id
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:role role}))))
|
:role role}))))
|
||||||
@ -274,10 +321,10 @@
|
|||||||
([params] (update-file* *pool* params))
|
([params] (update-file* *pool* params))
|
||||||
([pool {:keys [file-id changes session-id profile-id revn]
|
([pool {:keys [file-id changes session-id profile-id revn]
|
||||||
:or {session-id (uuid/next) revn 0}}]
|
:or {session-id (uuid/next) revn 0}}]
|
||||||
(with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
(let [features #{"components/v2"}
|
(let [features #{"components/v2"}
|
||||||
cfg (-> (select-keys *system* [::mbus/msgbus ::mtx/metrics])
|
cfg (-> (select-keys *system* [::mbus/msgbus ::mtx/metrics])
|
||||||
(assoc :conn conn))]
|
(assoc ::db/conn conn))]
|
||||||
(files.update/update-file cfg
|
(files.update/update-file cfg
|
||||||
{:id file-id
|
{:id file-id
|
||||||
:revn revn
|
:revn revn
|
||||||
@ -310,7 +357,7 @@
|
|||||||
(defmacro try-on!
|
(defmacro try-on!
|
||||||
[expr]
|
[expr]
|
||||||
`(try
|
`(try
|
||||||
(let [result# (deref ~expr)
|
(let [result# ~expr
|
||||||
result# (cond-> result# (rph/wrapped? result#) deref)]
|
result# (cond-> result# (rph/wrapped? result#) deref)]
|
||||||
{:error nil
|
{:error nil
|
||||||
:result result#})
|
:result result#})
|
||||||
@ -320,7 +367,7 @@
|
|||||||
|
|
||||||
(defn command!
|
(defn command!
|
||||||
[{:keys [::type] :as data}]
|
[{:keys [::type] :as data}]
|
||||||
(let [method-fn (get-in *system* [:app.rpc/methods :commands type])]
|
(let [[mdata method-fn] (get-in *system* [:app.rpc/methods :commands type])]
|
||||||
(when-not method-fn
|
(when-not method-fn
|
||||||
(ex/raise :type :assertion
|
(ex/raise :type :assertion
|
||||||
:code :rpc-method-not-found
|
:code :rpc-method-not-found
|
||||||
@ -333,7 +380,7 @@
|
|||||||
|
|
||||||
(defn mutation!
|
(defn mutation!
|
||||||
[{:keys [::type profile-id] :as data}]
|
[{:keys [::type profile-id] :as data}]
|
||||||
(let [method-fn (get-in *system* [:app.rpc/methods :mutations type])]
|
(let [[mdata method-fn] (get-in *system* [:app.rpc/methods :mutations type])]
|
||||||
(try-on! (method-fn (-> data
|
(try-on! (method-fn (-> data
|
||||||
(dissoc ::type)
|
(dissoc ::type)
|
||||||
(assoc ::rpc/profile-id profile-id)
|
(assoc ::rpc/profile-id profile-id)
|
||||||
@ -341,7 +388,7 @@
|
|||||||
|
|
||||||
(defn query!
|
(defn query!
|
||||||
[{:keys [::type profile-id] :as data}]
|
[{:keys [::type profile-id] :as data}]
|
||||||
(let [method-fn (get-in *system* [:app.rpc/methods :queries type])]
|
(let [[mdata method-fn] (get-in *system* [:app.rpc/methods :queries type])]
|
||||||
(try-on! (method-fn (-> data
|
(try-on! (method-fn (-> data
|
||||||
(dissoc ::type)
|
(dissoc ::type)
|
||||||
(assoc ::rpc/profile-id profile-id)
|
(assoc ::rpc/profile-id profile-id)
|
||||||
|
|||||||
@ -40,6 +40,6 @@
|
|||||||
{:keys [error result]} (th/command! (assoc params ::cond/key etag))]
|
{:keys [error result]} (th/command! (assoc params ::cond/key etag))]
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (fn? result))
|
(t/is (fn? result))
|
||||||
(t/is (= 304 (-> (result nil) :status))))
|
(t/is (= 304 (-> (result nil) :yetti.response/status))))
|
||||||
))))
|
))))
|
||||||
|
|
||||||
|
|||||||
@ -215,10 +215,10 @@
|
|||||||
(t/is (= 1 (count rows))))
|
(t/is (= 1 (count rows))))
|
||||||
|
|
||||||
;; The underlying storage objects are still available.
|
;; The underlying storage objects are still available.
|
||||||
(t/is (some? @(sto/get-object storage (:media-id fmo2))))
|
(t/is (some? (sto/get-object storage (:media-id fmo2))))
|
||||||
(t/is (some? @(sto/get-object storage (:thumbnail-id fmo2))))
|
(t/is (some? (sto/get-object storage (:thumbnail-id fmo2))))
|
||||||
(t/is (some? @(sto/get-object storage (:media-id fmo1))))
|
(t/is (some? (sto/get-object storage (:media-id fmo1))))
|
||||||
(t/is (some? @(sto/get-object storage (:thumbnail-id fmo1))))
|
(t/is (some? (sto/get-object storage (:thumbnail-id fmo1))))
|
||||||
|
|
||||||
;; proceed to remove usage of the file
|
;; proceed to remove usage of the file
|
||||||
(update-file {:file-id (:id file)
|
(update-file {:file-id (:id file)
|
||||||
@ -246,10 +246,10 @@
|
|||||||
;; Finally, check that some of the objects that are marked as
|
;; Finally, check that some of the objects that are marked as
|
||||||
;; deleted we are unable to retrieve them using standard storage
|
;; deleted we are unable to retrieve them using standard storage
|
||||||
;; public api.
|
;; public api.
|
||||||
(t/is (nil? @(sto/get-object storage (:media-id fmo2))))
|
(t/is (nil? (sto/get-object storage (:media-id fmo2))))
|
||||||
(t/is (nil? @(sto/get-object storage (:thumbnail-id fmo2))))
|
(t/is (nil? (sto/get-object storage (:thumbnail-id fmo2))))
|
||||||
(t/is (nil? @(sto/get-object storage (:media-id fmo1))))
|
(t/is (nil? (sto/get-object storage (:media-id fmo1))))
|
||||||
(t/is (nil? @(sto/get-object storage (:thumbnail-id fmo1))))
|
(t/is (nil? (sto/get-object storage (:thumbnail-id fmo1))))
|
||||||
)))
|
)))
|
||||||
|
|
||||||
(t/deftest permissions-checks-creating-file
|
(t/deftest permissions-checks-creating-file
|
||||||
|
|||||||
@ -26,9 +26,9 @@
|
|||||||
(let [storage (-> (:app.storage/storage th/*system*)
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
(configure-storage-backend))
|
(configure-storage-backend))
|
||||||
|
|
||||||
sobject @(sto/put-object! storage {::sto/content (sto/content "content")
|
sobject (sto/put-object! storage {::sto/content (sto/content "content")
|
||||||
:content-type "text/plain"
|
:content-type "text/plain"
|
||||||
:other "data"})
|
:other "data"})
|
||||||
profile (th/create-profile* 1 {:is-active true})
|
profile (th/create-profile* 1 {:is-active true})
|
||||||
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||||
:profile-id (:id profile)})
|
:profile-id (:id profile)})
|
||||||
@ -98,9 +98,9 @@
|
|||||||
(t/deftest duplicate-file-with-deleted-relations
|
(t/deftest duplicate-file-with-deleted-relations
|
||||||
(let [storage (-> (:app.storage/storage th/*system*)
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
(configure-storage-backend))
|
(configure-storage-backend))
|
||||||
sobject @(sto/put-object! storage {::sto/content (sto/content "content")
|
sobject (sto/put-object! storage {::sto/content (sto/content "content")
|
||||||
:content-type "text/plain"
|
:content-type "text/plain"
|
||||||
:other "data"})
|
:other "data"})
|
||||||
profile (th/create-profile* 1 {:is-active true})
|
profile (th/create-profile* 1 {:is-active true})
|
||||||
|
|
||||||
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||||
@ -120,7 +120,7 @@
|
|||||||
:media-id (:id sobject)})]
|
:media-id (:id sobject)})]
|
||||||
|
|
||||||
(th/mark-file-deleted* {:id (:id file2)})
|
(th/mark-file-deleted* {:id (:id file2)})
|
||||||
@(sto/del-object! storage sobject)
|
(sto/del-object! storage sobject)
|
||||||
|
|
||||||
(let [data {::th/type :duplicate-file
|
(let [data {::th/type :duplicate-file
|
||||||
::rpc/profile-id (:id profile)
|
::rpc/profile-id (:id profile)
|
||||||
@ -157,9 +157,9 @@
|
|||||||
(let [storage (-> (:app.storage/storage th/*system*)
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
(configure-storage-backend))
|
(configure-storage-backend))
|
||||||
|
|
||||||
sobject @(sto/put-object! storage {::sto/content (sto/content "content")
|
sobject (sto/put-object! storage {::sto/content (sto/content "content")
|
||||||
:content-type "text/plain"
|
:content-type "text/plain"
|
||||||
:other "data"})
|
:other "data"})
|
||||||
|
|
||||||
profile (th/create-profile* 1 {:is-active true})
|
profile (th/create-profile* 1 {:is-active true})
|
||||||
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||||
@ -230,9 +230,9 @@
|
|||||||
(t/deftest duplicate-project-with-deleted-files
|
(t/deftest duplicate-project-with-deleted-files
|
||||||
(let [storage (-> (:app.storage/storage th/*system*)
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
(configure-storage-backend))
|
(configure-storage-backend))
|
||||||
sobject @(sto/put-object! storage {::sto/content (sto/content "content")
|
sobject (sto/put-object! storage {::sto/content (sto/content "content")
|
||||||
:content-type "text/plain"
|
:content-type "text/plain"
|
||||||
:other "data"})
|
:other "data"})
|
||||||
profile (th/create-profile* 1 {:is-active true})
|
profile (th/create-profile* 1 {:is-active true})
|
||||||
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
project (th/create-project* 1 {:team-id (:default-team-id profile)
|
||||||
:profile-id (:id profile)})
|
:profile-id (:id profile)})
|
||||||
|
|||||||
@ -42,8 +42,8 @@
|
|||||||
(t/is (uuid? media-id))
|
(t/is (uuid? media-id))
|
||||||
(t/is (uuid? thumbnail-id))
|
(t/is (uuid? thumbnail-id))
|
||||||
(let [storage (:app.storage/storage th/*system*)
|
(let [storage (:app.storage/storage th/*system*)
|
||||||
mobj1 @(sto/get-object storage media-id)
|
mobj1 (sto/get-object storage media-id)
|
||||||
mobj2 @(sto/get-object storage thumbnail-id)]
|
mobj2 (sto/get-object storage thumbnail-id)]
|
||||||
(t/is (sto/object? mobj1))
|
(t/is (sto/object? mobj1))
|
||||||
(t/is (sto/object? mobj2))
|
(t/is (sto/object? mobj2))
|
||||||
(t/is (= 122785 (:size mobj1)))
|
(t/is (= 122785 (:size mobj1)))
|
||||||
@ -83,8 +83,8 @@
|
|||||||
(t/is (uuid? media-id))
|
(t/is (uuid? media-id))
|
||||||
(t/is (uuid? thumbnail-id))
|
(t/is (uuid? thumbnail-id))
|
||||||
(let [storage (:app.storage/storage th/*system*)
|
(let [storage (:app.storage/storage th/*system*)
|
||||||
mobj1 @(sto/get-object storage media-id)
|
mobj1 (sto/get-object storage media-id)
|
||||||
mobj2 @(sto/get-object storage thumbnail-id)]
|
mobj2 (sto/get-object storage thumbnail-id)]
|
||||||
(t/is (sto/object? mobj1))
|
(t/is (sto/object? mobj1))
|
||||||
(t/is (sto/object? mobj2))
|
(t/is (sto/object? mobj2))
|
||||||
(t/is (= 312043 (:size mobj1)))
|
(t/is (= 312043 (:size mobj1)))
|
||||||
@ -162,8 +162,8 @@
|
|||||||
(t/is (uuid? media-id))
|
(t/is (uuid? media-id))
|
||||||
(t/is (uuid? thumbnail-id))
|
(t/is (uuid? thumbnail-id))
|
||||||
(let [storage (:app.storage/storage th/*system*)
|
(let [storage (:app.storage/storage th/*system*)
|
||||||
mobj1 @(sto/get-object storage media-id)
|
mobj1 (sto/get-object storage media-id)
|
||||||
mobj2 @(sto/get-object storage thumbnail-id)]
|
mobj2 (sto/get-object storage thumbnail-id)]
|
||||||
(t/is (sto/object? mobj1))
|
(t/is (sto/object? mobj1))
|
||||||
(t/is (sto/object? mobj2))
|
(t/is (sto/object? mobj2))
|
||||||
(t/is (= 122785 (:size mobj1)))
|
(t/is (= 122785 (:size mobj1)))
|
||||||
@ -203,8 +203,8 @@
|
|||||||
(t/is (uuid? media-id))
|
(t/is (uuid? media-id))
|
||||||
(t/is (uuid? thumbnail-id))
|
(t/is (uuid? thumbnail-id))
|
||||||
(let [storage (:app.storage/storage th/*system*)
|
(let [storage (:app.storage/storage th/*system*)
|
||||||
mobj1 @(sto/get-object storage media-id)
|
mobj1 (sto/get-object storage media-id)
|
||||||
mobj2 @(sto/get-object storage thumbnail-id)]
|
mobj2 (sto/get-object storage thumbnail-id)]
|
||||||
(t/is (sto/object? mobj1))
|
(t/is (sto/object? mobj1))
|
||||||
(t/is (sto/object? mobj2))
|
(t/is (sto/object? mobj2))
|
||||||
(t/is (= 312043 (:size mobj1)))
|
(t/is (= 312043 (:size mobj1)))
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(ns backend-tests.rpc-team-test
|
(ns backend-tests.rpc-team-test
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.logging :as l]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http :as http]
|
[app.http :as http]
|
||||||
|
|||||||
@ -37,61 +37,61 @@
|
|||||||
(let [storage (-> (:app.storage/storage th/*system*)
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
(configure-storage-backend))
|
(configure-storage-backend))
|
||||||
content (sto/content "content")
|
content (sto/content "content")
|
||||||
object @(sto/put-object! storage {::sto/content content
|
object (sto/put-object! storage {::sto/content content
|
||||||
:content-type "text/plain"
|
:content-type "text/plain"
|
||||||
:other "data"})]
|
:other "data"})]
|
||||||
|
|
||||||
(t/is (sto/object? object))
|
(t/is (sto/object? object))
|
||||||
(t/is (fs/path? @(sto/get-object-path storage object)))
|
(t/is (fs/path? (sto/get-object-path storage object)))
|
||||||
|
|
||||||
(t/is (nil? (:expired-at object)))
|
(t/is (nil? (:expired-at object)))
|
||||||
(t/is (= :assets-fs (:backend object)))
|
(t/is (= :assets-fs (:backend object)))
|
||||||
(t/is (= "data" (:other (meta object))))
|
(t/is (= "data" (:other (meta object))))
|
||||||
(t/is (= "text/plain" (:content-type (meta object))))
|
(t/is (= "text/plain" (:content-type (meta object))))
|
||||||
(t/is (= "content" (slurp @(sto/get-object-data storage object))))
|
(t/is (= "content" (slurp (sto/get-object-data storage object))))
|
||||||
(t/is (= "content" (slurp @(sto/get-object-path storage object))))
|
(t/is (= "content" (slurp (sto/get-object-path storage object))))
|
||||||
))
|
))
|
||||||
|
|
||||||
(t/deftest put-and-retrieve-expired-object
|
(t/deftest put-and-retrieve-expired-object
|
||||||
(let [storage (-> (:app.storage/storage th/*system*)
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
(configure-storage-backend))
|
(configure-storage-backend))
|
||||||
content (sto/content "content")
|
content (sto/content "content")
|
||||||
object @(sto/put-object! storage {::sto/content content
|
object (sto/put-object! storage {::sto/content content
|
||||||
::sto/expired-at (dt/in-future {:seconds 1})
|
::sto/expired-at (dt/in-future {:seconds 1})
|
||||||
:content-type "text/plain"
|
:content-type "text/plain"
|
||||||
})]
|
})]
|
||||||
|
|
||||||
(t/is (sto/object? object))
|
(t/is (sto/object? object))
|
||||||
(t/is (dt/instant? (:expired-at object)))
|
(t/is (dt/instant? (:expired-at object)))
|
||||||
(t/is (dt/is-after? (:expired-at object) (dt/now)))
|
(t/is (dt/is-after? (:expired-at object) (dt/now)))
|
||||||
(t/is (= object @(sto/get-object storage (:id object))))
|
(t/is (= object (sto/get-object storage (:id object))))
|
||||||
|
|
||||||
(th/sleep 1000)
|
(th/sleep 1000)
|
||||||
(t/is (nil? @(sto/get-object storage (:id object))))
|
(t/is (nil? (sto/get-object storage (:id object))))
|
||||||
(t/is (nil? @(sto/get-object-data storage object)))
|
(t/is (nil? (sto/get-object-data storage object)))
|
||||||
(t/is (nil? @(sto/get-object-url storage object)))
|
(t/is (nil? (sto/get-object-url storage object)))
|
||||||
(t/is (nil? @(sto/get-object-path storage object)))
|
(t/is (nil? (sto/get-object-path storage object)))
|
||||||
))
|
))
|
||||||
|
|
||||||
(t/deftest put-and-delete-object
|
(t/deftest put-and-delete-object
|
||||||
(let [storage (-> (:app.storage/storage th/*system*)
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
(configure-storage-backend))
|
(configure-storage-backend))
|
||||||
content (sto/content "content")
|
content (sto/content "content")
|
||||||
object @(sto/put-object! storage {::sto/content content
|
object (sto/put-object! storage {::sto/content content
|
||||||
:content-type "text/plain"
|
:content-type "text/plain"
|
||||||
:expired-at (dt/in-future {:seconds 1})})]
|
:expired-at (dt/in-future {:seconds 1})})]
|
||||||
(t/is (sto/object? object))
|
(t/is (sto/object? object))
|
||||||
(t/is (true? @(sto/del-object! storage object)))
|
(t/is (true? (sto/del-object! storage object)))
|
||||||
|
|
||||||
;; retrieving the same object should be not nil because the
|
;; retrieving the same object should be not nil because the
|
||||||
;; deletion is not immediate
|
;; deletion is not immediate
|
||||||
(t/is (some? @(sto/get-object-data storage object)))
|
(t/is (some? (sto/get-object-data storage object)))
|
||||||
(t/is (some? @(sto/get-object-url storage object)))
|
(t/is (some? (sto/get-object-url storage object)))
|
||||||
(t/is (some? @(sto/get-object-path storage object)))
|
(t/is (some? (sto/get-object-path storage object)))
|
||||||
|
|
||||||
;; But you can't retrieve the object again because in database is
|
;; But you can't retrieve the object again because in database is
|
||||||
;; marked as deleted/expired.
|
;; marked as deleted/expired.
|
||||||
(t/is (nil? @(sto/get-object storage (:id object))))
|
(t/is (nil? (sto/get-object storage (:id object))))
|
||||||
))
|
))
|
||||||
|
|
||||||
(t/deftest test-deleted-gc-task
|
(t/deftest test-deleted-gc-task
|
||||||
@ -99,14 +99,14 @@
|
|||||||
(configure-storage-backend))
|
(configure-storage-backend))
|
||||||
content1 (sto/content "content1")
|
content1 (sto/content "content1")
|
||||||
content2 (sto/content "content2")
|
content2 (sto/content "content2")
|
||||||
object1 @(sto/put-object! storage {::sto/content content1
|
object1 (sto/put-object! storage {::sto/content content1
|
||||||
::sto/expired-at (dt/now)
|
::sto/expired-at (dt/now)
|
||||||
:content-type "text/plain"
|
:content-type "text/plain"
|
||||||
})
|
})
|
||||||
object2 @(sto/put-object! storage {::sto/content content2
|
object2 (sto/put-object! storage {::sto/content content2
|
||||||
::sto/expired-at (dt/in-past {:hours 2})
|
::sto/expired-at (dt/in-past {:hours 2})
|
||||||
:content-type "text/plain"
|
:content-type "text/plain"
|
||||||
})]
|
})]
|
||||||
|
|
||||||
(th/sleep 200)
|
(th/sleep 200)
|
||||||
|
|
||||||
|
|||||||
@ -23,15 +23,19 @@
|
|||||||
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
|
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
|
||||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||||
|
|
||||||
funcool/promesa {:mvn/version "10.0.594"}
|
|
||||||
funcool/cuerdas {:mvn/version "2022.06.16-403"}
|
funcool/cuerdas {:mvn/version "2022.06.16-403"}
|
||||||
|
funcool/promesa
|
||||||
|
{:git/tag "11.0-alpha13"
|
||||||
|
:git/sha "f6cab38"
|
||||||
|
:git/url "https://github.com/funcool/promesa.git"}
|
||||||
|
|
||||||
lambdaisland/uri {:mvn/version "1.13.95"
|
lambdaisland/uri {:mvn/version "1.13.95"
|
||||||
:exclusions [org.clojure/data.json]}
|
:exclusions [org.clojure/data.json]}
|
||||||
|
|
||||||
frankiesardo/linked {:mvn/version "1.3.0"}
|
frankiesardo/linked {:mvn/version "1.3.0"}
|
||||||
|
|
||||||
funcool/datoteka {:mvn/version "3.0.66"}
|
funcool/datoteka {:mvn/version "3.0.66"
|
||||||
|
:exclusions [funcool/promesa]}
|
||||||
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
|
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
|
||||||
org.la4j/la4j {:mvn/version "0.6.0"}
|
org.la4j/la4j {:mvn/version "0.6.0"}
|
||||||
|
|
||||||
|
|||||||
@ -178,6 +178,7 @@
|
|||||||
(print-detail cause)
|
(print-detail cause)
|
||||||
(recur cause))))))
|
(recur cause))))))
|
||||||
]
|
]
|
||||||
|
|
||||||
(with-out-str
|
(with-out-str
|
||||||
(print-all cause)))))
|
(print-all cause)))))
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"A version parsing helper."
|
"A version parsing helper."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
@ -28,11 +29,6 @@
|
|||||||
(def conjv (fnil conj []))
|
(def conjv (fnil conj []))
|
||||||
(def conjs (fnil conj #{}))
|
(def conjs (fnil conj #{}))
|
||||||
|
|
||||||
(defn- raise
|
|
||||||
[err-str]
|
|
||||||
#?(:clj (throw (Exception. err-str))
|
|
||||||
:cljs (throw (js/Error. err-str))))
|
|
||||||
|
|
||||||
(defn- commit-change
|
(defn- commit-change
|
||||||
([file change]
|
([file change]
|
||||||
(commit-change file change nil))
|
(commit-change file change nil))
|
||||||
@ -104,7 +100,9 @@
|
|||||||
|
|
||||||
(defn setup-rect-selrect [{:keys [x y width height transform] :as obj}]
|
(defn setup-rect-selrect [{:keys [x y width height transform] :as obj}]
|
||||||
(when-not (d/num? x y width height)
|
(when-not (d/num? x y width height)
|
||||||
(raise "Coords not valid for object"))
|
(ex/raise :type :assertion
|
||||||
|
:code :invalid-condition
|
||||||
|
:hint "Coords not valid for object"))
|
||||||
|
|
||||||
(let [rect (gsh/make-rect x y width height)
|
(let [rect (gsh/make-rect x y width height)
|
||||||
center (gsh/center-rect rect)
|
center (gsh/center-rect rect)
|
||||||
@ -121,7 +119,9 @@
|
|||||||
[{:keys [content center transform transform-inverse] :as obj}]
|
[{:keys [content center transform transform-inverse] :as obj}]
|
||||||
|
|
||||||
(when (or (empty? content) (nil? center))
|
(when (or (empty? content) (nil? center))
|
||||||
(raise "Path not valid"))
|
(ex/raise :type :assertion
|
||||||
|
:code :invalid-condition
|
||||||
|
:hint "Path not valid"))
|
||||||
|
|
||||||
(let [transform (gmt/transform-in center transform)
|
(let [transform (gmt/transform-in center transform)
|
||||||
transform-inverse (gmt/transform-in center transform-inverse)
|
transform-inverse (gmt/transform-in center transform-inverse)
|
||||||
|
|||||||
@ -72,7 +72,7 @@
|
|||||||
(apply matrix params)))
|
(apply matrix params)))
|
||||||
|
|
||||||
(defn close?
|
(defn close?
|
||||||
[m1 m2]
|
[^Matrix m1 ^Matrix m2]
|
||||||
(and (mth/close? (.-a m1) (.-a m2))
|
(and (mth/close? (.-a m1) (.-a m2))
|
||||||
(mth/close? (.-b m1) (.-b m2))
|
(mth/close? (.-b m1) (.-b m2))
|
||||||
(mth/close? (.-c m1) (.-c m2))
|
(mth/close? (.-c m1) (.-c m2))
|
||||||
@ -80,7 +80,7 @@
|
|||||||
(mth/close? (.-e m1) (.-e m2))
|
(mth/close? (.-e m1) (.-e m2))
|
||||||
(mth/close? (.-f m1) (.-f m2))))
|
(mth/close? (.-f m1) (.-f m2))))
|
||||||
|
|
||||||
(defn unit? [m1]
|
(defn unit? [^Matrix m1]
|
||||||
(and (some? m1)
|
(and (some? m1)
|
||||||
(mth/close? (.-a m1) 1)
|
(mth/close? (.-a m1) 1)
|
||||||
(mth/close? (.-b m1) 0)
|
(mth/close? (.-b m1) 0)
|
||||||
|
|||||||
@ -380,7 +380,6 @@
|
|||||||
(defn rotate
|
(defn rotate
|
||||||
"Rotates the point around center with an angle"
|
"Rotates the point around center with an angle"
|
||||||
[p c angle]
|
[p c angle]
|
||||||
(prn "ROTATE" p c angle)
|
|
||||||
(assert (point? p) "point instance expected")
|
(assert (point? p) "point instance expected")
|
||||||
(assert (point? c) "point instance expected")
|
(assert (point? c) "point instance expected")
|
||||||
(let [angle (mth/radians angle)
|
(let [angle (mth/radians angle)
|
||||||
|
|||||||
@ -213,26 +213,28 @@
|
|||||||
`(when (enabled? ~logger ~level)
|
`(when (enabled? ~logger ~level)
|
||||||
(let [props# (cond-> (delay ~props) ~sync? deref)
|
(let [props# (cond-> (delay ~props) ~sync? deref)
|
||||||
ts# (current-timestamp)
|
ts# (current-timestamp)
|
||||||
context# *context*]
|
context# *context*
|
||||||
(px/run! *default-executor*
|
logfn# (fn []
|
||||||
(fn []
|
(let [props# (if ~sync? props# (deref props#))
|
||||||
(let [props# (if ~sync? props# (deref props#))
|
props# (into (d/ordered-map) props#)
|
||||||
props# (into (d/ordered-map) props#)
|
cause# ~cause
|
||||||
cause# ~cause
|
context# (d/without-nils
|
||||||
context# (d/without-nils
|
(merge context# ~context))
|
||||||
(merge context# ~context))
|
lrecord# {::id (uuid/next)
|
||||||
lrecord# {::id (uuid/next)
|
::timestamp ts#
|
||||||
::timestamp ts#
|
::message (delay (build-message props#))
|
||||||
::message (delay (build-message props#))
|
::props props#
|
||||||
::props props#
|
::context context#
|
||||||
::context context#
|
::level ~level
|
||||||
::level ~level
|
::logger ~logger}
|
||||||
::logger ~logger}
|
lrecord# (cond-> lrecord#
|
||||||
lrecord# (cond-> lrecord#
|
(some? cause#)
|
||||||
(some? cause#)
|
(assoc ::cause cause#
|
||||||
(assoc ::cause cause#
|
::trace (delay (build-stack-trace cause#))))]
|
||||||
::trace (delay (build-stack-trace cause#))))]
|
(swap! log-record (constantly lrecord#))))]
|
||||||
(swap! log-record (constantly lrecord#)))))))))
|
(if ~sync?
|
||||||
|
(logfn#)
|
||||||
|
(px/exec! *default-executor* logfn#))))))
|
||||||
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(defn slf4j-log-handler
|
(defn slf4j-log-handler
|
||||||
@ -243,12 +245,12 @@
|
|||||||
(some? trace)
|
(some? trace)
|
||||||
(str "\n" @trace))]
|
(str "\n" @trace))]
|
||||||
(case level
|
(case level
|
||||||
:trace (.trace ^Logger logger ^String message ^Throwable cause)
|
:trace (.trace ^Logger logger ^String message)
|
||||||
:debug (.debug ^Logger logger ^String message ^Throwable cause)
|
:debug (.debug ^Logger logger ^String message)
|
||||||
:info (.info ^Logger logger ^String message ^Throwable cause)
|
:info (.info ^Logger logger ^String message)
|
||||||
:warn (.warn ^Logger logger ^String message ^Throwable cause)
|
:warn (.warn ^Logger logger ^String message)
|
||||||
:error (.error ^Logger logger ^String message ^Throwable cause)
|
:error (.error ^Logger logger ^String message)
|
||||||
:fatal (.error ^Logger logger ^String message ^Throwable cause)
|
:fatal (.error ^Logger logger ^String message)
|
||||||
(throw (IllegalArgumentException. (str "invalid level:" level))))))))
|
(throw (IllegalArgumentException. (str "invalid level:" level))))))))
|
||||||
|
|
||||||
#?(:cljs
|
#?(:cljs
|
||||||
|
|||||||
@ -103,7 +103,7 @@
|
|||||||
|
|
||||||
(defn font-weight->name
|
(defn font-weight->name
|
||||||
[weight]
|
[weight]
|
||||||
(case weight
|
(case (long weight)
|
||||||
100 "Hairline"
|
100 "Hairline"
|
||||||
200 "Extra Light"
|
200 "Extra Light"
|
||||||
300 "Light"
|
300 "Light"
|
||||||
|
|||||||
@ -252,6 +252,9 @@
|
|||||||
#?(:clj
|
#?(:clj
|
||||||
(s/def ::agent #(instance? clojure.lang.Agent %)))
|
(s/def ::agent #(instance? clojure.lang.Agent %)))
|
||||||
|
|
||||||
|
#?(:clj
|
||||||
|
(s/def ::atom #(instance? clojure.lang.Atom %)))
|
||||||
|
|
||||||
(defn bytes?
|
(defn bytes?
|
||||||
"Test if a first parameter is a byte
|
"Test if a first parameter is a byte
|
||||||
array or not."
|
array or not."
|
||||||
|
|||||||
@ -157,7 +157,7 @@
|
|||||||
(defn- code-points->text
|
(defn- code-points->text
|
||||||
[cpoints start end]
|
[cpoints start end]
|
||||||
#?(:cljs (apply str (subvec cpoints start end))
|
#?(:cljs (apply str (subvec cpoints start end))
|
||||||
:clj (let [sb (StringBuilder. (- end start))]
|
:clj (let [sb (StringBuilder. (- ^long end ^long start))]
|
||||||
(run! #(.appendCodePoint sb (int %)) (subvec cpoints start end))
|
(run! #(.appendCodePoint sb (int %)) (subvec cpoints start end))
|
||||||
(.toString sb))))
|
(.toString sb))))
|
||||||
|
|
||||||
|
|||||||
@ -17,43 +17,43 @@
|
|||||||
[app.common.types.page :as ctp]
|
[app.common.types.page :as ctp]
|
||||||
[app.common.types.file :as ctf]))
|
[app.common.types.file :as ctf]))
|
||||||
|
|
||||||
(defspec transit-encode-decode-with-shape 30
|
(defspec transit-encode-decode-with-shape 10
|
||||||
(props/for-all
|
(props/for-all
|
||||||
[fdata (s/gen ::cts/shape)]
|
[fdata (s/gen ::cts/shape)]
|
||||||
(let [res (-> fdata transit/encode-str transit/decode-str)]
|
(let [res (-> fdata transit/encode-str transit/decode-str)]
|
||||||
(t/is (= res fdata)))))
|
(t/is (= res fdata)))))
|
||||||
|
|
||||||
(defspec types-shape-spec 10
|
(defspec types-shape-spec 5
|
||||||
(props/for-all
|
(props/for-all
|
||||||
[fdata (s/gen ::cts/shape)]
|
[fdata (s/gen ::cts/shape)]
|
||||||
(t/is (us/valid? ::cts/shape fdata))))
|
(t/is (us/valid? ::cts/shape fdata))))
|
||||||
|
|
||||||
(defspec types-page-spec 10
|
(defspec types-page-spec 5
|
||||||
(props/for-all
|
(props/for-all
|
||||||
[fdata (s/gen ::ctp/page)]
|
[fdata (s/gen ::ctp/page)]
|
||||||
(t/is (us/valid? ::ctp/page fdata))))
|
(t/is (us/valid? ::ctp/page fdata))))
|
||||||
|
|
||||||
(defspec types-file-colors-spec 30
|
(defspec types-file-colors-spec 10
|
||||||
(props/for-all
|
(props/for-all
|
||||||
[fdata (s/gen ::ctf/colors)]
|
[fdata (s/gen ::ctf/colors)]
|
||||||
(t/is (us/valid? ::ctf/colors fdata))))
|
(t/is (us/valid? ::ctf/colors fdata))))
|
||||||
|
|
||||||
(defspec types-file-recent-colors-spec 30
|
(defspec types-file-recent-colors-spec 10
|
||||||
(props/for-all
|
(props/for-all
|
||||||
[fdata (s/gen ::ctf/recent-colors)]
|
[fdata (s/gen ::ctf/recent-colors)]
|
||||||
(t/is (us/valid? ::ctf/recent-colors fdata))))
|
(t/is (us/valid? ::ctf/recent-colors fdata))))
|
||||||
|
|
||||||
(defspec types-file-typographies-spec 30
|
(defspec types-file-typographies-spec 10
|
||||||
(props/for-all
|
(props/for-all
|
||||||
[fdata (s/gen ::ctf/typographies)]
|
[fdata (s/gen ::ctf/typographies)]
|
||||||
(t/is (us/valid? ::ctf/typographies fdata))))
|
(t/is (us/valid? ::ctf/typographies fdata))))
|
||||||
|
|
||||||
(defspec types-file-media-spec 30
|
(defspec types-file-media-spec 10
|
||||||
(props/for-all
|
(props/for-all
|
||||||
[fdata (s/gen ::ctf/media)]
|
[fdata (s/gen ::ctf/media)]
|
||||||
(t/is (us/valid? ::ctf/media fdata))))
|
(t/is (us/valid? ::ctf/media fdata))))
|
||||||
|
|
||||||
(defspec types-file-components-spec 10
|
(defspec types-file-components-spec 1
|
||||||
(props/for-all
|
(props/for-all
|
||||||
[fdata (s/gen ::ctf/components)]
|
[fdata (s/gen ::ctf/components)]
|
||||||
(t/is (us/valid? ::ctf/components fdata))))
|
(t/is (us/valid? ::ctf/components fdata))))
|
||||||
|
|||||||
@ -14,10 +14,8 @@
|
|||||||
[clojure.test.check.generators :as gen]
|
[clojure.test.check.generators :as gen]
|
||||||
[clojure.test.check.properties :as props]))
|
[clojure.test.check.properties :as props]))
|
||||||
|
|
||||||
(defspec non-repeating-uuid-next-1 5000
|
(defspec non-repeating-uuid-next-1 100
|
||||||
(props/for-all
|
(props/for-all
|
||||||
[uuid1 (s/gen ::us/uuid)
|
[uuid1 (s/gen ::us/uuid)
|
||||||
uuid2 (s/gen ::us/uuid)]
|
uuid2 (s/gen ::us/uuid)]
|
||||||
(t/is (not= uuid1 uuid2))))
|
(t/is (not= uuid1 uuid2))))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
listen_addresses = '*'
|
listen_addresses = '*'
|
||||||
max_connections = 100
|
max_connections = 50
|
||||||
shared_buffers = 128MB
|
shared_buffers = 256MB
|
||||||
temp_buffers = 8MB
|
temp_buffers = 18MB
|
||||||
work_mem = 8MB
|
work_mem = 18MB
|
||||||
|
|
||||||
dynamic_shared_memory_type = posix
|
dynamic_shared_memory_type = posix
|
||||||
synchronous_commit = off
|
synchronous_commit = off
|
||||||
wal_writer_delay = 900ms
|
wal_writer_delay = 900ms
|
||||||
max_wal_size = 1GB
|
max_wal_size = 1GB
|
||||||
min_wal_size = 80MB
|
min_wal_size = 80MB
|
||||||
|
full_page_writes = off
|
||||||
|
min_wal_size=1GB
|
||||||
|
max_wal_size=4GB
|
||||||
|
|
||||||
# log_min_duration_statement = 0
|
# log_min_duration_statement = 0
|
||||||
log_timezone = 'Europe/Madrid'
|
log_timezone = 'Europe/Madrid'
|
||||||
@ -19,4 +22,3 @@ lc_monetary = 'en_US.utf8'
|
|||||||
lc_numeric = 'en_US.utf8'
|
lc_numeric = 'en_US.utf8'
|
||||||
lc_time = 'en_US.utf8'
|
lc_time = 'en_US.utf8'
|
||||||
default_text_search_config = 'pg_catalog.english'
|
default_text_search_config = 'pg_catalog.english'
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
(derive :get-font-variants ::query)
|
(derive :get-font-variants ::query)
|
||||||
(derive :get-profile ::query)
|
(derive :get-profile ::query)
|
||||||
(derive :get-project ::query)
|
(derive :get-project ::query)
|
||||||
|
(derive :get-projects ::query)
|
||||||
(derive :get-team-invitations ::query)
|
(derive :get-team-invitations ::query)
|
||||||
(derive :get-team-members ::query)
|
(derive :get-team-members ::query)
|
||||||
(derive :get-team-shared-files ::query)
|
(derive :get-team-shared-files ::query)
|
||||||
@ -29,6 +30,9 @@
|
|||||||
(derive :get-teams ::query)
|
(derive :get-teams ::query)
|
||||||
(derive :get-view-only-bundle ::query)
|
(derive :get-view-only-bundle ::query)
|
||||||
(derive :search-files ::query)
|
(derive :search-files ::query)
|
||||||
|
(derive :retrieve-list-of-builtin-templates ::query)
|
||||||
|
(derive :get-unread-comment-threads ::query)
|
||||||
|
(derive :get-team-recent-files ::query)
|
||||||
|
|
||||||
(defn handle-response
|
(defn handle-response
|
||||||
[{:keys [status body] :as response}]
|
[{:keys [status body] :as response}]
|
||||||
|
|||||||
@ -46,9 +46,10 @@
|
|||||||
(defonce state
|
(defonce state
|
||||||
(ptk/store {:resolve ptk/resolve
|
(ptk/store {:resolve ptk/resolve
|
||||||
:on-event on-event
|
:on-event on-event
|
||||||
:on-error (fn [e]
|
:on-error (fn [cause]
|
||||||
(.log js/console "ERROR!!" e)
|
(when cause
|
||||||
(@on-error e))}))
|
(log/error :hint "unexpected exception on store" :cause cause)
|
||||||
|
(@on-error cause)))}))
|
||||||
|
|
||||||
(defonce stream
|
(defonce stream
|
||||||
(ptk/input-stream state))
|
(ptk/input-stream state))
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
(ns app.main.ui.auth.login
|
(ns app.main.ui.auth.login
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.logging :as log]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.data.messages :as dm]
|
[app.main.data.messages :as dm]
|
||||||
@ -38,7 +39,10 @@
|
|||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(->> (rp/command! :login-with-oidc (assoc params :provider provider))
|
(->> (rp/command! :login-with-oidc (assoc params :provider provider))
|
||||||
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
|
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
|
||||||
(.replace js/location redirect-uri))
|
(if redirect-uri
|
||||||
|
(.replace js/location redirect-uri)
|
||||||
|
(log/error :hint "unexpected response from OIDC method"
|
||||||
|
:resp (pr-str rsp))))
|
||||||
(fn [{:keys [type code] :as error}]
|
(fn [{:keys [type code] :as error}]
|
||||||
(cond
|
(cond
|
||||||
(and (= type :restriction)
|
(and (= type :restriction)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user