mirror of
https://github.com/penpot/penpot.git
synced 2025-07-09 18:37:20 +02:00
Merge branch 'staging' into main
This commit is contained in:
commit
a6d156438f
468 changed files with 16810 additions and 8136 deletions
|
@ -9,7 +9,7 @@ jobs:
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||||
# - image: circleci/postgres:9.4
|
# - image: circleci/postgres:9.4
|
||||||
- image: circleci/postgres:13.1-ram
|
- image: circleci/postgres:13.3-ram
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: penpot_test
|
POSTGRES_USER: penpot_test
|
||||||
POSTGRES_PASSWORD: penpot_test
|
POSTGRES_PASSWORD: penpot_test
|
||||||
|
@ -29,21 +29,30 @@ jobs:
|
||||||
# Download and cache dependencies
|
# Download and cache dependencies
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}
|
- v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
|
||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
|
|
||||||
# run lint
|
|
||||||
- run:
|
- run:
|
||||||
working_directory: "./backend"
|
name: common lint
|
||||||
name: backend lint
|
working_directory: "./common"
|
||||||
command: "clj-kondo --lint src/"
|
command: "clj-kondo --parallel --lint src/"
|
||||||
|
|
||||||
# run test
|
|
||||||
- run:
|
- run:
|
||||||
|
name: frontend lint
|
||||||
|
working_directory: "./frontend"
|
||||||
|
command: "clj-kondo --parallel --lint src/"
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: backend lint
|
||||||
working_directory: "./backend"
|
working_directory: "./backend"
|
||||||
|
command: "clj-kondo --parallel --lint src/"
|
||||||
|
|
||||||
|
# run backend test
|
||||||
|
- run:
|
||||||
name: backend test
|
name: backend test
|
||||||
command: "clojure -M:dev:tests"
|
working_directory: "./backend"
|
||||||
|
command: "clojure -X:dev:test"
|
||||||
environment:
|
environment:
|
||||||
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
|
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
|
||||||
PENPOT_TEST_DATABASE_USERNAME: penpot_test
|
PENPOT_TEST_DATABASE_USERNAME: penpot_test
|
||||||
|
@ -51,11 +60,26 @@ jobs:
|
||||||
PENPOT_TEST_REDIS_URI: "redis://localhost/1"
|
PENPOT_TEST_REDIS_URI: "redis://localhost/1"
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
working_directory: "./frontend"
|
|
||||||
name: frontend tests
|
name: frontend tests
|
||||||
|
working_directory: "./frontend"
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
npx shadow-cljs compile tests
|
clojure -M:dev:shadow-cljs compile test
|
||||||
|
node target/tests.js
|
||||||
|
|
||||||
|
environment:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/openjdk16
|
||||||
|
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin
|
||||||
|
|
||||||
|
- run:
|
||||||
|
working_directory: "./common"
|
||||||
|
name: common tests
|
||||||
|
command: |
|
||||||
|
yarn install
|
||||||
|
clojure -M:dev:shadow-cljs compile test
|
||||||
|
node target/tests.js
|
||||||
|
clojure -X:dev:test
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
JAVA_HOME: /usr/lib/jvm/openjdk16
|
JAVA_HOME: /usr/lib/jvm/openjdk16
|
||||||
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin
|
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin
|
||||||
|
@ -63,5 +87,5 @@ jobs:
|
||||||
- save_cache:
|
- save_cache:
|
||||||
paths:
|
paths:
|
||||||
- ~/.m2
|
- ~/.m2
|
||||||
key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}
|
key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,23 @@
|
||||||
{:lint-as {potok.core/reify clojure.core/reify
|
{:lint-as
|
||||||
promesa.core/let clojure.core/let
|
{promesa.core/let clojure.core/let
|
||||||
rumext.alpha/defc clojure.core/defn
|
rumext.alpha/defc clojure.core/defn
|
||||||
|
rumext.alpha/fnc clojure.core/fn
|
||||||
app.common.data/export clojure.core/def
|
app.common.data/export clojure.core/def
|
||||||
app.db/with-atomic clojure.core/with-open}
|
app.db/with-atomic clojure.core/with-open}
|
||||||
|
|
||||||
:hooks
|
:hooks
|
||||||
{:analyze-call {app.common.data/export hooks.export/export}}
|
{:analyze-call
|
||||||
|
{app.common.data/export hooks.export/export
|
||||||
|
potok.core/reify hooks.export/potok-reify
|
||||||
|
cljs.core/specify! hooks.export/clojure-specify
|
||||||
|
app.util.services/defmethod hooks.export/service-defmethod
|
||||||
|
}}
|
||||||
|
|
||||||
:output
|
:output
|
||||||
{:exclude-files ["data_readers.clj"]}
|
{:exclude-files
|
||||||
|
["data_readers.clj"
|
||||||
|
"app/util/perf.cljs"
|
||||||
|
"app/common/exceptions.cljc"]}
|
||||||
|
|
||||||
:linters
|
:linters
|
||||||
{:unsorted-required-namespaces
|
{:unsorted-required-namespaces
|
||||||
|
@ -21,12 +30,12 @@
|
||||||
:single-key-in
|
:single-key-in
|
||||||
{:level :warning}
|
{:level :warning}
|
||||||
|
|
||||||
|
:redundant-do
|
||||||
|
{:level :off}
|
||||||
|
|
||||||
:unused-binding
|
:unused-binding
|
||||||
{:exclude-destructured-as true
|
{:exclude-destructured-as true
|
||||||
:exclude-destructured-keys-in-fn-args false
|
:exclude-destructured-keys-in-fn-args false
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
:unresolved-symbol
|
|
||||||
{:exclude ['(app.util.services/defmethod)
|
|
||||||
]}}}
|
|
||||||
|
|
||||||
|
|
|
@ -9,3 +9,41 @@
|
||||||
(api/token-node (symbol (name (:value sname))))
|
(api/token-node (symbol (name (:value sname))))
|
||||||
sname])]
|
sname])]
|
||||||
{:node result}))
|
{:node result}))
|
||||||
|
|
||||||
|
(defn potok-reify
|
||||||
|
[{:keys [:node]}]
|
||||||
|
(let [[rnode rtype & other] (:children node)
|
||||||
|
result (api/list-node
|
||||||
|
(into [(api/token-node (symbol "deftype"))
|
||||||
|
(api/token-node (gensym (name (:k rtype))))
|
||||||
|
(api/vector-node [])]
|
||||||
|
other))]
|
||||||
|
{:node result}))
|
||||||
|
|
||||||
|
(defn clojure-specify
|
||||||
|
[{:keys [:node]}]
|
||||||
|
(let [[rnode rtype & other] (:children node)
|
||||||
|
result (api/list-node
|
||||||
|
(into [(api/token-node (symbol "extend-type"))
|
||||||
|
(api/token-node (gensym (:string-value rtype)))]
|
||||||
|
other))]
|
||||||
|
{:node result}))
|
||||||
|
|
||||||
|
|
||||||
|
(defn service-defmethod
|
||||||
|
[{:keys [:node]}]
|
||||||
|
(let [[rnode rtype & other] (:children node)
|
||||||
|
rsym (gensym (name (:k rtype)))
|
||||||
|
result (api/list-node
|
||||||
|
[(api/token-node (symbol "do"))
|
||||||
|
(api/list-node
|
||||||
|
[(api/token-node (symbol "declare"))
|
||||||
|
(api/token-node rsym)])
|
||||||
|
(api/list-node
|
||||||
|
(into [(api/token-node (symbol "defmethod"))
|
||||||
|
(api/token-node rsym)
|
||||||
|
rtype]
|
||||||
|
other))])]
|
||||||
|
{:node result}))
|
||||||
|
|
||||||
|
|
||||||
|
|
40
CHANGES.md
40
CHANGES.md
|
@ -5,6 +5,46 @@
|
||||||
### :sparkles: New features
|
### :sparkles: New features
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
### :arrow_up: Deps updates
|
||||||
|
### :boom: Breaking changes
|
||||||
|
### :heart: Community contributions by (Thank you!)
|
||||||
|
|
||||||
|
## 1.7.0-alpha
|
||||||
|
|
||||||
|
### :sparkles: New features
|
||||||
|
|
||||||
|
- Allow nested asset groups [Taiga #1716](https://tree.taiga.io/project/penpot/us/1716).
|
||||||
|
- Allow to ungroup assets [Taiga #1719](https://tree.taiga.io/project/penpot/us/1719).
|
||||||
|
- Allow to rename assets groups [Taiga #1721](https://tree.taiga.io/project/penpot/us/1721).
|
||||||
|
- Component constraints (left, right, left and right, center, scale...) [Taiga #1125](https://tree.taiga.io/project/penpot/us/1125).
|
||||||
|
- Export elements to PDF [Taiga #519](https://tree.taiga.io/project/penpot/us/519).
|
||||||
|
- Memorize collapse state of assets in panel [Taiga #1718](https://tree.taiga.io/project/penpot/us/1718).
|
||||||
|
- Headers button sets and menus review [Taiga #1663](https://tree.taiga.io/project/penpot/us/1663).
|
||||||
|
- Preserve components if possible, when pasted into a different file [Taiga #1063](https://tree.taiga.io/project/penpot/issue/1063).
|
||||||
|
- Add the ability to offload file data to a cheaper storage when file becomes inactive.
|
||||||
|
- Import/Export Penpot files from dashboard.
|
||||||
|
- Double click won't make a shape a path until you change a node [Taiga #1796](https://tree.taiga.io/project/penpot/us/1796)
|
||||||
|
- Incremental area selection [#779](https://github.com/penpot/penpot/discussions/779)
|
||||||
|
|
||||||
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
- Process numeric input changes only if the value actually changed.
|
||||||
|
- Remove unnecesary redirect from history when user goes to workspace from dashboard [Taiga #1820](https://tree.taiga.io/project/penpot/issue/1820).
|
||||||
|
- Detach shapes from deleted assets [Taiga #1850](https://tree.taiga.io/project/penpot/issue/1850).
|
||||||
|
- Fix tooltip position on view application [Taiga #1819](https://tree.taiga.io/project/penpot/issue/1819).
|
||||||
|
- Fix dashboard navigation on moving file to other team [Taiga #1817](https://tree.taiga.io/project/penpot/issue/1817).
|
||||||
|
- Fix workspace header presence styles and invalid link [Taiga #1813](https://tree.taiga.io/project/penpot/issue/1813).
|
||||||
|
- Fix color-input wrong behavior (on workspace page color) [Taiga #1795](https://tree.taiga.io/project/penpot/issue/1795).
|
||||||
|
- Fix file contextual menu in shared libraries at dashboard [Taiga #1865](https://tree.taiga.io/project/penpot/issue/1865).
|
||||||
|
- Fix problem with color picker and fonts [#1049](https://github.com/penpot/penpot/issues/1049)
|
||||||
|
- Fix negative values in blur [Taiga #1815](https://tree.taiga.io/project/penpot/issue/1815)
|
||||||
|
- Fix problem when editing color in group [Taiga #1816](https://tree.taiga.io/project/penpot/issue/1816)
|
||||||
|
- Fix resize/rotate with mouse buttons different than left [#1060](https://github.com/penpot/penpot/issues/1060)
|
||||||
|
- Fix header partialy visible on fullscreen viewer mode [Taiga #1875](https://tree.taiga.io/project/penpot/issue/1875)
|
||||||
|
- Fix dynamic alignment enabled with hidden objects [#1063](https://github.com/penpot/penpot/issues/1063)
|
||||||
|
|
||||||
|
|
||||||
### :arrow_up: Deps updates
|
### :arrow_up: Deps updates
|
||||||
### :boom: Breaking changes
|
### :boom: Breaking changes
|
||||||
### :heart: Community contributions by (Thank you!)
|
### :heart: Community contributions by (Thank you!)
|
||||||
|
|
73
README.md
73
README.md
|
@ -2,24 +2,60 @@
|
||||||
[uri_license]: https://www.mozilla.org/en-US/MPL/2.0
|
[uri_license]: https://www.mozilla.org/en-US/MPL/2.0
|
||||||
[uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg
|
[uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg
|
||||||
|
|
||||||
[![License: MPL-2.0][uri_license_image]][uri_license]
|
<h1 align="center">
|
||||||
[](https://gitter.im/penpot/community)
|
<br>
|
||||||
[](https://tree.taiga.io/project/penpot/ "Managed with Taiga.io")
|
<img src="https://penpot.app/images/readme/readme-logo.jpg" alt="PENPOT">
|
||||||
[](https://gitpod.io/#https://github.com/penpot/penpot)
|
</h1>
|
||||||
|
|
||||||
|
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||||
|
<a href="https://gitter.im/penpot/community" rel="nofollow"><img src="https://camo.githubusercontent.com/5b0aecb33434f82a7b158eab7247544235ada0cf7eeb9ce8e52562dd67f614b7/68747470733a2f2f6261646765732e6769747465722e696d2f736572656e6f2d78797a2f636f6d6d756e6974792e737667" alt="Gitter" data-canonical-src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
|
||||||
|
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img src="https://camo.githubusercontent.com/4a1d1112f0272e3393b1e8da312ff4435418e9e2eb4c0964881e3680f90a653c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d616e61676564253230776974682d54414947412e696f2d3730396631342e737667" alt="Managed with Taiga.io" data-canonical-src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||||
|
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
# PENPOT #
|
## What is Penpot? ##
|
||||||
|
|
||||||
Penpot is the first Open Source design and prototyping platform meant
|
Penpot is the first **Open Source design** and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open web standards (SVG). For all and empowered by the community.
|
||||||
for cross-domain teams. Non dependent on operating systems, Penpot is
|
|
||||||
web based and works with open web standards (SVG). For all and
|
|
||||||
empowered by the community.
|
|
||||||
|
|
||||||

|
- [How to use](#how-to-use)
|
||||||
|
- [Help center](#help-center)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Give feedback](#give-feedback)
|
||||||
|
- [Tutorials](#tutorials)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## How to use ##
|
||||||
|
|
||||||
|
Login or Register on our Penpot cloud app. Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** by your own.
|
||||||
|
|
||||||
|
✏️ [Start using Penpot](https://design.penpot.app)
|
||||||
|
|
||||||
|
You can also install Penpot in a local environment. This section details everything you need to know to get Penpot up and running in production environments. Although it can be installed in many ways, the recommended approach is using **docker** and **docker-compose**.
|
||||||
|
|
||||||
|
🐳 [Install docker](https://help.penpot.app/technical-guide/getting-started/)
|
||||||
|
|
||||||
|
## Help center ##
|
||||||
|
|
||||||
|
In this documentation you will find (almost) everything you need to know about how to work with Penpot. From the interface basics to advanced functionality.
|
||||||
|
|
||||||
|
📖 [User guide](https://help.penpot.app/user-guide/)
|
||||||
|
|
||||||
|
❓ [FAQs](https://help.penpot.app/faqs/)
|
||||||
|
|
||||||
|
🖥️ [Technical guide](https://help.penpot.app/technical-guide/)
|
||||||
|
|
||||||
|
❤️ [Contributing guide](https://help.penpot.app/contributing-guide/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Contributing ##
|
## Contributing ##
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://penpot.app/images/open-source.png" alt="Open Source">
|
||||||
|
</p>
|
||||||
|
|
||||||
**Open to you!**
|
**Open to you!**
|
||||||
|
|
||||||
We love the open source software community. Contributing is our
|
We love the open source software community. Contributing is our
|
||||||
|
@ -28,11 +64,24 @@ and improve Penpot. All your awesome ideas and code are welcome!
|
||||||
|
|
||||||
Please refer to the [Contributing Guide](./CONTRIBUTING.md)
|
Please refer to the [Contributing Guide](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## Give feedback ##
|
||||||
|
|
||||||
## Documentation ##
|
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
||||||
|
|
||||||
Please refer to the [help center](https://help.penpot.app).
|
✉️ [Mail us](mailto:info@penpot.app)
|
||||||
|
|
||||||
|
💬 [Github discussions](https://github.com/penpot/penpot/discussions)
|
||||||
|
|
||||||
|
🐞 [Github issues](mailto:info@penpot.apphttps://github.com/penpot/penpot/issues)
|
||||||
|
|
||||||
|
✍️️ [Gitter](https://gitter.im/penpot/community)
|
||||||
|
|
||||||
|
## Tutorials ##
|
||||||
|
|
||||||
|
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
||||||
|
Would you like to know more about Penpot? We recommend you to visit our youtube channel and learn more about the functionalities and possibilities of Penpot with our video tutorials.
|
||||||
|
|
||||||
|
🎞️ [Youtube channel](https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g)
|
||||||
|
|
||||||
## License ##
|
## License ##
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,14 @@
|
||||||
{:mvn/repos
|
{
|
||||||
{"central" {:url "https://repo1.maven.org/maven2/"}
|
;; :mvn/repos
|
||||||
"clojars" {:url "https://clojars.org/repo"}
|
;; {"central" {:url "https://repo1.maven.org/maven2/"}
|
||||||
"jcenter" {:url "https://jcenter.bintray.com/"}}
|
;; "clojars" {:url "https://clojars.org/repo"}
|
||||||
|
;; "jcenter" {:url "https://jcenter.bintray.com/"}
|
||||||
|
;; }
|
||||||
:deps
|
:deps
|
||||||
{org.clojure/clojure {:mvn/version "1.10.3"}
|
{penpot/common
|
||||||
org.clojure/data.json {:mvn/version "2.2.3"}
|
{:local/root "../common"}
|
||||||
org.clojure/core.async {:mvn/version "1.3.618"}
|
|
||||||
org.clojure/tools.cli {:mvn/version "1.0.206"}
|
|
||||||
org.clojure/clojurescript {:mvn/version "1.10.844"}
|
|
||||||
|
|
||||||
;; Logging
|
;; Logging
|
||||||
org.clojure/tools.logging {:mvn/version "1.1.0"}
|
|
||||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.14.1"}
|
|
||||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.14.1"}
|
|
||||||
org.apache.logging.log4j/log4j-web {:mvn/version "2.14.1"}
|
|
||||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.14.1"}
|
|
||||||
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.14.1"}
|
|
||||||
org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"}
|
|
||||||
org.zeromq/jeromq {:mvn/version "0.5.2"}
|
org.zeromq/jeromq {:mvn/version "0.5.2"}
|
||||||
|
|
||||||
com.taoensso/nippy {:mvn/version "3.1.1"}
|
com.taoensso/nippy {:mvn/version "3.1.1"}
|
||||||
|
@ -32,69 +24,57 @@
|
||||||
org.eclipse.jetty/jetty-servlet]}
|
org.eclipse.jetty/jetty-servlet]}
|
||||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"}
|
io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"}
|
||||||
|
|
||||||
selmer/selmer {:mvn/version "1.12.40"}
|
|
||||||
expound/expound {:mvn/version "0.8.9"}
|
|
||||||
com.cognitect/transit-clj {:mvn/version "1.0.324"}
|
|
||||||
|
|
||||||
io.lettuce/lettuce-core {:mvn/version "6.1.2.RELEASE"}
|
io.lettuce/lettuce-core {:mvn/version "6.1.2.RELEASE"}
|
||||||
java-http-clj/java-http-clj {:mvn/version "0.4.2"}
|
java-http-clj/java-http-clj {:mvn/version "0.4.2"}
|
||||||
|
|
||||||
info.sunng/ring-jetty9-adapter {:mvn/version "0.15.1"}
|
info.sunng/ring-jetty9-adapter {:mvn/version "0.15.1"}
|
||||||
com.github.seancorfield/next.jdbc {:mvn/version "1.2.659"}
|
com.github.seancorfield/next.jdbc {:mvn/version "1.2.659"}
|
||||||
metosin/reitit-ring {:mvn/version "0.5.13"}
|
metosin/reitit-ring {:mvn/version "0.5.13"}
|
||||||
metosin/jsonista {:mvn/version "0.3.3"}
|
|
||||||
|
|
||||||
org.postgresql/postgresql {:mvn/version "42.2.20"}
|
org.postgresql/postgresql {:mvn/version "42.2.20"}
|
||||||
com.zaxxer/HikariCP {:mvn/version "4.0.3"}
|
com.zaxxer/HikariCP {:mvn/version "4.0.3"}
|
||||||
|
|
||||||
funcool/datoteka {:mvn/version "2.0.0"}
|
funcool/datoteka {:mvn/version "2.0.0"}
|
||||||
funcool/promesa {:mvn/version "6.0.1"}
|
|
||||||
funcool/cuerdas {:mvn/version "2021.05.09-0"}
|
|
||||||
|
|
||||||
buddy/buddy-core {:mvn/version "1.10.1"}
|
buddy/buddy-core {:mvn/version "1.10.1"}
|
||||||
buddy/buddy-hashers {:mvn/version "1.8.1"}
|
buddy/buddy-hashers {:mvn/version "1.8.1"}
|
||||||
buddy/buddy-sign {:mvn/version "3.4.1"}
|
buddy/buddy-sign {:mvn/version "3.4.1"}
|
||||||
|
|
||||||
lambdaisland/uri {:mvn/version "1.4.54"
|
|
||||||
:exclusions [org.clojure/data.json]}
|
|
||||||
|
|
||||||
frankiesardo/linked {:mvn/version "1.3.0"}
|
|
||||||
danlentz/clj-uuid {:mvn/version "0.1.9"}
|
|
||||||
org.jsoup/jsoup {:mvn/version "1.13.1"}
|
org.jsoup/jsoup {:mvn/version "1.13.1"}
|
||||||
org.im4java/im4java {:mvn/version "1.4.0"}
|
org.im4java/im4java {:mvn/version "1.4.0"}
|
||||||
org.lz4/lz4-java {:mvn/version "1.7.1"}
|
org.lz4/lz4-java {:mvn/version "1.7.1"}
|
||||||
commons-io/commons-io {:mvn/version "2.8.0"}
|
|
||||||
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
|
|
||||||
|
|
||||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||||
integrant/integrant {:mvn/version "0.8.0"}
|
integrant/integrant {:mvn/version "0.8.0"}
|
||||||
|
|
||||||
software.amazon.awssdk/s3 {:mvn/version "2.16.62"}
|
software.amazon.awssdk/s3 {:mvn/version "2.16.62"}}
|
||||||
|
|
||||||
;; exception printing
|
:paths ["src" "resources"]
|
||||||
io.aviso/pretty {:mvn/version "0.1.37"}
|
|
||||||
environ/environ {:mvn/version "1.2.0"}}
|
|
||||||
:paths ["src" "resources" "../common" "common"]
|
|
||||||
:aliases
|
:aliases
|
||||||
{:dev
|
{:dev
|
||||||
{:extra-deps
|
{:extra-deps
|
||||||
{com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
{com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||||
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||||
org.clojure/test.check {:mvn/version "RELEASE"}
|
org.clojure/test.check {:mvn/version "RELEASE"}
|
||||||
|
com.clojure-goes-fast/clj-async-profiler {:mvn/version "0.5.0"}
|
||||||
|
|
||||||
fipp/fipp {:mvn/version "0.6.23"}
|
criterium/criterium {:mvn/version "RELEASE"}
|
||||||
criterium/criterium {:mvn/version "0.4.6"}
|
mockery/mockery {:mvn/version "RELEASE"}}
|
||||||
mockery/mockery {:mvn/version "0.1.4"}}
|
:extra-paths ["test" "dev"]}
|
||||||
:extra-paths ["tests" "dev"]}
|
|
||||||
|
|
||||||
:fn-fixtures
|
:fn-fixtures
|
||||||
{:exec-fn app.cli.fixtures/run
|
{:exec-fn app.cli.fixtures/run
|
||||||
:args {}}
|
:args {}}
|
||||||
|
|
||||||
:tests
|
:kaocha
|
||||||
{:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.829"}}
|
{:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.829"}}
|
||||||
:main-opts ["-m" "kaocha.runner"]}
|
:main-opts ["-m" "kaocha.runner"]}
|
||||||
|
|
||||||
|
:test
|
||||||
|
{:extra-deps {io.github.cognitect-labs/test-runner
|
||||||
|
{:git/url "https://github.com/cognitect-labs/test-runner.git"
|
||||||
|
:sha "705ad25bbf0228b1c38d0244a36001c2987d7337"}}
|
||||||
|
:exec-fn cognitect.test-runner.api/test}
|
||||||
|
|
||||||
:outdated
|
:outdated
|
||||||
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
|
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
|
||||||
:main-opts ["-m" "antq.core"]}
|
:main-opts ["-m" "antq.core"]}
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
;; --- Development Stuff
|
;; --- Development Stuff
|
||||||
|
|
||||||
(defn- run-tests
|
(defn- run-tests
|
||||||
([] (run-tests #"^app.tests.*"))
|
([] (run-tests #"^app.*-test$"))
|
||||||
([o]
|
([o]
|
||||||
(repl/refresh)
|
(repl/refresh)
|
||||||
(cond
|
(cond
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
|
|
||||||
;; Create the application jar
|
;; Create the application jar
|
||||||
(spit "./target/dist/version.txt" version)
|
(spit "./target/dist/version.txt" version)
|
||||||
|
|
||||||
(-> ($ jar cvf "./target/dist/deps/app.jar" -C ~(first classpath-paths) ".") check)
|
(-> ($ jar cvf "./target/dist/deps/app.jar" -C ~(first classpath-paths) ".") check)
|
||||||
(-> ($ jar uvf "./target/dist/deps/app.jar" -C "./target/dist" "version.txt") check)
|
(-> ($ jar uvf "./target/dist/deps/app.jar" -C "./target/dist" "version.txt") check)
|
||||||
(run! (fn [item]
|
(run! (fn [item]
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
export PENPOT_ASSERTS_ENABLED=true
|
export PENPOT_ASSERTS_ENABLED=true
|
||||||
|
|
||||||
export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Dlog4j2.configurationFile=log4j2-devenv.xml -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m";
|
export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Dlog4j2.configurationFile=log4j2-devenv.xml -J-Djdk.attach.allowAttachSelf -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m";
|
||||||
# export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions";
|
# export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions";
|
||||||
# export OPTIONS="$OPTIONS -J-XX:-TieredCompilation -J-XX:CompileThreshold=10000";
|
# export OPTIONS="$OPTIONS -J-XX:-TieredCompilation -J-XX:CompileThreshold=10000";
|
||||||
|
|
||||||
|
|
|
@ -58,11 +58,8 @@
|
||||||
:srepl-host "127.0.0.1"
|
:srepl-host "127.0.0.1"
|
||||||
:srepl-port 6062
|
:srepl-port 6062
|
||||||
|
|
||||||
:storage-backend :fs
|
:assets-storage-backend :fs
|
||||||
|
:storage-assets-fs-directory "assets"
|
||||||
:storage-fs-directory "assets"
|
|
||||||
:storage-s3-region :eu-central-1
|
|
||||||
:storage-s3-bucket "penpot-devenv-assets-pre"
|
|
||||||
|
|
||||||
:feedback-destination "info@example.com"
|
:feedback-destination "info@example.com"
|
||||||
:feedback-enabled false
|
:feedback-enabled false
|
||||||
|
@ -175,10 +172,14 @@
|
||||||
(s/def ::smtp-username (s/nilable ::us/string))
|
(s/def ::smtp-username (s/nilable ::us/string))
|
||||||
(s/def ::srepl-host ::us/string)
|
(s/def ::srepl-host ::us/string)
|
||||||
(s/def ::srepl-port ::us/integer)
|
(s/def ::srepl-port ::us/integer)
|
||||||
(s/def ::storage-backend ::us/keyword)
|
(s/def ::assets-storage-backend ::us/keyword)
|
||||||
(s/def ::storage-fs-directory ::us/string)
|
(s/def ::fdata-storage-backend ::us/keyword)
|
||||||
(s/def ::storage-s3-bucket ::us/string)
|
(s/def ::storage-assets-fs-directory ::us/string)
|
||||||
(s/def ::storage-s3-region ::us/keyword)
|
(s/def ::storage-assets-s3-bucket ::us/string)
|
||||||
|
(s/def ::storage-assets-s3-region ::us/keyword)
|
||||||
|
(s/def ::storage-fdata-s3-bucket ::us/string)
|
||||||
|
(s/def ::storage-fdata-s3-region ::us/keyword)
|
||||||
|
(s/def ::storage-fdata-s3-prefix ::us/string)
|
||||||
(s/def ::telemetry-enabled ::us/boolean)
|
(s/def ::telemetry-enabled ::us/boolean)
|
||||||
(s/def ::telemetry-uri ::us/string)
|
(s/def ::telemetry-uri ::us/string)
|
||||||
(s/def ::telemetry-with-taiga ::us/boolean)
|
(s/def ::telemetry-with-taiga ::us/boolean)
|
||||||
|
@ -257,12 +258,20 @@
|
||||||
::smtp-ssl
|
::smtp-ssl
|
||||||
::smtp-tls
|
::smtp-tls
|
||||||
::smtp-username
|
::smtp-username
|
||||||
|
|
||||||
::srepl-host
|
::srepl-host
|
||||||
::srepl-port
|
::srepl-port
|
||||||
::storage-backend
|
|
||||||
::storage-fs-directory
|
::assets-storage-backend
|
||||||
::storage-s3-bucket
|
::storage-assets-fs-directory
|
||||||
::storage-s3-region
|
::storage-assets-s3-bucket
|
||||||
|
::storage-assets-s3-region
|
||||||
|
|
||||||
|
::fdata-storage-backend
|
||||||
|
::storage-fdata-s3-bucket
|
||||||
|
::storage-fdata-s3-region
|
||||||
|
::storage-fdata-s3-prefix
|
||||||
|
|
||||||
::telemetry-enabled
|
::telemetry-enabled
|
||||||
::telemetry-uri
|
::telemetry-uri
|
||||||
::telemetry-referer
|
::telemetry-referer
|
||||||
|
|
|
@ -10,13 +10,14 @@
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
|
[app.common.transit :as t]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[app.db.sql :as sql]
|
[app.db.sql :as sql]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.util.json :as json]
|
[app.util.json :as json]
|
||||||
[app.util.logging :as l]
|
[app.util.logging :as l]
|
||||||
[app.util.migrations :as mg]
|
[app.util.migrations :as mg]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.transit :as t]
|
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
|
@ -221,14 +222,20 @@
|
||||||
(sql/delete table params opts)
|
(sql/delete table params opts)
|
||||||
(assoc opts :return-keys true))))
|
(assoc opts :return-keys true))))
|
||||||
|
|
||||||
|
(defn- is-deleted?
|
||||||
|
[{:keys [deleted-at]}]
|
||||||
|
(and (dt/instant? deleted-at)
|
||||||
|
(< (inst-ms deleted-at)
|
||||||
|
(inst-ms (dt/now)))))
|
||||||
|
|
||||||
(defn get-by-params
|
(defn get-by-params
|
||||||
([ds table params]
|
([ds table params]
|
||||||
(get-by-params ds table params nil))
|
(get-by-params ds table params nil))
|
||||||
([ds table params {:keys [uncheked] :or {uncheked false} :as opts}]
|
([ds table params {:keys [uncheked] :or {uncheked false} :as opts}]
|
||||||
(let [res (exec-one! ds (sql/select table params opts))]
|
(let [res (exec-one! ds (sql/select table params opts))]
|
||||||
(when (and (not uncheked)
|
(when (and (not uncheked) (or (not res) (is-deleted? res)))
|
||||||
(or (:deleted-at res) (not res)))
|
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
|
:table table
|
||||||
:hint "database object not found"))
|
:hint "database object not found"))
|
||||||
res)))
|
res)))
|
||||||
|
|
||||||
|
@ -245,8 +252,11 @@
|
||||||
(exec! ds (sql/select table params opts))))
|
(exec! ds (sql/select table params opts))))
|
||||||
|
|
||||||
(defn pgobject?
|
(defn pgobject?
|
||||||
[v]
|
([v]
|
||||||
(instance? PGobject v))
|
(instance? PGobject v))
|
||||||
|
([v type]
|
||||||
|
(and (instance? PGobject v)
|
||||||
|
(= type (.getType ^PGobject v)))))
|
||||||
|
|
||||||
(defn pginterval?
|
(defn pginterval?
|
||||||
[v]
|
[v]
|
||||||
|
@ -339,12 +349,18 @@
|
||||||
(.setType "inet")
|
(.setType "inet")
|
||||||
(.setValue (str ip-addr))))
|
(.setValue (str ip-addr))))
|
||||||
|
|
||||||
|
(defn decode-inet
|
||||||
|
[^PGobject o]
|
||||||
|
(if (= "inet" (.getType o))
|
||||||
|
(.getValue o)
|
||||||
|
nil))
|
||||||
|
|
||||||
(defn tjson
|
(defn tjson
|
||||||
"Encode as transit json."
|
"Encode as transit json."
|
||||||
[data]
|
[data]
|
||||||
(doto (org.postgresql.util.PGobject.)
|
(doto (org.postgresql.util.PGobject.)
|
||||||
(.setType "jsonb")
|
(.setType "jsonb")
|
||||||
(.setValue (t/encode-verbose-str data))))
|
(.setValue (t/encode-str data {:type :json-verbose}))))
|
||||||
|
|
||||||
(defn json
|
(defn json
|
||||||
"Encode as plain json."
|
"Encode as plain json."
|
||||||
|
@ -360,3 +376,25 @@
|
||||||
(defn pgarray->vector
|
(defn pgarray->vector
|
||||||
[v]
|
[v]
|
||||||
(vec (.getArray ^PgArray v)))
|
(vec (.getArray ^PgArray v)))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- Locks
|
||||||
|
|
||||||
|
(defn- xact-check-param
|
||||||
|
[n]
|
||||||
|
(cond
|
||||||
|
(uuid? n) (uuid/get-word-high n)
|
||||||
|
(int? n) n
|
||||||
|
:else (throw (IllegalArgumentException. "uuid or number allowed"))))
|
||||||
|
|
||||||
|
(defn xact-lock!
|
||||||
|
[conn n]
|
||||||
|
(let [n (xact-check-param n)]
|
||||||
|
(exec-one! conn ["select pg_advisory_xact_lock(?::bigint) as lock" n])
|
||||||
|
true))
|
||||||
|
|
||||||
|
(defn xact-try-lock!
|
||||||
|
[conn n]
|
||||||
|
(let [n (xact-check-param n)
|
||||||
|
row (exec-one! conn ["select pg_try_advisory_xact_lock(?::bigint) as lock" n])]
|
||||||
|
(:lock row)))
|
||||||
|
|
|
@ -43,8 +43,8 @@
|
||||||
([table where-params opts]
|
([table where-params opts]
|
||||||
(let [opts (merge default-opts opts)
|
(let [opts (merge default-opts opts)
|
||||||
opts (cond-> opts
|
opts (cond-> opts
|
||||||
(:for-update opts)
|
(:for-update opts) (assoc :suffix "FOR UPDATE")
|
||||||
(assoc :suffix "FOR UPDATE"))]
|
(:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))]
|
||||||
(sql/for-query table where-params opts))))
|
(sql/for-query table where-params opts))))
|
||||||
|
|
||||||
(defn update
|
(defn update
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
{:status 200
|
{:status 200
|
||||||
:headers {"content-type" (:content-type mdata)
|
:headers {"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))}
|
||||||
:body (sto/get-object-data storage obj)}
|
:body (sto/get-object-bytes storage obj)}
|
||||||
|
|
||||||
:s3
|
:s3
|
||||||
(let [url (sto/get-object-url storage obj {:max-age signature-max-age})]
|
(let [url (sto/get-object-url storage obj {:max-age signature-max-age})]
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
|
|
||||||
(ns app.http.middleware
|
(ns app.http.middleware
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.transit :as t]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.util.json :as json]
|
[app.util.json :as json]
|
||||||
[app.util.logging :as l]
|
[app.util.logging :as l]
|
||||||
[app.util.transit :as t]
|
|
||||||
[buddy.core.codecs :as bc]
|
[buddy.core.codecs :as bc]
|
||||||
[buddy.core.hash :as bh]
|
[buddy.core.hash :as bh]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
|
[ring.core.protocols :as rp]
|
||||||
[ring.middleware.cookies :refer [wrap-cookies]]
|
[ring.middleware.cookies :refer [wrap-cookies]]
|
||||||
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
|
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
|
||||||
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
|
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
|
||||||
|
@ -73,17 +74,28 @@
|
||||||
{:name ::parse-request-body
|
{:name ::parse-request-body
|
||||||
:compile (constantly wrap-parse-request-body)})
|
:compile (constantly wrap-parse-request-body)})
|
||||||
|
|
||||||
|
(defn- transit-streamable-body
|
||||||
|
[data opts]
|
||||||
|
(reify rp/StreamableResponseBody
|
||||||
|
(write-body-to-stream [_ response output-stream]
|
||||||
|
(try
|
||||||
|
(let [tw (t/writer output-stream opts)]
|
||||||
|
(t/write! tw data))
|
||||||
|
(finally
|
||||||
|
(.close ^java.io.OutputStream output-stream))))))
|
||||||
|
|
||||||
(defn- impl-format-response-body
|
(defn- impl-format-response-body
|
||||||
[response]
|
[response request]
|
||||||
(let [body (:body response)
|
(let [body (:body response)
|
||||||
type :json-verbose]
|
opts {:type :json-verbose}]
|
||||||
(cond
|
(cond
|
||||||
(coll? body)
|
(coll? body)
|
||||||
(-> response
|
(-> response
|
||||||
(assoc :body (t/encode body {:type type}))
|
(update :headers assoc "content-type" "application/transit+json")
|
||||||
(update :headers assoc
|
(assoc :body
|
||||||
"content-type"
|
(if (= :post (:request-method request))
|
||||||
"application/transit+json"))
|
(transit-streamable-body body opts)
|
||||||
|
(t/encode body opts))))
|
||||||
|
|
||||||
(nil? body)
|
(nil? body)
|
||||||
(assoc response :status 204 :body "")
|
(assoc response :status 204 :body "")
|
||||||
|
@ -96,7 +108,7 @@
|
||||||
(fn [request]
|
(fn [request]
|
||||||
(let [response (handler request)]
|
(let [response (handler request)]
|
||||||
(cond-> response
|
(cond-> response
|
||||||
(map? response) (impl-format-response-body)))))
|
(map? response) (impl-format-response-body request)))))
|
||||||
|
|
||||||
(def format-response-body
|
(def format-response-body
|
||||||
{:name ::format-response-body
|
{:name ::format-response-body
|
||||||
|
|
|
@ -6,10 +6,14 @@
|
||||||
|
|
||||||
(ns app.http.oauth
|
(ns app.http.oauth
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[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.config :as cf]
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.loggers.audit :as audit]
|
||||||
|
[app.rpc.queries.profile :as profile]
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.logging :as l]
|
[app.util.logging :as l]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
@ -19,36 +23,6 @@
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(defn redirect-response
|
|
||||||
[uri]
|
|
||||||
{:status 302
|
|
||||||
:headers {"location" (str uri)}
|
|
||||||
:body ""})
|
|
||||||
|
|
||||||
(defn generate-error-redirect-uri
|
|
||||||
[cfg]
|
|
||||||
(-> (u/uri (:public-uri cfg))
|
|
||||||
(assoc :path "/#/auth/login")
|
|
||||||
(assoc :query (u/map->query-string {:error "unable-to-auth"}))))
|
|
||||||
|
|
||||||
(defn register-profile
|
|
||||||
[{:keys [rpc] :as cfg} info]
|
|
||||||
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
|
|
||||||
profile (method-fn info)]
|
|
||||||
(cond-> profile
|
|
||||||
(some? (:invitation-token info))
|
|
||||||
(assoc :invitation-token (:invitation-token info)))))
|
|
||||||
|
|
||||||
(defn generate-redirect-uri
|
|
||||||
[{:keys [tokens] :as cfg} profile]
|
|
||||||
(let [token (or (:invitation-token profile)
|
|
||||||
(tokens :generate {:iss :auth
|
|
||||||
:exp (dt/in-future "15m")
|
|
||||||
:profile-id (:id profile)}))]
|
|
||||||
(-> (u/uri (:public-uri cfg))
|
|
||||||
(assoc :path "/#/auth/verify-token")
|
|
||||||
(assoc :query (u/map->query-string {:token token})))))
|
|
||||||
|
|
||||||
(defn- build-redirect-uri
|
(defn- build-redirect-uri
|
||||||
[{:keys [provider] :as cfg}]
|
[{:keys [provider] :as cfg}]
|
||||||
(let [public (u/uri (:public-uri cfg))]
|
(let [public (u/uri (:public-uri cfg))]
|
||||||
|
@ -146,6 +120,7 @@
|
||||||
(string? roles) (into #{} (str/words roles))
|
(string? roles) (into #{} (str/words roles))
|
||||||
(vector? roles) (into #{} roles)
|
(vector? roles) (into #{} roles)
|
||||||
:else #{}))]
|
:else #{}))]
|
||||||
|
|
||||||
;; check if profile has a configured set of roles
|
;; check if profile has a configured set of roles
|
||||||
(when-not (set/subset? provider-roles profile-roles)
|
(when-not (set/subset? provider-roles profile-roles)
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
|
@ -175,6 +150,63 @@
|
||||||
{}
|
{}
|
||||||
params))
|
params))
|
||||||
|
|
||||||
|
(defn- retrieve-profile
|
||||||
|
[{:keys [pool] :as cfg} info]
|
||||||
|
(with-open [conn (db/open pool)]
|
||||||
|
(some->> (:email info)
|
||||||
|
(profile/retrieve-profile-data-by-email conn)
|
||||||
|
(profile/populate-additional-data conn)
|
||||||
|
(profile/decode-profile-row))))
|
||||||
|
|
||||||
|
(defn- redirect-response
|
||||||
|
[uri]
|
||||||
|
{:status 302
|
||||||
|
:headers {"location" (str uri)}
|
||||||
|
:body ""})
|
||||||
|
|
||||||
|
(defn- generate-error-redirect
|
||||||
|
[cfg error]
|
||||||
|
(let [uri (-> (u/uri (:public-uri cfg))
|
||||||
|
(assoc :path "/#/auth/login")
|
||||||
|
(assoc :query (u/map->query-string {:error "unable-to-auth" :hint (ex-message error)})))]
|
||||||
|
(redirect-response uri)))
|
||||||
|
|
||||||
|
(defn- generate-redirect
|
||||||
|
[{:keys [tokens session audit] :as cfg} request info profile]
|
||||||
|
(if profile
|
||||||
|
(let [sxf ((:create session) (:id profile))
|
||||||
|
token (or (:invitation-token info)
|
||||||
|
(tokens :generate {:iss :auth
|
||||||
|
:exp (dt/in-future "15m")
|
||||||
|
:profile-id (:id profile)}))
|
||||||
|
params {:token token}
|
||||||
|
|
||||||
|
uri (-> (u/uri (:public-uri cfg))
|
||||||
|
(assoc :path "/#/auth/verify-token")
|
||||||
|
(assoc :query (u/map->query-string params)))]
|
||||||
|
|
||||||
|
(when (fn? audit)
|
||||||
|
(audit :cmd :submit
|
||||||
|
:type "mutation"
|
||||||
|
:name "login"
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:ip-addr (audit/parse-client-ip request)
|
||||||
|
:props (audit/profile->props profile)))
|
||||||
|
|
||||||
|
(->> (redirect-response uri)
|
||||||
|
(sxf request)))
|
||||||
|
(let [info (assoc info
|
||||||
|
:iss :prepared-register
|
||||||
|
:exp (dt/in-future {:hours 48}))
|
||||||
|
token (tokens :generate info)
|
||||||
|
params (d/without-nils
|
||||||
|
{:token token
|
||||||
|
:fullname (:fullname info)})
|
||||||
|
uri (-> (u/uri (:public-uri cfg))
|
||||||
|
(assoc :path "/#/auth/register/validate")
|
||||||
|
(assoc :query (u/map->query-string params)))]
|
||||||
|
(redirect-response uri))))
|
||||||
|
|
||||||
(defn- auth-handler
|
(defn- auth-handler
|
||||||
[{:keys [tokens] :as cfg} {:keys [params] :as request}]
|
[{:keys [tokens] :as cfg} {:keys [params] :as request}]
|
||||||
(let [invitation (:invitation-token params)
|
(let [invitation (:invitation-token params)
|
||||||
|
@ -189,17 +221,15 @@
|
||||||
:body {:redirect-uri uri}}))
|
:body {:redirect-uri uri}}))
|
||||||
|
|
||||||
(defn- callback-handler
|
(defn- callback-handler
|
||||||
[{:keys [session] :as cfg} request]
|
[cfg request]
|
||||||
(try
|
(try
|
||||||
(let [info (retrieve-info cfg request)
|
(let [info (retrieve-info cfg request)
|
||||||
profile (register-profile cfg info)
|
profile (retrieve-profile cfg info)]
|
||||||
uri (generate-redirect-uri cfg profile)
|
(generate-redirect cfg request info profile))
|
||||||
sxf ((:create session) (:id profile))]
|
(catch Exception e
|
||||||
(->> (redirect-response uri)
|
(l/warn :hint "error on oauth process"
|
||||||
(sxf request)))
|
:cause e)
|
||||||
(catch Exception _e
|
(generate-error-redirect cfg e))))
|
||||||
(-> (generate-error-redirect-uri cfg)
|
|
||||||
(redirect-response)))))
|
|
||||||
|
|
||||||
;; --- INIT
|
;; --- INIT
|
||||||
|
|
||||||
|
@ -210,8 +240,8 @@
|
||||||
(s/def ::tokens fn?)
|
(s/def ::tokens fn?)
|
||||||
(s/def ::rpc map?)
|
(s/def ::rpc map?)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec :app.http.oauth/handlers [_]
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
(s/keys :req-un [::public-uri ::session ::tokens ::rpc]))
|
(s/keys :req-un [::public-uri ::session ::tokens ::rpc ::db/pool]))
|
||||||
|
|
||||||
(defn wrap-handler
|
(defn wrap-handler
|
||||||
[cfg handler]
|
[cfg handler]
|
||||||
|
@ -225,7 +255,7 @@
|
||||||
(-> (assoc @cfg :provider provider)
|
(-> (assoc @cfg :provider provider)
|
||||||
(handler request)))))
|
(handler request)))))
|
||||||
|
|
||||||
(defmethod ig/init-key :app.http.oauth/handlers
|
(defmethod ig/init-key ::handler
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(let [cfg (initialize cfg)]
|
(let [cfg (initialize cfg)]
|
||||||
{:handler (wrap-handler cfg auth-handler)
|
{:handler (wrap-handler cfg auth-handler)
|
||||||
|
|
|
@ -7,8 +7,10 @@
|
||||||
(ns app.loggers.audit
|
(ns app.loggers.audit
|
||||||
"Services related to the user activity (audit log)."
|
"Services related to the user activity (audit log)."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
|
[app.common.transit :as t]
|
||||||
[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]
|
||||||
|
@ -16,13 +18,25 @@
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.logging :as l]
|
[app.util.logging :as l]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.transit :as t]
|
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.core.async :as a]
|
[clojure.core.async :as a]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[lambdaisland.uri :as u]))
|
[lambdaisland.uri :as u]))
|
||||||
|
|
||||||
|
(defn parse-client-ip
|
||||||
|
[{:keys [headers] :as request}]
|
||||||
|
(or (some-> (get headers "x-forwarded-for") (str/split ",") first)
|
||||||
|
(get headers "x-real-ip")
|
||||||
|
(get request :remote-addr)))
|
||||||
|
|
||||||
|
(defn profile->props
|
||||||
|
[profile]
|
||||||
|
(-> profile
|
||||||
|
(select-keys [:is-active :is-muted :auth-backend :email :default-team-id :default-project-id :fullname :lang])
|
||||||
|
(d/without-nils)))
|
||||||
|
|
||||||
(defn clean-props
|
(defn clean-props
|
||||||
[{:keys [profile-id] :as event}]
|
[{:keys [profile-id] :as event}]
|
||||||
(letfn [(clean-common [props]
|
(letfn [(clean-common [props]
|
||||||
|
@ -50,6 +64,7 @@
|
||||||
(assoc k (name v))))
|
(assoc k (name v))))
|
||||||
{}
|
{}
|
||||||
props))]
|
props))]
|
||||||
|
|
||||||
(update event :props #(-> % clean-common clean-profile-id clean-complex-data))))
|
(update event :props #(-> % clean-common clean-profile-id clean-complex-data))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -87,11 +102,12 @@
|
||||||
:cause res)))
|
:cause res)))
|
||||||
(recur)))
|
(recur)))
|
||||||
|
|
||||||
(fn [& [cmd & params]]
|
(fn [& {:keys [cmd] :as params}]
|
||||||
|
(let [params (dissoc params :cmd)]
|
||||||
(case cmd
|
(case cmd
|
||||||
:stop (a/close! input)
|
:stop (a/close! input)
|
||||||
:submit (when-not (a/offer! input (first params))
|
:submit (when-not (a/offer! input params)
|
||||||
(l/warn :msg "activity channel is full")))))))
|
(l/warn :msg "activity channel is full"))))))))
|
||||||
|
|
||||||
|
|
||||||
(defn- persist-events
|
(defn- persist-events
|
||||||
|
@ -101,12 +117,13 @@
|
||||||
(:name event)
|
(:name event)
|
||||||
(:type event)
|
(:type event)
|
||||||
(:profile-id event)
|
(:profile-id event)
|
||||||
|
(some-> (:ip-addr event) db/inet)
|
||||||
(db/tjson (:props event))])]
|
(db/tjson (:props event))])]
|
||||||
|
|
||||||
(aa/with-thread executor
|
(aa/with-thread executor
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(db/insert-multi! conn :audit-log
|
(db/insert-multi! conn :audit-log
|
||||||
[:id :name :type :profile-id :props]
|
[:id :name :type :profile-id :ip-addr :props]
|
||||||
(sequence (map event->row) events))))))
|
(sequence (map event->row) events))))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -147,17 +164,22 @@
|
||||||
|
|
||||||
(defn archive-events
|
(defn archive-events
|
||||||
[{:keys [pool uri tokens] :as cfg}]
|
[{:keys [pool uri tokens] :as cfg}]
|
||||||
(letfn [(decode-row [{:keys [props] :as row}]
|
(letfn [(decode-row [{:keys [props ip-addr] :as row}]
|
||||||
(cond-> row
|
(cond-> row
|
||||||
(db/pgobject? props)
|
(db/pgobject? props)
|
||||||
(assoc :props (db/decode-transit-pgobject props))))
|
(assoc :props (db/decode-transit-pgobject props))
|
||||||
|
|
||||||
(row->event [{:keys [name type created-at profile-id props]}]
|
(db/pgobject? ip-addr "inet")
|
||||||
{:type type
|
(assoc :ip-addr (db/decode-inet ip-addr))))
|
||||||
|
|
||||||
|
(row->event [{:keys [name type created-at profile-id props ip-addr]}]
|
||||||
|
(cond-> {:type type
|
||||||
:name name
|
:name name
|
||||||
:timestamp created-at
|
:timestamp created-at
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:props props})
|
:props props}
|
||||||
|
(some? ip-addr)
|
||||||
|
(update :context assoc :source-ip ip-addr)))
|
||||||
|
|
||||||
(send [events]
|
(send [events]
|
||||||
(let [token (tokens :generate {:iss "authentication"
|
(let [token (tokens :generate {:iss "authentication"
|
||||||
|
@ -168,7 +190,7 @@
|
||||||
"origin" (cf/get :public-uri)
|
"origin" (cf/get :public-uri)
|
||||||
"cookie" (u/map->query-string {:auth-token token})}
|
"cookie" (u/map->query-string {:auth-token token})}
|
||||||
params {:uri uri
|
params {:uri uri
|
||||||
:timeout 5000
|
:timeout 6000
|
||||||
:method :post
|
:method :post
|
||||||
:headers headers
|
:headers headers
|
||||||
:body body}
|
:body body}
|
||||||
|
@ -187,7 +209,6 @@
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [rows (db/exec! conn [sql:retrieve-batch-of-audit-log])
|
(let [rows (db/exec! conn [sql:retrieve-batch-of-audit-log])
|
||||||
|
|
||||||
xform (comp (map decode-row)
|
xform (comp (map decode-row)
|
||||||
(map row->event))
|
(map row->event))
|
||||||
events (into [] xform rows)]
|
events (into [] xform rows)]
|
||||||
|
|
|
@ -40,7 +40,8 @@
|
||||||
|
|
||||||
(defmethod ig/init-key ::reporter
|
(defmethod ig/init-key ::reporter
|
||||||
[_ {:keys [receiver uri] :as cfg}]
|
[_ {:keys [receiver uri] :as cfg}]
|
||||||
(l/info :msg "intializing mattermost error reporter" :uri uri)
|
(when uri
|
||||||
|
(l/info :msg "initializing mattermost error reporter" :uri uri)
|
||||||
(let [output (a/chan (a/sliding-buffer 128)
|
(let [output (a/chan (a/sliding-buffer 128)
|
||||||
(filter #(= (:level %) "error")))]
|
(filter #(= (:level %) "error")))]
|
||||||
(receiver :sub output)
|
(receiver :sub output)
|
||||||
|
@ -51,11 +52,12 @@
|
||||||
(do
|
(do
|
||||||
(a/<! (handle-event cfg msg))
|
(a/<! (handle-event cfg msg))
|
||||||
(recur)))))
|
(recur)))))
|
||||||
output))
|
output)))
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::reporter
|
(defmethod ig/halt-key! ::reporter
|
||||||
[_ output]
|
[_ output]
|
||||||
(a/close! output))
|
(when output
|
||||||
|
(a/close! output)))
|
||||||
|
|
||||||
(defn- send-mattermost-notification!
|
(defn- send-mattermost-notification!
|
||||||
[cfg {:keys [host version id] :as cdata}]
|
[cfg {:keys [host version id] :as cdata}]
|
||||||
|
@ -110,7 +112,7 @@
|
||||||
(aa/with-thread executor
|
(aa/with-thread executor
|
||||||
(try
|
(try
|
||||||
(let [cdata (parse-event event)]
|
(let [cdata (parse-event event)]
|
||||||
(when (and (:uri cfg) @enabled-mattermost)
|
(when @enabled-mattermost
|
||||||
(send-mattermost-notification! cfg cdata))
|
(send-mattermost-notification! cfg cdata))
|
||||||
(persist-on-database! cfg cdata))
|
(persist-on-database! cfg cdata))
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
:redis-uri (cf/get :redis-uri)}
|
:redis-uri (cf/get :redis-uri)}
|
||||||
|
|
||||||
:app.tokens/tokens
|
:app.tokens/tokens
|
||||||
{:props (ig/ref :app.setup/props)}
|
{:keys (ig/ref :app.setup/keys)}
|
||||||
|
|
||||||
:app.storage/gc-deleted-task
|
:app.storage/gc-deleted-task
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
:tokens (ig/ref :app.tokens/tokens)
|
:tokens (ig/ref :app.tokens/tokens)
|
||||||
:public-uri (cf/get :public-uri)
|
:public-uri (cf/get :public-uri)
|
||||||
:metrics (ig/ref :app.metrics/metrics)
|
:metrics (ig/ref :app.metrics/metrics)
|
||||||
:oauth (ig/ref :app.http.oauth/handlers)
|
:oauth (ig/ref :app.http.oauth/handler)
|
||||||
:assets (ig/ref :app.http.assets/handlers)
|
:assets (ig/ref :app.http.assets/handlers)
|
||||||
:storage (ig/ref :app.storage/storage)
|
:storage (ig/ref :app.storage/storage)
|
||||||
:sns-webhook (ig/ref :app.http.awsns/handler)
|
:sns-webhook (ig/ref :app.http.awsns/handler)
|
||||||
|
@ -107,10 +107,12 @@
|
||||||
:app.http.feedback/handler
|
:app.http.feedback/handler
|
||||||
{:pool (ig/ref :app.db/pool)}
|
{:pool (ig/ref :app.db/pool)}
|
||||||
|
|
||||||
:app.http.oauth/handlers
|
:app.http.oauth/handler
|
||||||
{:rpc (ig/ref :app.rpc/rpc)
|
{:rpc (ig/ref :app.rpc/rpc)
|
||||||
:session (ig/ref :app.http.session/session)
|
:session (ig/ref :app.http.session/session)
|
||||||
|
:pool (ig/ref :app.db/pool)
|
||||||
:tokens (ig/ref :app.tokens/tokens)
|
:tokens (ig/ref :app.tokens/tokens)
|
||||||
|
:audit (ig/ref :app.loggers.audit/collector)
|
||||||
:public-uri (cf/get :public-uri)}
|
:public-uri (cf/get :public-uri)}
|
||||||
|
|
||||||
;; RLimit definition for password hashing
|
;; RLimit definition for password hashing
|
||||||
|
@ -166,27 +168,34 @@
|
||||||
:tasks (ig/ref :app.worker/registry)
|
:tasks (ig/ref :app.worker/registry)
|
||||||
:pool (ig/ref :app.db/pool)
|
:pool (ig/ref :app.db/pool)
|
||||||
:schedule
|
:schedule
|
||||||
[{:cron #app/cron "0 0 0 * * ? *" ;; daily
|
[{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||||
:task :file-media-gc}
|
:task :file-media-gc}
|
||||||
|
|
||||||
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||||
:task :file-xlog-gc}
|
:task :file-xlog-gc}
|
||||||
|
|
||||||
{:cron #app/cron "0 0 1 * * ?" ;; daily (1 hour shift)
|
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||||
:task :storage-deleted-gc}
|
:task :storage-deleted-gc}
|
||||||
|
|
||||||
{:cron #app/cron "0 0 2 * * ?" ;; daily (2 hour shift)
|
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||||
:task :storage-touched-gc}
|
:task :storage-touched-gc}
|
||||||
|
|
||||||
{:cron #app/cron "0 0 3 * * ?" ;; daily (3 hour shift)
|
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||||
:task :session-gc}
|
:task :session-gc}
|
||||||
|
|
||||||
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||||
:task :storage-recheck}
|
:task :storage-recheck}
|
||||||
|
|
||||||
|
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||||
|
:task :objects-gc}
|
||||||
|
|
||||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||||
:task :tasks-gc}
|
:task :tasks-gc}
|
||||||
|
|
||||||
|
(when (cf/get :fdata-storage-backed)
|
||||||
|
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||||
|
:task :file-offload})
|
||||||
|
|
||||||
(when (cf/get :audit-archive-enabled)
|
(when (cf/get :audit-archive-enabled)
|
||||||
{:cron #app/cron "0 0 * * * ?" ;; every 1h
|
{:cron #app/cron "0 0 * * * ?" ;; every 1h
|
||||||
:task :audit-archive})
|
:task :audit-archive})
|
||||||
|
@ -203,6 +212,7 @@
|
||||||
{:metrics (ig/ref :app.metrics/metrics)
|
{:metrics (ig/ref :app.metrics/metrics)
|
||||||
:tasks
|
:tasks
|
||||||
{:sendmail (ig/ref :app.emails/sendmail-handler)
|
{:sendmail (ig/ref :app.emails/sendmail-handler)
|
||||||
|
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
|
||||||
:delete-object (ig/ref :app.tasks.delete-object/handler)
|
:delete-object (ig/ref :app.tasks.delete-object/handler)
|
||||||
:delete-profile (ig/ref :app.tasks.delete-profile/handler)
|
:delete-profile (ig/ref :app.tasks.delete-profile/handler)
|
||||||
:file-media-gc (ig/ref :app.tasks.file-media-gc/handler)
|
:file-media-gc (ig/ref :app.tasks.file-media-gc/handler)
|
||||||
|
@ -213,6 +223,7 @@
|
||||||
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
||||||
:telemetry (ig/ref :app.tasks.telemetry/handler)
|
:telemetry (ig/ref :app.tasks.telemetry/handler)
|
||||||
:session-gc (ig/ref :app.http.session/gc-task)
|
:session-gc (ig/ref :app.http.session/gc-task)
|
||||||
|
:file-offload (ig/ref :app.tasks.file-offload/handler)
|
||||||
:audit-archive (ig/ref :app.loggers.audit/archive-task)
|
:audit-archive (ig/ref :app.loggers.audit/archive-task)
|
||||||
:audit-archive-gc (ig/ref :app.loggers.audit/archive-gc-task)}}
|
:audit-archive-gc (ig/ref :app.loggers.audit/archive-gc-task)}}
|
||||||
|
|
||||||
|
@ -236,6 +247,11 @@
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
:storage (ig/ref :app.storage/storage)}
|
:storage (ig/ref :app.storage/storage)}
|
||||||
|
|
||||||
|
:app.tasks.objects-gc/handler
|
||||||
|
{:pool (ig/ref :app.db/pool)
|
||||||
|
:storage (ig/ref :app.storage/storage)
|
||||||
|
:max-age cf/deletion-delay}
|
||||||
|
|
||||||
:app.tasks.delete-profile/handler
|
:app.tasks.delete-profile/handler
|
||||||
{:pool (ig/ref :app.db/pool)}
|
{:pool (ig/ref :app.db/pool)}
|
||||||
|
|
||||||
|
@ -245,7 +261,13 @@
|
||||||
|
|
||||||
:app.tasks.file-xlog-gc/handler
|
:app.tasks.file-xlog-gc/handler
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
:max-age (dt/duration {:hours 24})}
|
:max-age (dt/duration {:hours 72})}
|
||||||
|
|
||||||
|
:app.tasks.file-offload/handler
|
||||||
|
{:pool (ig/ref :app.db/pool)
|
||||||
|
:max-age (dt/duration {:seconds 5})
|
||||||
|
:storage (ig/ref :app.storage/storage)
|
||||||
|
:backend (cf/get :fdata-storage-backed :fdata-s3)}
|
||||||
|
|
||||||
:app.tasks.telemetry/handler
|
:app.tasks.telemetry/handler
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
|
@ -261,6 +283,9 @@
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
:key (cf/get :secret-key)}
|
:key (cf/get :secret-key)}
|
||||||
|
|
||||||
|
:app.setup/keys
|
||||||
|
{:props (ig/ref :app.setup/props)}
|
||||||
|
|
||||||
:app.loggers.zmq/receiver
|
:app.loggers.zmq/receiver
|
||||||
{:endpoint (cf/get :loggers-zmq-uri)}
|
{:endpoint (cf/get :loggers-zmq-uri)}
|
||||||
|
|
||||||
|
@ -297,23 +322,32 @@
|
||||||
:app.storage/storage
|
:app.storage/storage
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
:executor (ig/ref :app.worker/executor)
|
:executor (ig/ref :app.worker/executor)
|
||||||
:backend (cf/get :storage-backend :fs)
|
:backend (cf/get :assets-storage-backend :assets-fs)
|
||||||
:backends {:s3 (ig/ref [::main :app.storage.s3/backend])
|
:backends {:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
|
||||||
:db (ig/ref [::main :app.storage.db/backend])
|
:assets-db (ig/ref [::assets :app.storage.db/backend])
|
||||||
:fs (ig/ref [::main :app.storage.fs/backend])
|
:assets-fs (ig/ref [::assets :app.storage.fs/backend])
|
||||||
:tmp (ig/ref [::tmp :app.storage.fs/backend])}}
|
:s3 (ig/ref [::assets :app.storage.s3/backend])
|
||||||
|
:db (ig/ref [::assets :app.storage.db/backend])
|
||||||
|
:fs (ig/ref [::assets :app.storage.fs/backend])
|
||||||
|
:tmp (ig/ref [::tmp :app.storage.fs/backend])
|
||||||
|
:fdata-s3 (ig/ref [::fdata :app.storage.s3/backend])}}
|
||||||
|
|
||||||
[::main :app.storage.s3/backend]
|
[::fdata :app.storage.s3/backend]
|
||||||
{:region (cf/get :storage-s3-region)
|
{:region (cf/get :storage-fdata-s3-region)
|
||||||
:bucket (cf/get :storage-s3-bucket)}
|
:bucket (cf/get :storage-fdata-s3-bucket)
|
||||||
|
:prefix (cf/get :storage-fdata-s3-prefix)}
|
||||||
|
|
||||||
[::main :app.storage.fs/backend]
|
[::assets :app.storage.s3/backend]
|
||||||
{:directory (cf/get :storage-fs-directory)}
|
{:region (cf/get :storage-assets-s3-region)
|
||||||
|
:bucket (cf/get :storage-assets-s3-bucket)}
|
||||||
|
|
||||||
|
[::assets :app.storage.fs/backend]
|
||||||
|
{:directory (cf/get :storage-assets-fs-directory)}
|
||||||
|
|
||||||
[::tmp :app.storage.fs/backend]
|
[::tmp :app.storage.fs/backend]
|
||||||
{:directory "/tmp/penpot"}
|
{:directory "/tmp/penpot"}
|
||||||
|
|
||||||
[::main :app.storage.db/backend]
|
[::assets :app.storage.db/backend]
|
||||||
{:pool (ig/ref :app.db/pool)}})
|
{:pool (ig/ref :app.db/pool)}})
|
||||||
|
|
||||||
(def system nil)
|
(def system nil)
|
||||||
|
|
|
@ -210,9 +210,15 @@
|
||||||
([a b]
|
([a b]
|
||||||
(mobj :inc)
|
(mobj :inc)
|
||||||
(origf a b))
|
(origf a b))
|
||||||
([a b & more]
|
([a b c]
|
||||||
(mobj :inc)
|
(mobj :inc)
|
||||||
(apply origf a b more)))
|
(origf a b c))
|
||||||
|
([a b c d]
|
||||||
|
(mobj :inc)
|
||||||
|
(origf a b c d))
|
||||||
|
([a b c d & more]
|
||||||
|
(mobj :inc)
|
||||||
|
(apply origf a b c d more)))
|
||||||
(assoc mdata ::original origf))))
|
(assoc mdata ::original origf))))
|
||||||
([rootf mobj labels]
|
([rootf mobj labels]
|
||||||
(let [mdata (meta rootf)
|
(let [mdata (meta rootf)
|
||||||
|
|
|
@ -175,6 +175,24 @@
|
||||||
|
|
||||||
{:name "0055-mod-file-media-object-table"
|
{:name "0055-mod-file-media-object-table"
|
||||||
:fn (mg/resource "app/migrations/sql/0055-mod-file-media-object-table.sql")}
|
:fn (mg/resource "app/migrations/sql/0055-mod-file-media-object-table.sql")}
|
||||||
|
|
||||||
|
{:name "0056-add-missing-index-on-deleted-at"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0056-add-missing-index-on-deleted-at.sql")}
|
||||||
|
|
||||||
|
{:name "0057-del-profile-on-delete-trigger"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0057-del-profile-on-delete-trigger.sql")}
|
||||||
|
|
||||||
|
{:name "0058-del-team-on-delete-trigger"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0058-del-team-on-delete-trigger.sql")}
|
||||||
|
|
||||||
|
{:name "0059-mod-audit-log-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0059-mod-audit-log-table.sql")}
|
||||||
|
|
||||||
|
{:name "0060-mod-file-change-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0060-mod-file-change-table.sql")}
|
||||||
|
|
||||||
|
{:name "0061-mod-file-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0061-mod-file-table.sql")}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
CREATE INDEX profile_deleted_at_idx
|
||||||
|
ON profile(deleted_at, id)
|
||||||
|
WHERE deleted_at IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX project_deleted_at_idx
|
||||||
|
ON project(deleted_at, id)
|
||||||
|
WHERE deleted_at IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX team_deleted_at_idx
|
||||||
|
ON team(deleted_at, id)
|
||||||
|
WHERE deleted_at IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX team_font_variant_deleted_at_idx
|
||||||
|
ON team_font_variant(deleted_at, id)
|
||||||
|
WHERE deleted_at IS NOT NULL;
|
|
@ -0,0 +1,2 @@
|
||||||
|
DROP TRIGGER profile__on_delete__tgr ON profile CASCADE;
|
||||||
|
DROP FUNCTION on_delete_profile ();
|
|
@ -0,0 +1,2 @@
|
||||||
|
DROP TRIGGER team__on_delete__tgr ON team CASCADE;
|
||||||
|
DROP FUNCTION on_delete_team ();
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE audit_log
|
||||||
|
ADD COLUMN ip_addr inet NULL;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE file_change
|
||||||
|
ALTER COLUMN data DROP NOT NULL;
|
10
backend/src/app/migrations/sql/0061-mod-file-table.sql
Normal file
10
backend/src/app/migrations/sql/0061-mod-file-table.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
CREATE INDEX IF NOT EXISTS file__modified_at__with__data__idx
|
||||||
|
ON file (modified_at, id)
|
||||||
|
WHERE data IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE file
|
||||||
|
ADD COLUMN data_backend text NULL,
|
||||||
|
ALTER COLUMN data_backend SET STORAGE EXTERNAL;
|
||||||
|
|
||||||
|
DROP TRIGGER file_on_update_tgr ON file;
|
||||||
|
DROP FUNCTION handle_file_update ();
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Fix problem with content-type inconherence
|
||||||
|
|
||||||
|
UPDATE storage_object so
|
||||||
|
SET metadata = jsonb_set(metadata, '{~:content-type}', to_jsonb(fmo.mtype))
|
||||||
|
FROM file_media_object fmo
|
||||||
|
WHERE so.id = fmo.media_id and
|
||||||
|
so.metadata->>'~:content-type' != fmo.mtype;
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
"A websocket based notifications mechanism."
|
"A websocket based notifications mechanism."
|
||||||
(:require
|
(:require
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
|
[app.common.transit :as t]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.util.async :as aa]
|
[app.util.async :as aa]
|
||||||
[app.util.logging :as l]
|
[app.util.logging :as l]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.transit :as t]
|
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[clojure.core.async :as a]
|
[clojure.core.async :as a]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
@ -163,7 +163,7 @@
|
||||||
|
|
||||||
;; when connection is closed
|
;; when connection is closed
|
||||||
(mtx-aconn :dec)
|
(mtx-aconn :dec)
|
||||||
(mtx-sessions :observe (/ (inst-ms (dt/duration-between created-at (dt/now))) 1000.0))
|
(mtx-sessions :observe (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0))
|
||||||
|
|
||||||
;; close subscription
|
;; close subscription
|
||||||
(a/close! sub-ch))))
|
(a/close! sub-ch))))
|
||||||
|
|
|
@ -32,9 +32,10 @@
|
||||||
[methods {:keys [profile-id] :as request}]
|
[methods {:keys [profile-id] :as request}]
|
||||||
(let [type (keyword (get-in request [:path-params :type]))
|
(let [type (keyword (get-in request [:path-params :type]))
|
||||||
|
|
||||||
data (d/merge (:params request)
|
data (merge (:params request)
|
||||||
(:body-params request)
|
(:body-params request)
|
||||||
(:uploads request))
|
(:uploads request)
|
||||||
|
{::request request})
|
||||||
|
|
||||||
data (if profile-id
|
data (if profile-id
|
||||||
(assoc data :profile-id profile-id)
|
(assoc data :profile-id profile-id)
|
||||||
|
@ -50,12 +51,15 @@
|
||||||
(defn- rpc-mutation-handler
|
(defn- rpc-mutation-handler
|
||||||
[methods {:keys [profile-id] :as request}]
|
[methods {:keys [profile-id] :as request}]
|
||||||
(let [type (keyword (get-in request [:path-params :type]))
|
(let [type (keyword (get-in request [:path-params :type]))
|
||||||
data (d/merge (:params request)
|
data (merge (:params request)
|
||||||
(:body-params request)
|
(:body-params request)
|
||||||
(:uploads request))
|
(:uploads request)
|
||||||
|
{::request request})
|
||||||
|
|
||||||
data (if profile-id
|
data (if profile-id
|
||||||
(assoc data :profile-id profile-id)
|
(assoc data :profile-id profile-id)
|
||||||
(dissoc data :profile-id))
|
(dissoc data :profile-id))
|
||||||
|
|
||||||
result ((get methods type default-handler) data)
|
result ((get methods type default-handler) data)
|
||||||
mdata (meta result)]
|
mdata (meta result)]
|
||||||
(cond->> {:status 200 :body result}
|
(cond->> {:status 200 :body result}
|
||||||
|
@ -85,7 +89,6 @@
|
||||||
(rlm/execute rlinst (f cfg params))))
|
(rlm/execute rlinst (f cfg params))))
|
||||||
f))
|
f))
|
||||||
|
|
||||||
|
|
||||||
(defn- wrap-impl
|
(defn- wrap-impl
|
||||||
[{:keys [audit] :as cfg} f mdata]
|
[{:keys [audit] :as cfg} f mdata]
|
||||||
(let [f (wrap-with-rlimits cfg f mdata)
|
(let [f (wrap-with-rlimits cfg f mdata)
|
||||||
|
@ -95,23 +98,34 @@
|
||||||
|
|
||||||
(l/trace :action "register" :name (::sv/name mdata))
|
(l/trace :action "register" :name (::sv/name mdata))
|
||||||
(fn [params]
|
(fn [params]
|
||||||
|
|
||||||
|
;; Raise authentication error when rpc method requires auth but
|
||||||
|
;; no profile-id is found in the request.
|
||||||
(when (and auth? (not (uuid? (:profile-id params))))
|
(when (and auth? (not (uuid? (:profile-id params))))
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :authentication
|
||||||
:code :authentication-required
|
:code :authentication-required
|
||||||
:hint "authentication required for this endpoint"))
|
:hint "authentication required for this endpoint"))
|
||||||
(let [params (us/conform spec params)
|
|
||||||
result (f cfg params)
|
(let [params' (dissoc params ::request)
|
||||||
resultm (meta result)]
|
params' (us/conform spec params')
|
||||||
(when (and (::type cfg) (fn? audit))
|
result (f cfg params')]
|
||||||
(let [profile-id (or (:profile-id params)
|
|
||||||
|
;; When audit log is enabled (default false).
|
||||||
|
(when (fn? audit)
|
||||||
|
(let [resultm (meta result)
|
||||||
|
request (::request params)
|
||||||
|
profile-id (or (:profile-id params')
|
||||||
(:profile-id result)
|
(:profile-id result)
|
||||||
(::audit/profile-id resultm))
|
(::audit/profile-id resultm))
|
||||||
props (d/merge params (::audit/props resultm))]
|
props (d/merge params (::audit/props resultm))]
|
||||||
(audit :submit {:type (::type cfg)
|
(audit :cmd :submit
|
||||||
|
:type (::type cfg)
|
||||||
:name (or (::audit/name resultm)
|
:name (or (::audit/name resultm)
|
||||||
(::sv/name mdata))
|
(::sv/name mdata))
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:props props})))
|
:ip-addr (audit/parse-client-ip request)
|
||||||
|
:props (audit/profile->props props))))
|
||||||
|
|
||||||
result))))
|
result))))
|
||||||
|
|
||||||
(defn- process-method
|
(defn- process-method
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
[app.rpc.mutations.profile :as profile]
|
[app.rpc.mutations.profile :as profile]
|
||||||
[app.setup.initial-data :as sid]
|
[app.setup.initial-data :as sid]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.worker :as wrk]
|
[app.util.time :as dt]
|
||||||
[buddy.core.codecs :as bc]
|
[buddy.core.codecs :as bc]
|
||||||
[buddy.core.nonce :as bn]
|
[buddy.core.nonce :as bn]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
@ -35,6 +35,7 @@
|
||||||
:email email
|
:email email
|
||||||
:fullname fullname
|
:fullname fullname
|
||||||
:is-demo true
|
:is-demo true
|
||||||
|
:deleted-at (dt/in-future cfg/deletion-delay)
|
||||||
:password password
|
:password password
|
||||||
:props {:onboarding-viewed true}}]
|
:props {:onboarding-viewed true}}]
|
||||||
|
|
||||||
|
@ -48,12 +49,6 @@
|
||||||
(#'profile/create-profile-relations conn)
|
(#'profile/create-profile-relations conn)
|
||||||
(sid/load-initial-project! conn))
|
(sid/load-initial-project! conn))
|
||||||
|
|
||||||
;; Schedule deletion of the demo profile
|
|
||||||
(wrk/submit! {::wrk/task :delete-profile
|
|
||||||
::wrk/delay cfg/deletion-delay
|
|
||||||
::wrk/conn conn
|
|
||||||
:profile-id id})
|
|
||||||
|
|
||||||
(with-meta {:email email
|
(with-meta {:email email
|
||||||
:password password}
|
:password password}
|
||||||
{::audit/profile-id id}))))
|
{::audit/profile-id id}))))
|
||||||
|
|
|
@ -11,17 +11,19 @@
|
||||||
[app.common.pages.migrations :as pmg]
|
[app.common.pages.migrations :as pmg]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cfg]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc.permissions :as perms]
|
[app.rpc.permissions :as perms]
|
||||||
[app.rpc.queries.files :as files]
|
[app.rpc.queries.files :as files]
|
||||||
[app.rpc.queries.projects :as proj]
|
[app.rpc.queries.projects :as proj]
|
||||||
|
[app.storage.impl :as simpl]
|
||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
[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]))
|
||||||
|
|
||||||
|
(declare create-file)
|
||||||
|
|
||||||
;; --- Helpers & Specs
|
;; --- Helpers & Specs
|
||||||
|
|
||||||
(s/def ::id ::us/uuid)
|
(s/def ::id ::us/uuid)
|
||||||
|
@ -32,8 +34,6 @@
|
||||||
|
|
||||||
;; --- Mutation: Create File
|
;; --- Mutation: Create File
|
||||||
|
|
||||||
(declare create-file)
|
|
||||||
|
|
||||||
(s/def ::is-shared ::us/boolean)
|
(s/def ::is-shared ::us/boolean)
|
||||||
(s/def ::create-file
|
(s/def ::create-file
|
||||||
(s/keys :req-un [::profile-id ::name ::project-id]
|
(s/keys :req-un [::profile-id ::name ::project-id]
|
||||||
|
@ -45,7 +45,6 @@
|
||||||
(proj/check-edition-permissions! conn profile-id project-id)
|
(proj/check-edition-permissions! conn profile-id project-id)
|
||||||
(create-file conn params)))
|
(create-file conn params)))
|
||||||
|
|
||||||
|
|
||||||
(defn create-file-role
|
(defn create-file-role
|
||||||
[conn {:keys [file-id profile-id role]}]
|
[conn {:keys [file-id profile-id role]}]
|
||||||
(let [params {:file-id file-id
|
(let [params {:file-id file-id
|
||||||
|
@ -54,21 +53,24 @@
|
||||||
(db/insert! conn :file-profile-rel))))
|
(db/insert! conn :file-profile-rel))))
|
||||||
|
|
||||||
(defn create-file
|
(defn create-file
|
||||||
[conn {:keys [id name project-id is-shared]
|
[conn {:keys [id name project-id is-shared data deleted-at]
|
||||||
:or {is-shared false}
|
:or {is-shared false
|
||||||
|
deleted-at nil}
|
||||||
:as params}]
|
:as params}]
|
||||||
(let [id (or id (uuid/next))
|
(let [id (or id (:id data) (uuid/next))
|
||||||
data (cp/make-file-data id)
|
data (or data (cp/make-file-data id))
|
||||||
file (db/insert! conn :file
|
file (db/insert! conn :file
|
||||||
{:id id
|
{:id id
|
||||||
:project-id project-id
|
:project-id project-id
|
||||||
:name name
|
:name name
|
||||||
:is-shared is-shared
|
:is-shared is-shared
|
||||||
:data (blob/encode data)})]
|
:data (blob/encode data)
|
||||||
|
:deleted-at deleted-at})]
|
||||||
|
|
||||||
(->> (assoc params :file-id id :role :owner)
|
(->> (assoc params :file-id id :role :owner)
|
||||||
(create-file-role conn))
|
(create-file-role conn))
|
||||||
(assoc file :data data)))
|
|
||||||
|
|
||||||
|
(assoc file :data data)))
|
||||||
|
|
||||||
;; --- Mutation: Rename File
|
;; --- Mutation: Rename File
|
||||||
|
|
||||||
|
@ -109,7 +111,6 @@
|
||||||
{:is-shared is-shared}
|
{:is-shared is-shared}
|
||||||
{:id id}))
|
{:id id}))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Delete File
|
;; --- Mutation: Delete File
|
||||||
|
|
||||||
(declare mark-file-deleted)
|
(declare mark-file-deleted)
|
||||||
|
@ -122,13 +123,6 @@
|
||||||
(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)
|
||||||
|
|
||||||
;; Schedule object deletion
|
|
||||||
(wrk/submit! {::wrk/task :delete-object
|
|
||||||
::wrk/delay cfg/deletion-delay
|
|
||||||
::wrk/conn conn
|
|
||||||
:id id
|
|
||||||
:type :file})
|
|
||||||
|
|
||||||
(mark-file-deleted conn params)))
|
(mark-file-deleted conn params)))
|
||||||
|
|
||||||
(defn mark-file-deleted
|
(defn mark-file-deleted
|
||||||
|
@ -175,7 +169,7 @@
|
||||||
(s/keys :req-un [::profile-id ::file-id ::library-id]))
|
(s/keys :req-un [::profile-id ::file-id ::library-id]))
|
||||||
|
|
||||||
(sv/defmethod ::unlink-file-from-library
|
(sv/defmethod ::unlink-file-from-library
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(unlink-file-from-library conn params)))
|
(unlink-file-from-library conn params)))
|
||||||
|
@ -195,7 +189,7 @@
|
||||||
(s/keys :req-un [::profile-id ::file-id ::library-id]))
|
(s/keys :req-un [::profile-id ::file-id ::library-id]))
|
||||||
|
|
||||||
(sv/defmethod ::update-sync
|
(sv/defmethod ::update-sync
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(update-sync conn params)))
|
(update-sync conn params)))
|
||||||
|
@ -207,7 +201,6 @@
|
||||||
{:file-id file-id
|
{:file-id file-id
|
||||||
:library-file-id library-id}))
|
:library-file-id library-id}))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Ignore updates in linked files
|
;; --- Mutation: Ignore updates in linked files
|
||||||
|
|
||||||
(declare ignore-sync)
|
(declare ignore-sync)
|
||||||
|
@ -216,7 +209,7 @@
|
||||||
(s/keys :req-un [::profile-id ::file-id ::date]))
|
(s/keys :req-un [::profile-id ::file-id ::date]))
|
||||||
|
|
||||||
(sv/defmethod ::ignore-sync
|
(sv/defmethod ::ignore-sync
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id date] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(ignore-sync conn params)))
|
(ignore-sync conn params)))
|
||||||
|
@ -278,15 +271,31 @@
|
||||||
(sv/defmethod ::update-file
|
(sv/defmethod ::update-file
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [{:keys [id] :as file} (db/get-by-id conn :file id {:for-update true})]
|
(db/xact-lock! conn id)
|
||||||
|
(let [{:keys [id] :as file} (db/get-by-id conn :file id {:for-key-share true})]
|
||||||
(files/check-edition-permissions! conn profile-id id)
|
(files/check-edition-permissions! conn profile-id id)
|
||||||
(update-file (assoc cfg :conn conn)
|
(update-file (assoc cfg :conn conn)
|
||||||
(assoc params :file file)))))
|
(assoc params :file file)))))
|
||||||
|
|
||||||
|
(defn- take-snapshot?
|
||||||
|
"Defines the rule when file `data` snapshot should be saved."
|
||||||
|
[{:keys [revn modified-at] :as file}]
|
||||||
|
;; The snapshot will be saved every 20 changes or if the last
|
||||||
|
;; modification is older than 3 hour.
|
||||||
|
(or (zero? (mod revn 20))
|
||||||
|
(> (inst-ms (dt/diff modified-at (dt/now)))
|
||||||
|
(inst-ms (dt/duration {:hours 3})))))
|
||||||
|
|
||||||
|
(defn- delete-from-storage
|
||||||
|
[{:keys [storage] :as cfg} file]
|
||||||
|
(when-let [backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))]
|
||||||
|
(simpl/del-object backend file)))
|
||||||
|
|
||||||
(defn- update-file
|
(defn- update-file
|
||||||
[{:keys [conn] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}]
|
[{:keys [conn] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}]
|
||||||
(when (> (:revn params)
|
(when (> (:revn params)
|
||||||
(:revn file))
|
(:revn file))
|
||||||
|
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :revn-conflict
|
:code :revn-conflict
|
||||||
:hint "The incoming revision number is greater that stored version."
|
:hint "The incoming revision number is greater that stored version."
|
||||||
|
@ -297,7 +306,8 @@
|
||||||
(mapcat :changes changes-with-metadata)
|
(mapcat :changes changes-with-metadata)
|
||||||
changes)
|
changes)
|
||||||
|
|
||||||
file (-> file
|
ts (dt/now)
|
||||||
|
file (-> (files/retrieve-data cfg file)
|
||||||
(update :revn inc)
|
(update :revn inc)
|
||||||
(update :data (fn [data]
|
(update :data (fn [data]
|
||||||
(-> data
|
(-> data
|
||||||
|
@ -311,26 +321,55 @@
|
||||||
{:id (uuid/next)
|
{:id (uuid/next)
|
||||||
:session-id session-id
|
:session-id session-id
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
|
:created-at ts
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:revn (:revn file)
|
:revn (:revn file)
|
||||||
:data (:data file)
|
:data (when (take-snapshot? file)
|
||||||
|
(:data file))
|
||||||
:changes (blob/encode changes)})
|
:changes (blob/encode changes)})
|
||||||
|
|
||||||
;; Update file
|
;; Update file
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
{:revn (:revn file)
|
{:revn (:revn file)
|
||||||
:data (:data file)
|
:data (:data file)
|
||||||
|
:data-backend nil
|
||||||
|
:modified-at ts
|
||||||
:has-media-trimmed false}
|
:has-media-trimmed false}
|
||||||
{:id (:id file)})
|
{:id (:id file)})
|
||||||
|
|
||||||
(let [params (-> params (assoc :file file
|
;; We need to delete the data from external storage backend
|
||||||
:changes changes))]
|
(when-not (nil? (:data-backend file))
|
||||||
|
(delete-from-storage cfg file))
|
||||||
|
|
||||||
|
(db/update! conn :project
|
||||||
|
{:modified-at ts}
|
||||||
|
{:id (:project-id file)})
|
||||||
|
|
||||||
|
(let [params (assoc params :file file :changes changes)]
|
||||||
;; Send asynchronous notifications
|
;; Send asynchronous notifications
|
||||||
(send-notifications cfg params)
|
(send-notifications cfg params)
|
||||||
|
|
||||||
;; Retrieve and return lagged data
|
;; Retrieve and return lagged data
|
||||||
(retrieve-lagged-changes conn params))))
|
(retrieve-lagged-changes conn params))))
|
||||||
|
|
||||||
|
(def ^:private
|
||||||
|
sql:lagged-changes
|
||||||
|
"select s.id, s.revn, s.file_id,
|
||||||
|
s.session_id, s.changes
|
||||||
|
from file_change as s
|
||||||
|
where s.file_id = ?
|
||||||
|
and s.revn > ?
|
||||||
|
order by s.created_at asc")
|
||||||
|
|
||||||
|
(defn- retrieve-lagged-changes
|
||||||
|
[conn params]
|
||||||
|
(->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)])
|
||||||
|
(into [] (comp (map files/decode-row)
|
||||||
|
(map (fn [row]
|
||||||
|
(cond-> row
|
||||||
|
(= (:revn row) (:revn (:file params)))
|
||||||
|
(assoc :changes []))))))))
|
||||||
|
|
||||||
(defn- send-notifications
|
(defn- send-notifications
|
||||||
[{:keys [msgbus conn] :as cfg} {:keys [file changes session-id] :as params}]
|
[{:keys [msgbus conn] :as cfg} {:keys [file changes session-id] :as params}]
|
||||||
(let [lchanges (filter library-change? changes)]
|
(let [lchanges (filter library-change? changes)]
|
||||||
|
@ -362,17 +401,24 @@
|
||||||
[conn project-id]
|
[conn project-id]
|
||||||
(:team-id (db/get-by-id conn :project project-id {:columns [:team-id]})))
|
(:team-id (db/get-by-id conn :project project-id {:columns [:team-id]})))
|
||||||
|
|
||||||
(def ^:private
|
|
||||||
sql:lagged-changes
|
|
||||||
"select s.id, s.revn, s.file_id,
|
|
||||||
s.session_id, s.changes
|
|
||||||
from file_change as s
|
|
||||||
where s.file_id = ?
|
|
||||||
and s.revn > ?
|
|
||||||
order by s.created_at asc")
|
|
||||||
|
|
||||||
(defn- retrieve-lagged-changes
|
;; TEMPORARY FILE CREATION
|
||||||
[conn params]
|
|
||||||
(->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)])
|
|
||||||
(mapv files/decode-row)))
|
|
||||||
|
|
||||||
|
(s/def ::create-temp-file ::create-file)
|
||||||
|
|
||||||
|
(sv/defmethod ::create-temp-file
|
||||||
|
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(proj/check-edition-permissions! conn profile-id project-id)
|
||||||
|
(create-file conn (assoc params :deleted-at (dt/in-future {:days 1})))))
|
||||||
|
|
||||||
|
(s/def ::persist-temp-file
|
||||||
|
(s/keys :req-un [::id ::profile-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::persist-temp-file
|
||||||
|
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(files/check-edition-permissions! conn profile-id id)
|
||||||
|
(db/update! conn :file
|
||||||
|
{:deleted-at nil}
|
||||||
|
{:id id})))
|
||||||
|
|
|
@ -104,21 +104,10 @@
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(teams/check-edition-permissions! conn profile-id team-id)
|
(teams/check-edition-permissions! conn profile-id team-id)
|
||||||
|
|
||||||
(let [items (db/query conn :team-font-variant
|
|
||||||
{:font-id id :team-id team-id}
|
|
||||||
{:for-update true})]
|
|
||||||
(doseq [item items]
|
|
||||||
;; Schedule object deletion
|
|
||||||
(wrk/submit! {::wrk/task :delete-object
|
|
||||||
::wrk/delay cf/deletion-delay
|
|
||||||
::wrk/conn conn
|
|
||||||
:id (:id item)
|
|
||||||
:type :team-font-variant}))
|
|
||||||
|
|
||||||
(db/update! conn :team-font-variant
|
(db/update! conn :team-font-variant
|
||||||
{:deleted-at (dt/now)}
|
{:deleted-at (dt/now)}
|
||||||
{:font-id id :team-id team-id})
|
{:font-id id :team-id team-id})
|
||||||
nil)))
|
nil))
|
||||||
|
|
||||||
;; --- DELETE FONT VARIANT
|
;; --- DELETE FONT VARIANT
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,10 @@
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.rpc.mutations.profile :refer [login-or-register]]
|
[app.db :as db]
|
||||||
|
[app.loggers.audit :as audit]
|
||||||
|
[app.rpc.mutations.profile :as profile-m]
|
||||||
|
[app.rpc.queries.profile :as profile-q]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clj-ldap.client :as ldap]
|
[clj-ldap.client :as ldap]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
@ -34,6 +37,7 @@
|
||||||
;; --- Mutation: login-with-ldap
|
;; --- Mutation: login-with-ldap
|
||||||
|
|
||||||
(declare authenticate)
|
(declare authenticate)
|
||||||
|
(declare login-or-register)
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
(s/def ::email ::us/email)
|
||||||
(s/def ::password ::us/string)
|
(s/def ::password ::us/string)
|
||||||
|
@ -44,12 +48,15 @@
|
||||||
:opt-un [::invitation-token]))
|
:opt-un [::invitation-token]))
|
||||||
|
|
||||||
(sv/defmethod ::login-with-ldap {:auth false :rlimit :password}
|
(sv/defmethod ::login-with-ldap {:auth false :rlimit :password}
|
||||||
[{:keys [pool session tokens] :as cfg} {:keys [email password invitation-token] :as params}]
|
[{:keys [pool session tokens] :as cfg} params]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
(let [info (authenticate params)
|
(let [info (authenticate params)
|
||||||
cfg (assoc cfg :conn pool)]
|
cfg (assoc cfg :conn conn)]
|
||||||
|
|
||||||
(when-not info
|
(when-not info
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :wrong-credentials))
|
:code :wrong-credentials))
|
||||||
|
|
||||||
(let [profile (login-or-register cfg {:email (:email info)
|
(let [profile (login-or-register cfg {:email (:email info)
|
||||||
:backend (:backend info)
|
:backend (:backend info)
|
||||||
:fullname (:fullname info)})]
|
:fullname (:fullname info)})]
|
||||||
|
@ -63,12 +70,15 @@
|
||||||
:member-id (:id profile)
|
:member-id (:id profile)
|
||||||
:member-email (:email profile))
|
:member-email (:email profile))
|
||||||
token (tokens :generate claims)]
|
token (tokens :generate claims)]
|
||||||
(with-meta
|
(with-meta {:invitation-token token}
|
||||||
{:invitation-token token}
|
{:transform-response ((:create session) (:id profile))
|
||||||
{:transform-response ((:create session) (:id profile))}))
|
::audit/props (:props profile)
|
||||||
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
(with-meta profile
|
(with-meta profile
|
||||||
{:transform-response ((:create session) (:id profile))})))))
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/props (:props profile)
|
||||||
|
::audit/profile-id (:id profile)}))))))
|
||||||
|
|
||||||
(defn- replace-several [s & {:as replacements}]
|
(defn- replace-several [s & {:as replacements}]
|
||||||
(reduce-kv clojure.string/replace s replacements))
|
(reduce-kv clojure.string/replace s replacements))
|
||||||
|
@ -88,11 +98,25 @@
|
||||||
(first (ldap/search cpool base-dn params))))
|
(first (ldap/search cpool base-dn params))))
|
||||||
|
|
||||||
(defn- authenticate
|
(defn- authenticate
|
||||||
[{:keys [password] :as params}]
|
[{:keys [password email] :as params}]
|
||||||
(with-open [conn (connect)]
|
(with-open [conn (connect)]
|
||||||
(when-let [{:keys [dn] :as luser} (get-ldap-user conn params)]
|
(when-let [{:keys [dn] :as luser} (get-ldap-user conn params)]
|
||||||
(when (ldap/bind? conn dn password)
|
(when (ldap/bind? conn dn password)
|
||||||
{:photo (get luser (keyword (cfg/get :ldap-attrs-photo)))
|
{:photo (get luser (keyword (cfg/get :ldap-attrs-photo)))
|
||||||
:fullname (get luser (keyword (cfg/get :ldap-attrs-fullname)))
|
:fullname (get luser (keyword (cfg/get :ldap-attrs-fullname)))
|
||||||
:email (get luser (keyword (cfg/get :ldap-attrs-email)))
|
:email email
|
||||||
:backend "ldap"}))))
|
:backend "ldap"}))))
|
||||||
|
|
||||||
|
(defn- login-or-register
|
||||||
|
[{:keys [conn] :as cfg} info]
|
||||||
|
(or (some->> (:email info)
|
||||||
|
(profile-q/retrieve-profile-data-by-email conn)
|
||||||
|
(profile-q/populate-additional-data conn)
|
||||||
|
(profile-q/decode-profile-row))
|
||||||
|
(let [params (-> info
|
||||||
|
(assoc :is-active true)
|
||||||
|
(assoc :is-demo false))]
|
||||||
|
(->> params
|
||||||
|
(profile-m/create-profile conn)
|
||||||
|
(profile-m/create-profile-relations conn)
|
||||||
|
(profile-q/strip-private-attrs)))))
|
||||||
|
|
|
@ -167,7 +167,7 @@
|
||||||
:opt-un [::name]))
|
:opt-un [::name]))
|
||||||
|
|
||||||
(sv/defmethod ::duplicate-file
|
(sv/defmethod ::duplicate-file
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id name] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [file (db/get-by-id conn :file file-id)
|
(let [file (db/get-by-id conn :file file-id)
|
||||||
index {file-id (uuid/next)}
|
index {file-id (uuid/next)}
|
||||||
|
@ -187,7 +187,7 @@
|
||||||
:opt-un [::name]))
|
:opt-un [::name]))
|
||||||
|
|
||||||
(sv/defmethod ::duplicate-project
|
(sv/defmethod ::duplicate-project
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id name] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [project (db/get-by-id conn :project project-id)]
|
(let [project (db/get-by-id conn :project project-id)]
|
||||||
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
|
|
||||||
|
|
||||||
(defn create-file-media-object
|
(defn create-file-media-object
|
||||||
[{:keys [conn storage] :as cfg} {:keys [file-id is-local name content] :as params}]
|
[{:keys [conn storage] :as cfg} {:keys [id file-id is-local name content] :as params}]
|
||||||
(media/validate-media-type (:content-type content))
|
(media/validate-media-type (:content-type content))
|
||||||
(let [storage (assoc storage :conn conn)
|
(let [storage (assoc storage :conn conn)
|
||||||
source-path (fs/path (:tempfile content))
|
source-path (fs/path (:tempfile content))
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
(sto/put-object storage {:content (sto/content (:data thumb) (:size thumb))
|
(sto/put-object storage {:content (sto/content (:data thumb) (:size thumb))
|
||||||
:content-type (:mtype thumb)}))]
|
:content-type (:mtype thumb)}))]
|
||||||
(db/insert! conn :file-media-object
|
(db/insert! conn :file-media-object
|
||||||
{:id (uuid/next)
|
{:id (or id (uuid/next))
|
||||||
:file-id file-id
|
:file-id file-id
|
||||||
:is-local is-local
|
:is-local is-local
|
||||||
:name name
|
:name name
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
[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 wrk]
|
|
||||||
[buddy.hashers :as hashers]
|
[buddy.hashers :as hashers]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
@ -37,106 +36,14 @@
|
||||||
(s/def ::password ::us/not-empty-string)
|
(s/def ::password ::us/not-empty-string)
|
||||||
(s/def ::old-password ::us/not-empty-string)
|
(s/def ::old-password ::us/not-empty-string)
|
||||||
(s/def ::theme ::us/string)
|
(s/def ::theme ::us/string)
|
||||||
|
(s/def ::invitation-token ::us/not-empty-string)
|
||||||
;; --- Mutation: Register Profile
|
|
||||||
|
|
||||||
(declare annotate-profile-register)
|
(declare annotate-profile-register)
|
||||||
(declare check-profile-existence!)
|
(declare check-profile-existence!)
|
||||||
(declare create-profile)
|
(declare create-profile)
|
||||||
(declare create-profile-relations)
|
(declare create-profile-relations)
|
||||||
(declare email-domain-in-whitelist?)
|
|
||||||
(declare register-profile)
|
(declare register-profile)
|
||||||
|
|
||||||
(s/def ::invitation-token ::us/not-empty-string)
|
|
||||||
(s/def ::terms-privacy ::us/boolean)
|
|
||||||
|
|
||||||
(s/def ::register-profile
|
|
||||||
(s/keys :req-un [::email ::password ::fullname ::terms-privacy]
|
|
||||||
:opt-un [::invitation-token]))
|
|
||||||
|
|
||||||
(sv/defmethod ::register-profile {:auth false :rlimit :password}
|
|
||||||
[{:keys [pool tokens session] :as cfg} params]
|
|
||||||
(when-not (cfg/get :registration-enabled)
|
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code :registration-disabled))
|
|
||||||
|
|
||||||
(when-let [domains (cfg/get :registration-domain-whitelist)]
|
|
||||||
(when-not (email-domain-in-whitelist? domains (:email params))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-domain-is-not-allowed)))
|
|
||||||
|
|
||||||
(when-not (:terms-privacy params)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :invalid-terms-and-privacy))
|
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [cfg (assoc cfg :conn conn)]
|
|
||||||
(register-profile cfg params))))
|
|
||||||
|
|
||||||
(defn- annotate-profile-register
|
|
||||||
"A helper for properly increase the profile-register metric once the
|
|
||||||
transaction is completed."
|
|
||||||
[metrics profile]
|
|
||||||
(fn []
|
|
||||||
(when (::created profile)
|
|
||||||
((get-in metrics [:definitions :profile-register]) :inc))))
|
|
||||||
|
|
||||||
(defn- register-profile
|
|
||||||
[{:keys [conn tokens session metrics] :as cfg} params]
|
|
||||||
(check-profile-existence! conn params)
|
|
||||||
(let [profile (->> (create-profile conn params)
|
|
||||||
(create-profile-relations conn))
|
|
||||||
profile (assoc profile ::created true)]
|
|
||||||
|
|
||||||
(sid/load-initial-project! conn profile)
|
|
||||||
|
|
||||||
(if-let [token (:invitation-token params)]
|
|
||||||
;; If invitation token comes in params, this is because the
|
|
||||||
;; user comes from team-invitation process; in this case,
|
|
||||||
;; regenerate token and send back to the user a new invitation
|
|
||||||
;; token (and mark current session as logged).
|
|
||||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
|
||||||
claims (assoc claims
|
|
||||||
:member-id (:id profile)
|
|
||||||
:member-email (:email profile))
|
|
||||||
token (tokens :generate claims)
|
|
||||||
resp {:invitation-token token}]
|
|
||||||
(with-meta resp
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
:before-complete (annotate-profile-register metrics profile)
|
|
||||||
::audit/props (:props profile)
|
|
||||||
::audit/profile-id (:id profile)}))
|
|
||||||
|
|
||||||
;; If no token is provided, send a verification email
|
|
||||||
(let [vtoken (tokens :generate
|
|
||||||
{:iss :verify-email
|
|
||||||
:exp (dt/in-future "48h")
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:email (:email profile)})
|
|
||||||
ptoken (tokens :generate-predefined
|
|
||||||
{:iss :profile-identity
|
|
||||||
:profile-id (:id profile)})]
|
|
||||||
|
|
||||||
;; Don't allow proceed in register page if the email is
|
|
||||||
;; already reported as permanent bounced
|
|
||||||
(when (eml/has-bounce-reports? conn (:email profile))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-has-permanent-bounces
|
|
||||||
:hint "looks like the email has one or many bounces reported"))
|
|
||||||
|
|
||||||
(eml/send! {::eml/conn conn
|
|
||||||
::eml/factory eml/register
|
|
||||||
:public-uri (:public-uri cfg)
|
|
||||||
:to (:email profile)
|
|
||||||
:name (:fullname profile)
|
|
||||||
:token vtoken
|
|
||||||
:extra-data ptoken})
|
|
||||||
|
|
||||||
(with-meta profile
|
|
||||||
{:before-complete (annotate-profile-register metrics profile)
|
|
||||||
::audit/props (:props profile)
|
|
||||||
::audit/profile-id (:id profile)})))))
|
|
||||||
|
|
||||||
(defn email-domain-in-whitelist?
|
(defn email-domain-in-whitelist?
|
||||||
"Returns true if email's domain is in the given whitelist or if
|
"Returns true if email's domain is in the given whitelist or if
|
||||||
given whitelist is an empty string."
|
given whitelist is an empty string."
|
||||||
|
@ -178,28 +85,176 @@
|
||||||
{:update false
|
{:update false
|
||||||
:valid false})))
|
:valid false})))
|
||||||
|
|
||||||
|
(defn decode-profile-row
|
||||||
|
[{:keys [props] :as profile}]
|
||||||
|
(cond-> profile
|
||||||
|
(db/pgobject? props "jsonb")
|
||||||
|
(assoc :props (db/decode-transit-pgobject props))))
|
||||||
|
|
||||||
|
;; --- MUTATION: Prepare Register
|
||||||
|
|
||||||
|
(s/def ::prepare-register-profile
|
||||||
|
(s/keys :req-un [::email ::password]
|
||||||
|
:opt-un [::invitation-token]))
|
||||||
|
|
||||||
|
(sv/defmethod ::prepare-register-profile {:auth false}
|
||||||
|
[{:keys [pool tokens] :as cfg} params]
|
||||||
|
(when-not (cfg/get :registration-enabled)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :registration-disabled))
|
||||||
|
|
||||||
|
(when-let [domains (cfg/get :registration-domain-whitelist)]
|
||||||
|
(when-not (email-domain-in-whitelist? domains (:email params))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-domain-is-not-allowed)))
|
||||||
|
|
||||||
|
;; Don't allow proceed in preparing registration if the profile is
|
||||||
|
;; already reported as spamer.
|
||||||
|
(when (eml/has-bounce-reports? pool (:email params))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:hint "looks like the email has one or many bounces reported"))
|
||||||
|
|
||||||
|
(check-profile-existence! pool params)
|
||||||
|
|
||||||
|
(let [params (assoc params
|
||||||
|
:backend "penpot"
|
||||||
|
:iss :prepared-register
|
||||||
|
:exp (dt/in-future "48h"))
|
||||||
|
token (tokens :generate params)]
|
||||||
|
{:token token}))
|
||||||
|
|
||||||
|
;; --- MUTATION: Register Profile
|
||||||
|
|
||||||
|
(s/def ::accept-terms-and-privacy ::us/boolean)
|
||||||
|
(s/def ::accept-newsletter-subscription ::us/boolean)
|
||||||
|
(s/def ::token ::us/not-empty-string)
|
||||||
|
|
||||||
|
(s/def ::register-profile
|
||||||
|
(s/keys :req-un [::token ::fullname
|
||||||
|
::accept-terms-and-privacy]
|
||||||
|
:opt-un [::accept-newsletter-subscription]))
|
||||||
|
|
||||||
|
(sv/defmethod ::register-profile {:auth false :rlimit :password}
|
||||||
|
[{:keys [pool] :as cfg} params]
|
||||||
|
(when-not (:accept-terms-and-privacy params)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :invalid-terms-and-privacy))
|
||||||
|
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [cfg (assoc cfg :conn conn)]
|
||||||
|
(register-profile cfg params))))
|
||||||
|
|
||||||
|
(defn- annotate-profile-register
|
||||||
|
"A helper for properly increase the profile-register metric once the
|
||||||
|
transaction is completed."
|
||||||
|
[metrics]
|
||||||
|
(fn []
|
||||||
|
((get-in metrics [:definitions :profile-register]) :inc)))
|
||||||
|
|
||||||
|
(defn register-profile
|
||||||
|
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]
|
||||||
|
(let [claims (tokens :verify {:token token :iss :prepared-register})
|
||||||
|
params (merge params claims)]
|
||||||
|
(check-profile-existence! conn params)
|
||||||
|
(let [profile (->> params
|
||||||
|
(create-profile conn)
|
||||||
|
(create-profile-relations conn)
|
||||||
|
(decode-profile-row))]
|
||||||
|
(sid/load-initial-project! conn profile)
|
||||||
|
|
||||||
|
(cond
|
||||||
|
;; If invitation token comes in params, this is because the
|
||||||
|
;; user comes from team-invitation process; in this case,
|
||||||
|
;; regenerate token and send back to the user a new invitation
|
||||||
|
;; token (and mark current session as logged).
|
||||||
|
(some? (:invitation-token params))
|
||||||
|
(let [token (:invitation-token params)
|
||||||
|
claims (tokens :verify {:token token :iss :team-invitation})
|
||||||
|
claims (assoc claims
|
||||||
|
:member-id (:id profile)
|
||||||
|
:member-email (:email profile))
|
||||||
|
token (tokens :generate claims)
|
||||||
|
resp {:invitation-token token}]
|
||||||
|
(with-meta resp
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
:before-complete (annotate-profile-register metrics)
|
||||||
|
::audit/props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
|
;; If auth backend is different from "penpot" means user is
|
||||||
|
;; registring using third party auth mechanism; in this case
|
||||||
|
;; we need to mark this session as logged.
|
||||||
|
(not= "penpot" (:auth-backend profile))
|
||||||
|
(with-meta (profile/strip-private-attrs profile)
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
:before-complete (annotate-profile-register metrics)
|
||||||
|
::audit/props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)})
|
||||||
|
|
||||||
|
;; In all other cases, send a verification email.
|
||||||
|
:else
|
||||||
|
(let [vtoken (tokens :generate
|
||||||
|
{:iss :verify-email
|
||||||
|
:exp (dt/in-future "48h")
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:email (:email profile)})
|
||||||
|
ptoken (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})]
|
||||||
|
|
||||||
|
(eml/send! {::eml/conn conn
|
||||||
|
::eml/factory eml/register
|
||||||
|
:public-uri (:public-uri cfg)
|
||||||
|
:to (:email profile)
|
||||||
|
:name (:fullname profile)
|
||||||
|
:token vtoken
|
||||||
|
:extra-data ptoken})
|
||||||
|
|
||||||
|
(with-meta profile
|
||||||
|
{:before-complete (annotate-profile-register metrics)
|
||||||
|
::audit/props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)}))))))
|
||||||
|
|
||||||
(defn create-profile
|
(defn create-profile
|
||||||
"Create the profile entry on the database with limited input
|
"Create the profile entry on the database with limited input filling
|
||||||
filling all the other fields with defaults."
|
all the other fields with defaults."
|
||||||
[conn {:keys [id fullname email password is-active is-muted is-demo opts]
|
[conn params]
|
||||||
:or {is-active false is-muted false is-demo false}
|
(let [id (or (:id params) (uuid/next))
|
||||||
:as params}]
|
|
||||||
(let [id (or id (uuid/next))
|
props (-> (extract-props params)
|
||||||
is-active (if is-demo true is-active)
|
(merge (:props params))
|
||||||
props (-> params extract-props db/tjson)
|
(assoc :accept-terms-and-privacy (:accept-terms-and-privacy params true))
|
||||||
password (derive-password password)
|
(assoc :accept-newsletter-subscription (:accept-newsletter-subscription params false))
|
||||||
|
(db/tjson))
|
||||||
|
|
||||||
|
password (if-let [password (:password params)]
|
||||||
|
(derive-password password)
|
||||||
|
"!")
|
||||||
|
|
||||||
|
locale (as-> (:locale params) locale
|
||||||
|
(and (string? locale) (not (str/blank? locale)) locale))
|
||||||
|
|
||||||
|
backend (:backend params "penpot")
|
||||||
|
is-demo (:is-demo params false)
|
||||||
|
is-muted (:is-muted params false)
|
||||||
|
is-active (:is-active params (or (not= "penpot" backend) is-demo))
|
||||||
|
email (str/lower (:email params))
|
||||||
|
|
||||||
params {:id id
|
params {:id id
|
||||||
:fullname fullname
|
:fullname (:fullname params)
|
||||||
:email (str/lower email)
|
:email email
|
||||||
:auth-backend "penpot"
|
:auth-backend backend
|
||||||
|
:lang locale
|
||||||
:password password
|
:password password
|
||||||
|
:deleted-at (:deleted-at params)
|
||||||
:props props
|
:props props
|
||||||
:is-active is-active
|
:is-active is-active
|
||||||
:is-muted is-muted
|
:is-muted is-muted
|
||||||
:is-demo is-demo}]
|
:is-demo is-demo}]
|
||||||
(try
|
(try
|
||||||
(-> (db/insert! conn :profile params opts)
|
(-> (db/insert! conn :profile params)
|
||||||
(update :props db/decode-transit-pgobject))
|
(decode-profile-row))
|
||||||
(catch org.postgresql.util.PSQLException e
|
(catch org.postgresql.util.PSQLException e
|
||||||
(let [state (.getSQLState e)]
|
(let [state (.getSQLState e)]
|
||||||
(if (not= state "23505")
|
(if (not= state "23505")
|
||||||
|
@ -231,7 +286,7 @@
|
||||||
(assoc :default-team-id (:id team))
|
(assoc :default-team-id (:id team))
|
||||||
(assoc :default-project-id (:id project)))))
|
(assoc :default-project-id (:id project)))))
|
||||||
|
|
||||||
;; --- Mutation: Login
|
;; --- MUTATION: Login
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
(s/def ::email ::us/email)
|
||||||
(s/def ::scope ::us/string)
|
(s/def ::scope ::us/string)
|
||||||
|
@ -241,7 +296,7 @@
|
||||||
:opt-un [::scope ::invitation-token]))
|
:opt-un [::scope ::invitation-token]))
|
||||||
|
|
||||||
(sv/defmethod ::login {:auth false :rlimit :password}
|
(sv/defmethod ::login {:auth false :rlimit :password}
|
||||||
[{:keys [pool session tokens] :as cfg} {:keys [email password scope] :as params}]
|
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
|
||||||
(letfn [(check-password [profile password]
|
(letfn [(check-password [profile password]
|
||||||
(when (= (:password profile) "!")
|
(when (= (:password profile) "!")
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
|
@ -264,7 +319,8 @@
|
||||||
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
|
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
|
||||||
(validate-profile)
|
(validate-profile)
|
||||||
(profile/strip-private-attrs)
|
(profile/strip-private-attrs)
|
||||||
(profile/populate-additional-data conn))]
|
(profile/populate-additional-data conn)
|
||||||
|
(decode-profile-row))]
|
||||||
(if-let [token (:invitation-token params)]
|
(if-let [token (:invitation-token params)]
|
||||||
;; If the request comes with an invitation token, this means
|
;; If the request comes with an invitation token, this means
|
||||||
;; that user wants to accept it with different user. A very
|
;; that user wants to accept it with different user. A very
|
||||||
|
@ -279,90 +335,25 @@
|
||||||
token (tokens :generate claims)]
|
token (tokens :generate claims)]
|
||||||
(with-meta {:invitation-token token}
|
(with-meta {:invitation-token token}
|
||||||
{:transform-response ((:create session) (:id profile))
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/props (audit/profile->props profile)
|
||||||
::audit/profile-id (:id profile)}))
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
(with-meta profile
|
(with-meta profile
|
||||||
{:transform-response ((:create session) (:id profile))
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/props (audit/profile->props profile)
|
||||||
::audit/profile-id (:id profile)}))))))
|
::audit/profile-id (:id profile)}))))))
|
||||||
|
|
||||||
;; --- Mutation: Logout
|
;; --- MUTATION: Logout
|
||||||
|
|
||||||
(s/def ::logout
|
(s/def ::logout
|
||||||
(s/keys :req-un [::profile-id]))
|
(s/keys :req-un [::profile-id]))
|
||||||
|
|
||||||
(sv/defmethod ::logout
|
(sv/defmethod ::logout
|
||||||
[{:keys [pool session] :as cfg} {:keys [profile-id] :as params}]
|
[{:keys [session] :as cfg} _]
|
||||||
(with-meta {}
|
(with-meta {}
|
||||||
{:transform-response (:delete session)}))
|
{:transform-response (:delete session)}))
|
||||||
|
|
||||||
|
;; --- MUTATION: Update Profile (own)
|
||||||
;; --- Mutation: Register if not exists
|
|
||||||
|
|
||||||
(declare login-or-register)
|
|
||||||
|
|
||||||
(s/def ::backend ::us/string)
|
|
||||||
(s/def ::login-or-register
|
|
||||||
(s/keys :req-un [::email ::fullname ::backend]))
|
|
||||||
|
|
||||||
(sv/defmethod ::login-or-register {:auth false}
|
|
||||||
[{:keys [pool metrics] :as cfg} params]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [profile (-> (assoc cfg :conn conn)
|
|
||||||
(login-or-register params))
|
|
||||||
props (merge
|
|
||||||
(select-keys profile [:backend :fullname :email])
|
|
||||||
(:props profile))]
|
|
||||||
(with-meta profile
|
|
||||||
{:before-complete (annotate-profile-register metrics profile)
|
|
||||||
::audit/name (if (::created profile) "register" "login")
|
|
||||||
::audit/props props
|
|
||||||
::audit/profile-id (:id profile)}))))
|
|
||||||
|
|
||||||
(defn login-or-register
|
|
||||||
[{:keys [conn] :as cfg} {:keys [email] :as params}]
|
|
||||||
(letfn [(info->lang [{:keys [locale] :as info}]
|
|
||||||
(when (and (string? locale)
|
|
||||||
(not (str/blank? locale)))
|
|
||||||
locale))
|
|
||||||
|
|
||||||
(create-profile [conn {:keys [fullname backend email props] :as info}]
|
|
||||||
(let [params {:id (uuid/next)
|
|
||||||
:fullname fullname
|
|
||||||
:email (str/lower email)
|
|
||||||
:lang (info->lang props)
|
|
||||||
:auth-backend backend
|
|
||||||
:is-active true
|
|
||||||
:password "!"
|
|
||||||
:props (db/tjson props)
|
|
||||||
:is-demo false}]
|
|
||||||
(-> (db/insert! conn :profile params)
|
|
||||||
(update :props db/decode-transit-pgobject))))
|
|
||||||
|
|
||||||
(update-profile [conn info profile]
|
|
||||||
(let [props (merge (:props profile)
|
|
||||||
(:props info))]
|
|
||||||
(db/update! conn :profile
|
|
||||||
{:props (db/tjson props)
|
|
||||||
:modified-at (dt/now)}
|
|
||||||
{:id (:id profile)})
|
|
||||||
(assoc profile :props props)))
|
|
||||||
|
|
||||||
(register-profile [conn params]
|
|
||||||
(let [profile (->> (create-profile conn params)
|
|
||||||
(create-profile-relations conn))]
|
|
||||||
(sid/load-initial-project! conn profile)
|
|
||||||
(assoc profile ::created true)))]
|
|
||||||
|
|
||||||
(let [profile (profile/retrieve-profile-data-by-email conn email)
|
|
||||||
profile (if profile
|
|
||||||
(->> profile
|
|
||||||
(update-profile conn params)
|
|
||||||
(profile/populate-additional-data conn))
|
|
||||||
(register-profile conn params))]
|
|
||||||
(profile/strip-private-attrs profile))))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Update Profile (own)
|
|
||||||
|
|
||||||
(defn- update-profile
|
(defn- update-profile
|
||||||
[conn {:keys [id fullname lang theme] :as params}]
|
[conn {:keys [id fullname lang theme] :as params}]
|
||||||
|
@ -382,7 +373,7 @@
|
||||||
(update-profile conn params)
|
(update-profile conn params)
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
;; --- Mutation: Update Password
|
;; --- MUTATION: Update Password
|
||||||
|
|
||||||
(declare validate-password!)
|
(declare validate-password!)
|
||||||
(declare update-profile-password!)
|
(declare update-profile-password!)
|
||||||
|
@ -391,7 +382,7 @@
|
||||||
(s/keys :req-un [::profile-id ::password ::old-password]))
|
(s/keys :req-un [::profile-id ::password ::old-password]))
|
||||||
|
|
||||||
(sv/defmethod ::update-profile-password {:rlimit :password}
|
(sv/defmethod ::update-profile-password {:rlimit :password}
|
||||||
[{:keys [pool] :as cfg} {:keys [password profile-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [password] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [profile (validate-password! conn params)]
|
(let [profile (validate-password! conn params)]
|
||||||
(update-profile-password! conn (assoc profile :password password))
|
(update-profile-password! conn (assoc profile :password password))
|
||||||
|
@ -411,7 +402,7 @@
|
||||||
{:password (derive-password password)}
|
{:password (derive-password password)}
|
||||||
{:id id}))
|
{:id id}))
|
||||||
|
|
||||||
;; --- Mutation: Update Photo
|
;; --- MUTATION: Update Photo
|
||||||
|
|
||||||
(declare update-profile-photo)
|
(declare update-profile-photo)
|
||||||
|
|
||||||
|
@ -446,7 +437,7 @@
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Request Email Change
|
;; --- MUTATION: Request Email Change
|
||||||
|
|
||||||
(declare request-email-change)
|
(declare request-email-change)
|
||||||
(declare change-email-inmediatelly)
|
(declare change-email-inmediatelly)
|
||||||
|
@ -514,7 +505,7 @@
|
||||||
[conn id]
|
[conn id]
|
||||||
(db/get-by-id conn :profile id {:for-update true}))
|
(db/get-by-id conn :profile id {:for-update true}))
|
||||||
|
|
||||||
;; --- Mutation: Request Profile Recovery
|
;; --- MUTATION: Request Profile Recovery
|
||||||
|
|
||||||
(s/def ::request-profile-recovery
|
(s/def ::request-profile-recovery
|
||||||
(s/keys :req-un [::email]))
|
(s/keys :req-un [::email]))
|
||||||
|
@ -563,7 +554,7 @@
|
||||||
(send-email-notification conn))))))
|
(send-email-notification conn))))))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Recover Profile
|
;; --- MUTATION: Recover Profile
|
||||||
|
|
||||||
(s/def ::token ::us/not-empty-string)
|
(s/def ::token ::us/not-empty-string)
|
||||||
(s/def ::recover-profile
|
(s/def ::recover-profile
|
||||||
|
@ -584,7 +575,7 @@
|
||||||
(update-password conn))
|
(update-password conn))
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
;; --- Mutation: Update Profile Props
|
;; --- MUTATION: Update Profile Props
|
||||||
|
|
||||||
(s/def ::props map?)
|
(s/def ::props map?)
|
||||||
(s/def ::update-profile-props
|
(s/def ::update-profile-props
|
||||||
|
@ -606,7 +597,7 @@
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Delete Profile
|
;; --- MUTATION: Delete Profile
|
||||||
|
|
||||||
(declare check-can-delete-profile!)
|
(declare check-can-delete-profile!)
|
||||||
(declare mark-profile-as-deleted!)
|
(declare mark-profile-as-deleted!)
|
||||||
|
@ -619,12 +610,6 @@
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-can-delete-profile! conn profile-id)
|
(check-can-delete-profile! conn profile-id)
|
||||||
|
|
||||||
;; Schedule a complete deletion of profile
|
|
||||||
(wrk/submit! {::wrk/task :delete-profile
|
|
||||||
::wrk/delay cfg/deletion-delay
|
|
||||||
::wrk/conn conn
|
|
||||||
:profile-id profile-id})
|
|
||||||
|
|
||||||
(db/update! conn :profile
|
(db/update! conn :profile
|
||||||
{:deleted-at (dt/now)}
|
{:deleted-at (dt/now)}
|
||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
|
|
|
@ -8,14 +8,12 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cfg]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc.permissions :as perms]
|
[app.rpc.permissions :as perms]
|
||||||
[app.rpc.queries.projects :as proj]
|
[app.rpc.queries.projects :as proj]
|
||||||
[app.rpc.queries.teams :as teams]
|
[app.rpc.queries.teams :as teams]
|
||||||
[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]))
|
||||||
|
|
||||||
;; --- Helpers & Specs
|
;; --- Helpers & Specs
|
||||||
|
@ -123,14 +121,6 @@
|
||||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(proj/check-edition-permissions! conn profile-id id)
|
(proj/check-edition-permissions! conn profile-id id)
|
||||||
|
|
||||||
;; Schedule object deletion
|
|
||||||
(wrk/submit! {::wrk/task :delete-object
|
|
||||||
::wrk/delay cfg/deletion-delay
|
|
||||||
::wrk/conn conn
|
|
||||||
:id id
|
|
||||||
:type :project})
|
|
||||||
|
|
||||||
(db/update! conn :project
|
(db/update! conn :project
|
||||||
{:deleted-at (dt/now)}
|
{:deleted-at (dt/now)}
|
||||||
{:id id})
|
{:id id})
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
[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]
|
||||||
[app.config :as cfg]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.emails :as eml]
|
[app.emails :as eml]
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
|
@ -21,7 +20,6 @@
|
||||||
[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 wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[datoteka.core :as fs]))
|
[datoteka.core :as fs]))
|
||||||
|
|
||||||
|
@ -135,13 +133,6 @@
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :only-owner-can-delete-team))
|
:code :only-owner-can-delete-team))
|
||||||
|
|
||||||
;; Schedule object deletion
|
|
||||||
(wrk/submit! {::wrk/task :delete-object
|
|
||||||
::wrk/delay cfg/deletion-delay
|
|
||||||
::wrk/conn conn
|
|
||||||
:id id
|
|
||||||
:type :team})
|
|
||||||
|
|
||||||
(db/update! conn :team
|
(db/update! conn :team
|
||||||
{:deleted-at (dt/now)}
|
{:deleted-at (dt/now)}
|
||||||
{:id id})
|
{:id id})
|
||||||
|
|
|
@ -9,10 +9,12 @@
|
||||||
[app.common.pages.migrations :as pmg]
|
[app.common.pages.migrations :as pmg]
|
||||||
[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.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc.permissions :as perms]
|
[app.rpc.permissions :as perms]
|
||||||
[app.rpc.queries.projects :as projects]
|
[app.rpc.queries.projects :as projects]
|
||||||
[app.rpc.queries.teams :as teams]
|
[app.rpc.queries.teams :as teams]
|
||||||
|
[app.storage.impl :as simpl]
|
||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
@ -171,9 +173,21 @@
|
||||||
|
|
||||||
;; --- Query: File (By ID)
|
;; --- Query: File (By ID)
|
||||||
|
|
||||||
|
(defn- retrieve-data*
|
||||||
|
[{:keys [storage] :as cfg} file]
|
||||||
|
(when-let [backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))]
|
||||||
|
(simpl/get-object-bytes backend file)))
|
||||||
|
|
||||||
|
(defn retrieve-data
|
||||||
|
[cfg file]
|
||||||
|
(if (bytes? (:data file))
|
||||||
|
file
|
||||||
|
(assoc file :data (retrieve-data* cfg file))))
|
||||||
|
|
||||||
(defn retrieve-file
|
(defn retrieve-file
|
||||||
[conn id]
|
[{:keys [conn] :as cfg} id]
|
||||||
(-> (db/get-by-id conn :file id)
|
(->> (db/get-by-id conn :file id)
|
||||||
|
(retrieve-data cfg)
|
||||||
(decode-row)
|
(decode-row)
|
||||||
(pmg/migrate-file)))
|
(pmg/migrate-file)))
|
||||||
|
|
||||||
|
@ -183,8 +197,9 @@
|
||||||
(sv/defmethod ::file
|
(sv/defmethod ::file
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
|
(let [cfg (assoc cfg :conn conn)]
|
||||||
(check-edition-permissions! conn profile-id id)
|
(check-edition-permissions! conn profile-id id)
|
||||||
(retrieve-file conn id)))
|
(retrieve-file cfg id))))
|
||||||
|
|
||||||
(s/def ::page
|
(s/def ::page
|
||||||
(s/keys :req-un [::profile-id ::file-id]))
|
(s/keys :req-un [::profile-id ::file-id]))
|
||||||
|
@ -217,11 +232,11 @@
|
||||||
(update data :objects update-objects)))
|
(update data :objects update-objects)))
|
||||||
|
|
||||||
(sv/defmethod ::page
|
(sv/defmethod ::page
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id id strip-thumbnails]}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id strip-thumbnails]}]
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id]}]
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(let [file (retrieve-file conn file-id)
|
(let [cfg (assoc cfg :conn conn)
|
||||||
|
file (retrieve-file cfg file-id)
|
||||||
page-id (get-in file [:data :pages 0])]
|
page-id (get-in file [:data :pages 0])]
|
||||||
(cond-> (get-in file [:data :pages-index page-id])
|
(cond-> (get-in file [:data :pages-index page-id])
|
||||||
strip-thumbnails
|
strip-thumbnails
|
||||||
|
@ -245,7 +260,7 @@
|
||||||
(s/keys :req-un [::profile-id ::team-id]))
|
(s/keys :req-un [::profile-id ::team-id]))
|
||||||
|
|
||||||
(sv/defmethod ::shared-files
|
(sv/defmethod ::shared-files
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [team-id] :as params}]
|
||||||
(into [] decode-row-xf (db/exec! pool [sql:shared-files team-id])))
|
(into [] decode-row-xf (db/exec! pool [sql:shared-files team-id])))
|
||||||
|
|
||||||
|
|
||||||
|
@ -270,30 +285,43 @@
|
||||||
(s/keys :req-un [::profile-id ::team-id]))
|
(s/keys :req-un [::profile-id ::team-id]))
|
||||||
|
|
||||||
(sv/defmethod ::team-shared-files
|
(sv/defmethod ::team-shared-files
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [team-id] :as params}]
|
||||||
(db/exec! pool [sql:team-shared-files team-id]))
|
(db/exec! pool [sql:team-shared-files team-id]))
|
||||||
|
|
||||||
|
|
||||||
;; --- Query: File Libraries used by a File
|
;; --- Query: File Libraries used by a File
|
||||||
|
|
||||||
(def ^:private sql:file-libraries
|
(def ^:private sql:file-libraries
|
||||||
"select fl.*,
|
"WITH RECURSIVE libs AS (
|
||||||
|
SELECT fl.*, flr.synced_at
|
||||||
flr.synced_at as synced_at
|
FROM file AS fl
|
||||||
from file as fl
|
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
|
||||||
inner join file_library_rel as flr on (flr.library_file_id = fl.id)
|
WHERE flr.file_id = ?::uuid
|
||||||
where flr.file_id = ?
|
UNION
|
||||||
and fl.deleted_at is null")
|
SELECT fl.*, flr.synced_at
|
||||||
|
FROM file AS fl
|
||||||
|
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
|
||||||
|
JOIN libs AS l ON (flr.file_id = l.id)
|
||||||
|
)
|
||||||
|
SELECT l.id,
|
||||||
|
l.data,
|
||||||
|
l.project_id,
|
||||||
|
l.created_at,
|
||||||
|
l.modified_at,
|
||||||
|
l.deleted_at,
|
||||||
|
l.name,
|
||||||
|
l.revn,
|
||||||
|
l.synced_at
|
||||||
|
FROM libs AS l
|
||||||
|
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||||
|
|
||||||
(defn retrieve-file-libraries
|
(defn retrieve-file-libraries
|
||||||
[conn is-indirect file-id]
|
[{:keys [conn] :as cfg} is-indirect file-id]
|
||||||
(let [libraries (->> (db/exec! conn [sql:file-libraries file-id])
|
(let [xform (comp
|
||||||
(map #(assoc % :is-indirect is-indirect))
|
(map #(assoc % :is-indirect is-indirect))
|
||||||
(into #{} decode-row-xf))]
|
(map #(retrieve-data cfg %))
|
||||||
(reduce #(into %1 (retrieve-file-libraries conn true %2))
|
(map decode-row))]
|
||||||
libraries
|
(into #{} xform (db/exec! conn [sql:file-libraries file-id]))))
|
||||||
(map :id libraries))))
|
|
||||||
|
|
||||||
|
|
||||||
(s/def ::file-libraries
|
(s/def ::file-libraries
|
||||||
(s/keys :req-un [::profile-id ::file-id]))
|
(s/keys :req-un [::profile-id ::file-id]))
|
||||||
|
@ -301,8 +329,9 @@
|
||||||
(sv/defmethod ::file-libraries
|
(sv/defmethod ::file-libraries
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
|
(let [cfg (assoc cfg :conn conn)]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(retrieve-file-libraries conn false file-id)))
|
(retrieve-file-libraries cfg false file-id))))
|
||||||
|
|
||||||
;; --- QUERY: team-recent-files
|
;; --- QUERY: team-recent-files
|
||||||
|
|
||||||
|
@ -334,7 +363,6 @@
|
||||||
(teams/check-read-permissions! conn profile-id team-id)
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
(db/exec! conn [sql:team-recent-files team-id])))
|
(db/exec! conn [sql:team-recent-files team-id])))
|
||||||
|
|
||||||
|
|
||||||
;; --- Helpers
|
;; --- Helpers
|
||||||
|
|
||||||
(defn decode-row
|
(defn decode-row
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
;; --- Helpers & Specs
|
;; --- Helpers & Specs
|
||||||
|
|
||||||
|
@ -72,7 +73,8 @@
|
||||||
(defn decode-profile-row
|
(defn decode-profile-row
|
||||||
[{:keys [props] :as row}]
|
[{:keys [props] :as row}]
|
||||||
(cond-> row
|
(cond-> row
|
||||||
(db/pgobject? props) (assoc :props (db/decode-transit-pgobject props))))
|
(db/pgobject? props "jsonb")
|
||||||
|
(assoc :props (db/decode-transit-pgobject props))))
|
||||||
|
|
||||||
(defn retrieve-profile-data
|
(defn retrieve-profile-data
|
||||||
[conn id]
|
[conn id]
|
||||||
|
@ -90,16 +92,11 @@
|
||||||
|
|
||||||
profile))
|
profile))
|
||||||
|
|
||||||
(def sql:retrieve-profile-by-email
|
|
||||||
"select p.* from profile as p
|
|
||||||
where p.email = lower(?)
|
|
||||||
and p.deleted_at is null")
|
|
||||||
|
|
||||||
(defn retrieve-profile-data-by-email
|
(defn retrieve-profile-data-by-email
|
||||||
[conn email]
|
[conn email]
|
||||||
(let [sql [sql:retrieve-profile-by-email email]]
|
(try
|
||||||
(some-> (db/exec-one! conn sql)
|
(db/get-by-params conn :profile {:email (str/lower email)})
|
||||||
(decode-profile-row))))
|
(catch Exception _e)))
|
||||||
|
|
||||||
;; --- Attrs Helpers
|
;; --- Attrs Helpers
|
||||||
|
|
||||||
|
|
|
@ -42,12 +42,13 @@
|
||||||
(sv/defmethod ::viewer-bundle {:auth false}
|
(sv/defmethod ::viewer-bundle {:auth false}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id page-id token] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id page-id token] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [file (files/retrieve-file conn file-id)
|
(let [cfg (assoc cfg :conn conn)
|
||||||
|
file (files/retrieve-file cfg file-id)
|
||||||
project (retrieve-project conn (:project-id file))
|
project (retrieve-project conn (:project-id file))
|
||||||
page (get-in file [:data :pages-index page-id])
|
page (get-in file [:data :pages-index page-id])
|
||||||
file (merge (dissoc file :data)
|
file (merge (dissoc file :data)
|
||||||
(select-keys (:data file) [:colors :media :typographies]))
|
(select-keys (:data file) [:colors :media :typographies]))
|
||||||
libs (files/retrieve-file-libraries conn false file-id)
|
libs (files/retrieve-file-libraries cfg false file-id)
|
||||||
users (teams/retrieve-users conn (:team-id project))
|
users (teams/retrieve-users conn (:team-id project))
|
||||||
|
|
||||||
fonts (db/query conn :team-font-variant
|
fonts (db/query conn :team-font-variant
|
||||||
|
|
29
backend/src/app/setup/keys.clj
Normal file
29
backend/src/app/setup/keys.clj
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.setup.keys
|
||||||
|
"Keys derivation service."
|
||||||
|
(:require
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[buddy.core.kdf :as bk]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
(s/def ::secret-key ::us/string)
|
||||||
|
(s/def ::props (s/keys :req-un [::secret-key]))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec :app.setup/keys [_]
|
||||||
|
(s/keys :req-un [::props]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key :app.setup/keys
|
||||||
|
[_ {:keys [props] :as cfg}]
|
||||||
|
(fn [& {:keys [salt _]}]
|
||||||
|
(let [engine (bk/engine {:key (:secret-key props)
|
||||||
|
:salt salt
|
||||||
|
:alg :hkdf
|
||||||
|
:digest :blake2b-512})]
|
||||||
|
(bk/get-bytes engine 32))))
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.srepl.main]
|
[app.srepl.main]
|
||||||
|
[app.util.logging :as l]
|
||||||
[clojure.core.server :as ccs]
|
[clojure.core.server :as ccs]
|
||||||
[clojure.main :as cm]
|
[clojure.main :as cm]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
@ -41,14 +42,17 @@
|
||||||
|
|
||||||
(defmethod ig/init-key ::server
|
(defmethod ig/init-key ::server
|
||||||
[_ {:keys [port host name] :as cfg}]
|
[_ {:keys [port host name] :as cfg}]
|
||||||
|
(when (and port host name)
|
||||||
|
(l/info :msg "initializing server repl" :port port :host host :name name)
|
||||||
(ccs/start-server {:address host
|
(ccs/start-server {:address host
|
||||||
:port port
|
:port port
|
||||||
:name name
|
:name name
|
||||||
:accept 'app.srepl/repl})
|
:accept 'app.srepl/repl})
|
||||||
cfg)
|
cfg))
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::server
|
(defmethod ig/halt-key! ::server
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(ccs/stop-server (:name cfg)))
|
(when cfg
|
||||||
|
(ccs/stop-server (:name cfg))))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.storage
|
(ns app.storage
|
||||||
"File Storage abstraction layer."
|
"Objects storage abstraction layer."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
|
@ -20,13 +20,9 @@
|
||||||
[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]
|
||||||
[cuerdas.core :as str]
|
|
||||||
[datoteka.core :as fs]
|
[datoteka.core :as fs]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[promesa.exec :as px])
|
[promesa.exec :as px]))
|
||||||
(:import
|
|
||||||
java.io.InputStream))
|
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Storage Module State
|
;; Storage Module State
|
||||||
|
@ -39,7 +35,11 @@
|
||||||
(s/def ::db ::sdb/backend)
|
(s/def ::db ::sdb/backend)
|
||||||
|
|
||||||
(s/def ::backends
|
(s/def ::backends
|
||||||
(s/keys :opt-un [::s3 ::fs ::db]))
|
(s/map-of ::us/keyword
|
||||||
|
(s/nilable
|
||||||
|
(s/or :s3 ::ss3/backend
|
||||||
|
:fs ::sfs/backend
|
||||||
|
:db ::sdb/backend))))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::storage [_]
|
(defmethod ig/pre-init-spec ::storage [_]
|
||||||
(s/keys :req-un [::backend ::wrk/executor ::db/pool ::backends]))
|
(s/keys :req-un [::backend ::wrk/executor ::db/pool ::backends]))
|
||||||
|
@ -50,8 +50,9 @@
|
||||||
(assoc :backends (d/without-nils backends))))
|
(assoc :backends (d/without-nils backends))))
|
||||||
|
|
||||||
(defmethod ig/init-key ::storage
|
(defmethod ig/init-key ::storage
|
||||||
[_ cfg]
|
[_ {:keys [backends] :as cfg}]
|
||||||
cfg)
|
(-> (d/without-nils cfg)
|
||||||
|
(assoc :backends (d/without-nils backends))))
|
||||||
|
|
||||||
(s/def ::storage
|
(s/def ::storage
|
||||||
(s/keys :req-un [::backends ::wrk/executor ::db/pool ::backend]))
|
(s/keys :req-un [::backends ::wrk/executor ::db/pool ::backend]))
|
||||||
|
@ -151,8 +152,6 @@
|
||||||
;; API
|
;; API
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(declare resolve-backend)
|
|
||||||
|
|
||||||
(defn object->relative-path
|
(defn object->relative-path
|
||||||
[{:keys [id] :as obj}]
|
[{:keys [id] :as obj}]
|
||||||
(impl/id->path id))
|
(impl/id->path id))
|
||||||
|
@ -185,7 +184,7 @@
|
||||||
(px/run! executor #(register-recheck storage backend (:id object)))
|
(px/run! executor #(register-recheck storage backend (:id object)))
|
||||||
|
|
||||||
;; Store the data finally on the underlying storage subsystem.
|
;; Store the data finally on the underlying storage subsystem.
|
||||||
(-> (resolve-backend storage backend)
|
(-> (impl/resolve-backend storage backend)
|
||||||
(impl/put-object object content))
|
(impl/put-object object content))
|
||||||
|
|
||||||
object))
|
object))
|
||||||
|
@ -201,28 +200,37 @@
|
||||||
;; if the source and destination backends are the same, we
|
;; if the source and destination backends are the same, we
|
||||||
;; proceed to use the fast path with specific copy
|
;; proceed to use the fast path with specific copy
|
||||||
;; implementation on backend.
|
;; implementation on backend.
|
||||||
(-> (resolve-backend storage (:backend storage))
|
(-> (impl/resolve-backend storage (:backend storage))
|
||||||
(impl/copy-object object object*))
|
(impl/copy-object object object*))
|
||||||
|
|
||||||
;; if the source and destination backends are different, we just
|
;; if the source and destination backends are different, we just
|
||||||
;; need to obtain the streams and proceed full copy of the data
|
;; need to obtain the streams and proceed full copy of the data
|
||||||
(with-open [^InputStream input
|
(with-open [is (-> (impl/resolve-backend storage (:backend object))
|
||||||
(-> (resolve-backend storage (:backend object))
|
|
||||||
(impl/get-object-data object))]
|
(impl/get-object-data object))]
|
||||||
(-> (resolve-backend storage (:backend storage))
|
(-> (impl/resolve-backend storage (:backend storage))
|
||||||
(impl/put-object object* (impl/content input (:size object))))))
|
(impl/put-object object* (impl/content is (:size object))))))
|
||||||
|
|
||||||
object*))
|
object*))
|
||||||
|
|
||||||
(defn get-object-data
|
(defn get-object-data
|
||||||
|
"Return an input stream instance of the object content."
|
||||||
[{:keys [pool conn] :as storage} object]
|
[{:keys [pool conn] :as storage} object]
|
||||||
(us/assert ::storage storage)
|
(us/assert ::storage storage)
|
||||||
(when (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)))
|
||||||
(-> (assoc storage :conn (or conn pool))
|
(-> (assoc storage :conn (or conn pool))
|
||||||
(resolve-backend (:backend object))
|
(impl/resolve-backend (:backend object))
|
||||||
(impl/get-object-data object))))
|
(impl/get-object-data object))))
|
||||||
|
|
||||||
|
(defn get-object-bytes
|
||||||
|
"Returns a byte array of object content."
|
||||||
|
[{:keys [pool conn] :as storage} object]
|
||||||
|
(us/assert ::storage storage)
|
||||||
|
(when (or (nil? (:expired-at object))
|
||||||
|
(dt/is-after? (:expired-at object) (dt/now)))
|
||||||
|
(-> (assoc storage :conn (or conn pool))
|
||||||
|
(impl/resolve-backend (:backend object))
|
||||||
|
(impl/get-object-bytes object))))
|
||||||
|
|
||||||
(defn get-object-url
|
(defn get-object-url
|
||||||
([storage object]
|
([storage object]
|
||||||
(get-object-url storage object nil))
|
(get-object-url storage object nil))
|
||||||
|
@ -231,14 +239,14 @@
|
||||||
(when (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)))
|
||||||
(-> (assoc storage :conn (or conn pool))
|
(-> (assoc storage :conn (or conn pool))
|
||||||
(resolve-backend (:backend object))
|
(impl/resolve-backend (:backend object))
|
||||||
(impl/get-object-url object options)))))
|
(impl/get-object-url object options)))))
|
||||||
|
|
||||||
(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
|
||||||
storages."
|
storages."
|
||||||
[storage object]
|
[storage object]
|
||||||
(let [backend (resolve-backend storage (:backend object))]
|
(let [backend (impl/resolve-backend storage (:backend object))]
|
||||||
(when (not= :fs (:type backend))
|
(when (not= :fs (:type backend))
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
:code :operation-not-allowed
|
:code :operation-not-allowed
|
||||||
|
@ -254,16 +262,7 @@
|
||||||
(-> (assoc storage :conn (or conn pool))
|
(-> (assoc storage :conn (or conn pool))
|
||||||
(delete-database-object (if (uuid? id-or-obj) id-or-obj (:id id-or-obj)))))
|
(delete-database-object (if (uuid? id-or-obj) id-or-obj (:id id-or-obj)))))
|
||||||
|
|
||||||
;; --- impl
|
(d/export impl/resolve-backend)
|
||||||
|
|
||||||
(defn resolve-backend
|
|
||||||
[{:keys [conn pool] :as storage} backend-id]
|
|
||||||
(let [backend (get-in storage [:backends backend-id])]
|
|
||||||
(when-not backend
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :backend-not-configured
|
|
||||||
:hint (str/fmt "backend '%s' not configured" backend-id)))
|
|
||||||
(assoc backend :conn (or conn pool))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Garbage Collection: Permanently delete objects
|
;; Garbage Collection: Permanently delete objects
|
||||||
|
@ -295,7 +294,7 @@
|
||||||
(some-> (seq rows) (group-by-backend))))
|
(some-> (seq rows) (group-by-backend))))
|
||||||
|
|
||||||
(delete-in-bulk [conn [backend ids]]
|
(delete-in-bulk [conn [backend ids]]
|
||||||
(let [backend (resolve-backend storage backend)
|
(let [backend (impl/resolve-backend storage backend)
|
||||||
backend (assoc backend :conn conn)]
|
backend (assoc backend :conn conn)]
|
||||||
(impl/del-objects-in-bulk backend ids)))]
|
(impl/del-objects-in-bulk backend ids)))]
|
||||||
|
|
||||||
|
@ -445,7 +444,7 @@
|
||||||
(some-> (seq rows) (group-results))))
|
(some-> (seq rows) (group-results))))
|
||||||
|
|
||||||
(delete-group [conn [backend ids]]
|
(delete-group [conn [backend ids]]
|
||||||
(let [backend (resolve-backend storage backend)
|
(let [backend (impl/resolve-backend storage backend)
|
||||||
backend (assoc backend :conn conn)]
|
backend (assoc backend :conn conn)]
|
||||||
(impl/del-objects-in-bulk backend ids)))
|
(impl/del-objects-in-bulk backend ids)))
|
||||||
|
|
||||||
|
|
|
@ -46,12 +46,24 @@
|
||||||
(let [result (db/exec-one! conn ["select data from storage_data where id=?" id])]
|
(let [result (db/exec-one! conn ["select data from storage_data where id=?" id])]
|
||||||
(ByteArrayInputStream. (:data result))))
|
(ByteArrayInputStream. (:data result))))
|
||||||
|
|
||||||
|
(defmethod impl/get-object-bytes :db
|
||||||
|
[{:keys [conn] :as backend} {:keys [id] :as object}]
|
||||||
|
(let [result (db/exec-one! conn ["select data from storage_data where id=?" id])]
|
||||||
|
(:data result)))
|
||||||
|
|
||||||
(defmethod impl/get-object-url :db
|
(defmethod impl/get-object-url :db
|
||||||
[_ _]
|
[_ _]
|
||||||
(throw (UnsupportedOperationException. "not supported")))
|
(throw (UnsupportedOperationException. "not supported")))
|
||||||
|
|
||||||
|
(defmethod impl/del-object :db
|
||||||
|
[_ _]
|
||||||
|
;; NOOP: because deleting the row already deletes the file data from
|
||||||
|
;; the database.
|
||||||
|
nil)
|
||||||
|
|
||||||
(defmethod impl/del-objects-in-bulk :db
|
(defmethod impl/del-objects-in-bulk :db
|
||||||
[_ _]
|
[_ _]
|
||||||
;; NOOP: because deleting the row already deletes the file data from
|
;; NOOP: because deleting the row already deletes the file data from
|
||||||
;; the database.
|
;; the database.
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,10 @@
|
||||||
:path (str full)))
|
:path (str full)))
|
||||||
(io/input-stream full)))
|
(io/input-stream full)))
|
||||||
|
|
||||||
|
(defmethod impl/get-object-bytes :fs
|
||||||
|
[backend object]
|
||||||
|
(fs/slurp-bytes (impl/get-object-data backend object)))
|
||||||
|
|
||||||
(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} _]
|
||||||
(update uri :path
|
(update uri :path
|
||||||
|
@ -87,6 +91,13 @@
|
||||||
(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
|
||||||
|
[backend {:keys [id] :as object}]
|
||||||
|
(let [base (fs/path (:directory backend))
|
||||||
|
path (fs/path (impl/id->path id))
|
||||||
|
path (fs/join base path)]
|
||||||
|
(Files/deleteIfExists ^Path path)))
|
||||||
|
|
||||||
(defmethod impl/del-objects-in-bulk :fs
|
(defmethod impl/del-objects-in-bulk :fs
|
||||||
[backend ids]
|
[backend ids]
|
||||||
(let [base (fs/path (:directory backend))]
|
(let [base (fs/path (:directory backend))]
|
||||||
|
@ -94,3 +105,4 @@
|
||||||
(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)))))
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,10 @@
|
||||||
"Storage backends abstraction layer."
|
"Storage backends abstraction layer."
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[buddy.core.codecs :as bc]
|
[buddy.core.codecs :as bc]
|
||||||
[clojure.java.io :as io])
|
[clojure.java.io :as io]
|
||||||
|
[cuerdas.core :as str])
|
||||||
(:import
|
(:import
|
||||||
java.nio.ByteBuffer
|
java.nio.ByteBuffer
|
||||||
java.util.UUID
|
java.util.UUID
|
||||||
|
@ -45,6 +45,14 @@
|
||||||
:code :invalid-storage-backend
|
:code :invalid-storage-backend
|
||||||
:context cfg))
|
:context cfg))
|
||||||
|
|
||||||
|
(defmulti get-object-bytes (fn [cfg _] (:type cfg)))
|
||||||
|
|
||||||
|
(defmethod get-object-bytes :default
|
||||||
|
[cfg _]
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :invalid-storage-backend
|
||||||
|
:context cfg))
|
||||||
|
|
||||||
(defmulti get-object-url (fn [cfg _ _] (:type cfg)))
|
(defmulti get-object-url (fn [cfg _ _] (:type cfg)))
|
||||||
|
|
||||||
(defmethod get-object-url :default
|
(defmethod get-object-url :default
|
||||||
|
@ -54,6 +62,14 @@
|
||||||
:context cfg))
|
:context cfg))
|
||||||
|
|
||||||
|
|
||||||
|
(defmulti del-object (fn [cfg _] (:type cfg)))
|
||||||
|
|
||||||
|
(defmethod del-object :default
|
||||||
|
[cfg _]
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :invalid-storage-backend
|
||||||
|
:context cfg))
|
||||||
|
|
||||||
(defmulti del-objects-in-bulk (fn [cfg _] (:type cfg)))
|
(defmulti del-objects-in-bulk (fn [cfg _] (:type cfg)))
|
||||||
|
|
||||||
(defmethod del-objects-in-bulk :default
|
(defmethod del-objects-in-bulk :default
|
||||||
|
@ -62,7 +78,6 @@
|
||||||
:code :invalid-storage-backend
|
:code :invalid-storage-backend
|
||||||
:context cfg))
|
:context cfg))
|
||||||
|
|
||||||
|
|
||||||
;; --- HELPERS
|
;; --- HELPERS
|
||||||
|
|
||||||
(defn uuid->hex
|
(defn uuid->hex
|
||||||
|
@ -109,7 +124,10 @@
|
||||||
(make-output-stream [_ opts]
|
(make-output-stream [_ opts]
|
||||||
(throw (UnsupportedOperationException. "not implemented")))
|
(throw (UnsupportedOperationException. "not implemented")))
|
||||||
clojure.lang.Counted
|
clojure.lang.Counted
|
||||||
(count [_] size))))
|
(count [_] size)
|
||||||
|
|
||||||
|
java.lang.AutoCloseable
|
||||||
|
(close [_]))))
|
||||||
|
|
||||||
(defn string->content
|
(defn string->content
|
||||||
[^String v]
|
[^String v]
|
||||||
|
@ -129,7 +147,10 @@
|
||||||
|
|
||||||
clojure.lang.Counted
|
clojure.lang.Counted
|
||||||
(count [_]
|
(count [_]
|
||||||
(alength data)))))
|
(alength data))
|
||||||
|
|
||||||
|
java.lang.AutoCloseable
|
||||||
|
(close [_]))))
|
||||||
|
|
||||||
(defn- input-stream->content
|
(defn- input-stream->content
|
||||||
[^InputStream is size]
|
[^InputStream is size]
|
||||||
|
@ -146,7 +167,11 @@
|
||||||
(throw (UnsupportedOperationException. "not implemented")))
|
(throw (UnsupportedOperationException. "not implemented")))
|
||||||
|
|
||||||
clojure.lang.Counted
|
clojure.lang.Counted
|
||||||
(count [_] size)))
|
(count [_] size)
|
||||||
|
|
||||||
|
java.lang.AutoCloseable
|
||||||
|
(close [_]
|
||||||
|
(.close is))))
|
||||||
|
|
||||||
(defn content
|
(defn content
|
||||||
([data] (content data nil))
|
([data] (content data nil))
|
||||||
|
@ -179,10 +204,20 @@
|
||||||
|
|
||||||
(defn slurp-bytes
|
(defn slurp-bytes
|
||||||
[content]
|
[content]
|
||||||
(us/assert content? content)
|
|
||||||
(with-open [input (io/input-stream content)
|
(with-open [input (io/input-stream content)
|
||||||
output (java.io.ByteArrayOutputStream. (count content))]
|
output (java.io.ByteArrayOutputStream. (count content))]
|
||||||
(io/copy input output)
|
(io/copy input output)
|
||||||
(.toByteArray output)))
|
(.toByteArray output)))
|
||||||
|
|
||||||
|
(defn resolve-backend
|
||||||
|
[{:keys [conn pool] :as storage} backend-id]
|
||||||
|
(when backend-id
|
||||||
|
(let [backend (get-in storage [:backends backend-id])]
|
||||||
|
(when-not backend
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :backend-not-configured
|
||||||
|
:hint (str/fmt "backend '%s' not configured" backend-id)))
|
||||||
|
(assoc backend
|
||||||
|
:conn (or conn pool)
|
||||||
|
:id backend-id))))
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.storage.s3
|
(ns app.storage.s3
|
||||||
"Storage backends abstraction layer."
|
"S3 Storage backend implementation."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
|
@ -18,25 +18,34 @@
|
||||||
[integrant.core :as ig])
|
[integrant.core :as ig])
|
||||||
(:import
|
(:import
|
||||||
java.time.Duration
|
java.time.Duration
|
||||||
|
java.io.InputStream
|
||||||
java.util.Collection
|
java.util.Collection
|
||||||
software.amazon.awssdk.core.sync.RequestBody
|
software.amazon.awssdk.core.sync.RequestBody
|
||||||
|
software.amazon.awssdk.core.ResponseBytes
|
||||||
|
;; software.amazon.awssdk.core.ResponseInputStream
|
||||||
software.amazon.awssdk.regions.Region
|
software.amazon.awssdk.regions.Region
|
||||||
software.amazon.awssdk.services.s3.S3Client
|
software.amazon.awssdk.services.s3.S3Client
|
||||||
software.amazon.awssdk.services.s3.model.Delete
|
software.amazon.awssdk.services.s3.model.Delete
|
||||||
software.amazon.awssdk.services.s3.model.CopyObjectRequest
|
software.amazon.awssdk.services.s3.model.CopyObjectRequest
|
||||||
software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
|
software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
|
||||||
software.amazon.awssdk.services.s3.model.DeleteObjectsResponse
|
software.amazon.awssdk.services.s3.model.DeleteObjectsResponse
|
||||||
|
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
|
||||||
software.amazon.awssdk.services.s3.model.GetObjectRequest
|
software.amazon.awssdk.services.s3.model.GetObjectRequest
|
||||||
software.amazon.awssdk.services.s3.model.ObjectIdentifier
|
software.amazon.awssdk.services.s3.model.ObjectIdentifier
|
||||||
software.amazon.awssdk.services.s3.model.PutObjectRequest
|
software.amazon.awssdk.services.s3.model.PutObjectRequest
|
||||||
|
;; software.amazon.awssdk.services.s3.model.GetObjectResponse
|
||||||
software.amazon.awssdk.services.s3.presigner.S3Presigner
|
software.amazon.awssdk.services.s3.presigner.S3Presigner
|
||||||
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
|
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
|
||||||
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest))
|
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest
|
||||||
|
|
||||||
|
))
|
||||||
|
|
||||||
(declare put-object)
|
(declare put-object)
|
||||||
(declare copy-object)
|
(declare copy-object)
|
||||||
(declare get-object)
|
(declare get-object-bytes)
|
||||||
|
(declare get-object-data)
|
||||||
(declare get-object-url)
|
(declare get-object-url)
|
||||||
|
(declare del-object)
|
||||||
(declare del-object-in-bulk)
|
(declare del-object-in-bulk)
|
||||||
(declare build-s3-client)
|
(declare build-s3-client)
|
||||||
(declare build-s3-presigner)
|
(declare build-s3-presigner)
|
||||||
|
@ -87,12 +96,20 @@
|
||||||
|
|
||||||
(defmethod impl/get-object-data :s3
|
(defmethod impl/get-object-data :s3
|
||||||
[backend object]
|
[backend object]
|
||||||
(get-object backend object))
|
(get-object-data backend object))
|
||||||
|
|
||||||
|
(defmethod impl/get-object-bytes :s3
|
||||||
|
[backend object]
|
||||||
|
(get-object-bytes backend object))
|
||||||
|
|
||||||
(defmethod impl/get-object-url :s3
|
(defmethod impl/get-object-url :s3
|
||||||
[backend object options]
|
[backend object options]
|
||||||
(get-object-url backend object options))
|
(get-object-url backend object options))
|
||||||
|
|
||||||
|
(defmethod impl/del-object :s3
|
||||||
|
[backend object]
|
||||||
|
(del-object backend object))
|
||||||
|
|
||||||
(defmethod impl/del-objects-in-bulk :s3
|
(defmethod impl/del-objects-in-bulk :s3
|
||||||
[backend ids]
|
[backend ids]
|
||||||
(del-object-in-bulk backend ids))
|
(del-object-in-bulk backend ids))
|
||||||
|
@ -104,19 +121,19 @@
|
||||||
(case region
|
(case region
|
||||||
:eu-central-1 Region/EU_CENTRAL_1))
|
:eu-central-1 Region/EU_CENTRAL_1))
|
||||||
|
|
||||||
(defn- build-s3-client
|
(defn build-s3-client
|
||||||
[{:keys [region]}]
|
[{:keys [region]}]
|
||||||
(.. (S3Client/builder)
|
(.. (S3Client/builder)
|
||||||
(region (lookup-region region))
|
(region (lookup-region region))
|
||||||
(build)))
|
(build)))
|
||||||
|
|
||||||
(defn- build-s3-presigner
|
(defn build-s3-presigner
|
||||||
[{:keys [region]}]
|
[{:keys [region]}]
|
||||||
(.. (S3Presigner/builder)
|
(.. (S3Presigner/builder)
|
||||||
(region (lookup-region region))
|
(region (lookup-region region))
|
||||||
(build)))
|
(build)))
|
||||||
|
|
||||||
(defn- put-object
|
(defn put-object
|
||||||
[{:keys [client bucket prefix]} {:keys [id] :as object} content]
|
[{:keys [client bucket prefix]} {:keys [id] :as object} content]
|
||||||
(let [path (str prefix (impl/id->path id))
|
(let [path (str prefix (impl/id->path id))
|
||||||
mdata (meta object)
|
mdata (meta object)
|
||||||
|
@ -125,14 +142,15 @@
|
||||||
(bucket bucket)
|
(bucket bucket)
|
||||||
(contentType mtype)
|
(contentType mtype)
|
||||||
(key path)
|
(key path)
|
||||||
(build))
|
(build))]
|
||||||
content (RequestBody/fromInputStream (io/input-stream content)
|
|
||||||
(count content))]
|
(with-open [^InputStream is (io/input-stream content)]
|
||||||
|
(let [content (RequestBody/fromInputStream is (count content))]
|
||||||
(.putObject ^S3Client client
|
(.putObject ^S3Client client
|
||||||
^PutObjectRequest request
|
^PutObjectRequest request
|
||||||
^RequestBody content)))
|
^RequestBody content)))))
|
||||||
|
|
||||||
(defn- copy-object
|
(defn copy-object
|
||||||
[{:keys [client bucket prefix]} src-object dst-object]
|
[{:keys [client bucket prefix]} src-object dst-object]
|
||||||
(let [source-path (str prefix (impl/id->path (:id src-object)))
|
(let [source-path (str prefix (impl/id->path (:id src-object)))
|
||||||
source-mdata (meta src-object)
|
source-mdata (meta src-object)
|
||||||
|
@ -146,22 +164,33 @@
|
||||||
(contentType source-mtype)
|
(contentType source-mtype)
|
||||||
(build))]
|
(build))]
|
||||||
|
|
||||||
(.copyObject ^S3Client client
|
(.copyObject ^S3Client client ^CopyObjectRequest request)))
|
||||||
^CopyObjectRequest request)))
|
|
||||||
|
|
||||||
(defn- get-object
|
(defn get-object-data
|
||||||
[{:keys [client bucket prefix]} {:keys [id]}]
|
[{:keys [client bucket prefix]} {:keys [id]}]
|
||||||
(let [gor (.. (GetObjectRequest/builder)
|
(let [gor (.. (GetObjectRequest/builder)
|
||||||
(bucket bucket)
|
(bucket bucket)
|
||||||
(key (str prefix (impl/id->path id)))
|
(key (str prefix (impl/id->path id)))
|
||||||
(build))
|
(build))
|
||||||
obj (.getObject ^S3Client client ^GetObjectRequest gor)]
|
obj (.getObject ^S3Client client ^GetObjectRequest gor)
|
||||||
|
;; rsp (.response ^ResponseInputStream obj)
|
||||||
|
;; len (.contentLength ^GetObjectResponse rsp)
|
||||||
|
]
|
||||||
(io/input-stream obj)))
|
(io/input-stream obj)))
|
||||||
|
|
||||||
|
(defn get-object-bytes
|
||||||
|
[{:keys [client bucket prefix]} {:keys [id]}]
|
||||||
|
(let [gor (.. (GetObjectRequest/builder)
|
||||||
|
(bucket bucket)
|
||||||
|
(key (str prefix (impl/id->path id)))
|
||||||
|
(build))
|
||||||
|
obj (.getObjectAsBytes ^S3Client client ^GetObjectRequest gor)]
|
||||||
|
(.asByteArray ^ResponseBytes obj)))
|
||||||
|
|
||||||
(def default-max-age
|
(def default-max-age
|
||||||
(dt/duration {:minutes 10}))
|
(dt/duration {:minutes 10}))
|
||||||
|
|
||||||
(defn- get-object-url
|
(defn get-object-url
|
||||||
[{:keys [presigner bucket prefix]} {:keys [id]} {:keys [max-age] :or {max-age default-max-age}}]
|
[{:keys [presigner bucket prefix]} {:keys [id]} {:keys [max-age] :or {max-age default-max-age}}]
|
||||||
(us/assert dt/duration? max-age)
|
(us/assert dt/duration? max-age)
|
||||||
(let [gor (.. (GetObjectRequest/builder)
|
(let [gor (.. (GetObjectRequest/builder)
|
||||||
|
@ -175,7 +204,16 @@
|
||||||
pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)]
|
pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)]
|
||||||
(u/uri (str (.url ^PresignedGetObjectRequest pgor)))))
|
(u/uri (str (.url ^PresignedGetObjectRequest pgor)))))
|
||||||
|
|
||||||
(defn- del-object-in-bulk
|
(defn del-object
|
||||||
|
[{:keys [bucket client prefix]} {:keys [id] :as obj}]
|
||||||
|
(let [dor (.. (DeleteObjectRequest/builder)
|
||||||
|
(bucket bucket)
|
||||||
|
(key (str prefix (impl/id->path id)))
|
||||||
|
(build))]
|
||||||
|
(.deleteObject ^S3Client client
|
||||||
|
^DeleteObjectRequest dor)))
|
||||||
|
|
||||||
|
(defn del-object-in-bulk
|
||||||
[{:keys [bucket client prefix]} ids]
|
[{:keys [bucket client prefix]} ids]
|
||||||
(let [oids (map (fn [id]
|
(let [oids (map (fn [id]
|
||||||
(.. (ObjectIdentifier/builder)
|
(.. (ObjectIdentifier/builder)
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
;; TODO: DEPRECATED
|
||||||
|
;; Should be removed in the 1.8.x
|
||||||
|
|
||||||
(ns app.tasks.delete-object
|
(ns app.tasks.delete-object
|
||||||
"Generic task for permanent deletion of objects."
|
"Generic task for permanent deletion of objects."
|
||||||
(:require
|
(:require
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
;; TODO: DEPRECATED
|
||||||
|
;; Should be removed in the 1.8.x
|
||||||
|
|
||||||
(declare delete-profile-data)
|
(declare delete-profile-data)
|
||||||
|
|
||||||
;; --- INIT
|
;; --- INIT
|
||||||
|
|
|
@ -100,6 +100,7 @@
|
||||||
:id (:id mobj)
|
:id (:id mobj)
|
||||||
:media-id (:media-id mobj)
|
:media-id (:media-id mobj)
|
||||||
:thumbnail-id (:thumbnail-id mobj))
|
:thumbnail-id (:thumbnail-id mobj))
|
||||||
|
|
||||||
;; NOTE: deleting the file-media-object in the database
|
;; NOTE: deleting the file-media-object in the database
|
||||||
;; automatically marks as toched the referenced storage
|
;; automatically marks as toched the referenced storage
|
||||||
;; objects. The touch mechanism is needed because many files can
|
;; objects. The touch mechanism is needed because many files can
|
||||||
|
|
63
backend/src/app/tasks/file_offload.clj
Normal file
63
backend/src/app/tasks/file_offload.clj
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.tasks.file-offload
|
||||||
|
"A maintenance task that offloads file data to an external storage (S3)."
|
||||||
|
(:require
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.storage :as sto]
|
||||||
|
[app.storage.impl :as simpl]
|
||||||
|
[app.util.logging :as l]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
(def sql:offload-candidates-chunk
|
||||||
|
"select f.id, f.data from file as f
|
||||||
|
where f.data is not null
|
||||||
|
and f.modified_at < now() - ?::interval
|
||||||
|
order by f.modified_at
|
||||||
|
limit 10")
|
||||||
|
|
||||||
|
(defn- retrieve-candidates
|
||||||
|
[{:keys [conn max-age]}]
|
||||||
|
(db/exec! conn [sql:offload-candidates-chunk max-age]))
|
||||||
|
|
||||||
|
(defn- offload-candidate
|
||||||
|
[{:keys [storage conn backend] :as cfg} {:keys [id data] :as file}]
|
||||||
|
(l/debug :action "offload file data" :id id)
|
||||||
|
(let [backend (simpl/resolve-backend storage backend)]
|
||||||
|
(->> (simpl/content data)
|
||||||
|
(simpl/put-object backend file))
|
||||||
|
(db/update! conn :file
|
||||||
|
{:data nil
|
||||||
|
:data-backend (name (:id backend))}
|
||||||
|
{:id id})))
|
||||||
|
|
||||||
|
;; ---- STATE INIT
|
||||||
|
|
||||||
|
(s/def ::max-age ::dt/duration)
|
||||||
|
(s/def ::backend ::us/keyword)
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
|
(s/keys :req-un [::db/pool ::max-age ::sto/storage ::backend]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::handler
|
||||||
|
[_ {:keys [pool max-age] :as cfg}]
|
||||||
|
(fn [_]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [max-age (db/interval max-age)
|
||||||
|
cfg (-> cfg
|
||||||
|
(assoc :conn conn)
|
||||||
|
(assoc :max-age max-age))]
|
||||||
|
(loop [n 0]
|
||||||
|
(let [candidates (retrieve-candidates cfg)]
|
||||||
|
(if (seq candidates)
|
||||||
|
(do
|
||||||
|
(run! (partial offload-candidate cfg) candidates)
|
||||||
|
(recur (+ n (count candidates))))
|
||||||
|
(l/debug :hint "offload summary" :count n))))))))
|
171
backend/src/app/tasks/objects_gc.clj
Normal file
171
backend/src/app/tasks/objects_gc.clj
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.tasks.objects-gc
|
||||||
|
"A maintenance task that performs a general purpose garbage collection
|
||||||
|
of deleted objects."
|
||||||
|
(:require
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.storage :as sto]
|
||||||
|
[app.storage.impl :as simpl]
|
||||||
|
[app.util.logging :as l]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
(def target-tables
|
||||||
|
["profile"
|
||||||
|
"team"
|
||||||
|
"file"
|
||||||
|
"project"
|
||||||
|
"team_font_variant"])
|
||||||
|
|
||||||
|
(defmulti delete-objects :table)
|
||||||
|
|
||||||
|
(def sql:delete-objects
|
||||||
|
"with deleted as (
|
||||||
|
select id from %(table)s
|
||||||
|
where deleted_at is not null
|
||||||
|
and deleted_at < now() - ?::interval
|
||||||
|
order by deleted_at
|
||||||
|
limit %(limit)s
|
||||||
|
)
|
||||||
|
delete from %(table)s
|
||||||
|
where id in (select id from deleted)
|
||||||
|
returning *")
|
||||||
|
|
||||||
|
;; --- IMPL: generic object deletion
|
||||||
|
|
||||||
|
(defmethod delete-objects :default
|
||||||
|
[{:keys [conn max-age table] :as cfg}]
|
||||||
|
(let [sql (str/fmt sql:delete-objects
|
||||||
|
{:table table :limit 50})
|
||||||
|
result (db/exec! conn [sql max-age])]
|
||||||
|
|
||||||
|
(doseq [{:keys [id] :as item} result]
|
||||||
|
(l/trace :action "delete object" :table table :id id))
|
||||||
|
|
||||||
|
(count result)))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- IMPL: file deletion
|
||||||
|
|
||||||
|
(defmethod delete-objects "file"
|
||||||
|
[{:keys [conn max-age table storage] :as cfg}]
|
||||||
|
(let [sql (str/fmt sql:delete-objects
|
||||||
|
{:table table :limit 50})
|
||||||
|
result (db/exec! conn [sql max-age])
|
||||||
|
backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))]
|
||||||
|
|
||||||
|
(doseq [{:keys [id] :as item} result]
|
||||||
|
(l/trace :action "delete object" :table table :id id)
|
||||||
|
(when backend
|
||||||
|
(simpl/del-object backend item)))
|
||||||
|
|
||||||
|
(count result)))
|
||||||
|
|
||||||
|
;; --- IMPL: team-font-variant deletion
|
||||||
|
|
||||||
|
(defmethod delete-objects "team_font_variant"
|
||||||
|
[{:keys [conn max-age storage table] :as cfg}]
|
||||||
|
(let [sql (str/fmt sql:delete-objects
|
||||||
|
{:table table :limit 50})
|
||||||
|
fonts (db/exec! conn [sql max-age])
|
||||||
|
storage (assoc storage :conn conn)]
|
||||||
|
(doseq [{:keys [id] :as font} fonts]
|
||||||
|
(l/trace :action "delete object" :table table :id id)
|
||||||
|
(some->> (:woff1-file-id font) (sto/del-object storage))
|
||||||
|
(some->> (:woff2-file-id font) (sto/del-object storage))
|
||||||
|
(some->> (:otf-file-id font) (sto/del-object storage))
|
||||||
|
(some->> (:ttf-file-id font) (sto/del-object storage)))
|
||||||
|
(count fonts)))
|
||||||
|
|
||||||
|
;; --- IMPL: team deletion
|
||||||
|
|
||||||
|
(defmethod delete-objects "team"
|
||||||
|
[{:keys [conn max-age storage table] :as cfg}]
|
||||||
|
(let [sql (str/fmt sql:delete-objects
|
||||||
|
{:table table :limit 50})
|
||||||
|
teams (db/exec! conn [sql max-age])
|
||||||
|
storage (assoc storage :conn conn)]
|
||||||
|
|
||||||
|
(doseq [{:keys [id] :as team} teams]
|
||||||
|
(l/trace :action "delete object" :table table :id id)
|
||||||
|
(some->> (:photo-id team) (sto/del-object storage)))
|
||||||
|
|
||||||
|
(count teams)))
|
||||||
|
|
||||||
|
;; --- IMPL: profile deletion
|
||||||
|
|
||||||
|
(def sql:retrieve-deleted-profiles
|
||||||
|
"select id, photo_id from profile
|
||||||
|
where deleted_at is not null
|
||||||
|
and deleted_at < now() - ?::interval
|
||||||
|
order by deleted_at
|
||||||
|
limit %(limit)s
|
||||||
|
for update")
|
||||||
|
|
||||||
|
(def sql:mark-owned-teams-deleted
|
||||||
|
"with owned as (
|
||||||
|
select tpr.team_id as id
|
||||||
|
from team_profile_rel as tpr
|
||||||
|
where tpr.is_owner is true
|
||||||
|
and tpr.profile_id = ?
|
||||||
|
)
|
||||||
|
update team set deleted_at = now() - ?::interval
|
||||||
|
where id in (select id from owned)")
|
||||||
|
|
||||||
|
(defmethod delete-objects "profile"
|
||||||
|
[{:keys [conn max-age storage table] :as cfg}]
|
||||||
|
(let [sql (str/fmt sql:retrieve-deleted-profiles {:limit 50})
|
||||||
|
profiles (db/exec! conn [sql max-age])
|
||||||
|
storage (assoc storage :conn conn)]
|
||||||
|
|
||||||
|
(doseq [{:keys [id] :as profile} profiles]
|
||||||
|
(l/trace :action "delete object" :table table :id id)
|
||||||
|
|
||||||
|
;; Mark the owned teams as deleted; this enables them to be procesed
|
||||||
|
;; in the same transaction in the "team" table step.
|
||||||
|
(db/exec-one! conn [sql:mark-owned-teams-deleted id max-age])
|
||||||
|
|
||||||
|
;; Mark as deleted the storage object related with the photo-id
|
||||||
|
;; field.
|
||||||
|
(some->> (:photo-id profile) (sto/del-object storage))
|
||||||
|
|
||||||
|
;; And finally, permanently delete the profile.
|
||||||
|
(db/delete! conn :profile {:id id}))
|
||||||
|
|
||||||
|
(count profiles)))
|
||||||
|
|
||||||
|
;; --- INIT
|
||||||
|
|
||||||
|
(defn- process-table
|
||||||
|
[{:keys [table] :as cfg}]
|
||||||
|
(loop [n 0]
|
||||||
|
(let [res (delete-objects cfg)]
|
||||||
|
(if (pos? res)
|
||||||
|
(recur (+ n res))
|
||||||
|
(l/debug :hint "table gc summary" :table table :deleted n)))))
|
||||||
|
|
||||||
|
(s/def ::max-age ::dt/duration)
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
|
(s/keys :req-un [::db/pool ::sto/storage ::max-age]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::handler
|
||||||
|
[_ {:keys [pool max-age] :as cfg}]
|
||||||
|
(fn [task]
|
||||||
|
;; Checking first on task argument allows properly testing it.
|
||||||
|
(let [max-age (get task :max-age max-age)]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [max-age (db/interval max-age)
|
||||||
|
cfg (-> cfg
|
||||||
|
(assoc :max-age max-age)
|
||||||
|
(assoc :conn conn))]
|
||||||
|
(doseq [table target-tables]
|
||||||
|
(process-table (assoc cfg :table table))))))))
|
|
@ -9,21 +9,12 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
|
[app.common.transit :as t]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.transit :as t]
|
|
||||||
[buddy.core.kdf :as bk]
|
|
||||||
[buddy.sign.jwe :as jwe]
|
[buddy.sign.jwe :as jwe]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(defn- derive-tokens-secret
|
|
||||||
[key]
|
|
||||||
(let [engine (bk/engine {:key key
|
|
||||||
:salt "tokens"
|
|
||||||
:alg :hkdf
|
|
||||||
:digest :blake2b-512})]
|
|
||||||
(bk/get-bytes engine 32)))
|
|
||||||
|
|
||||||
(defn- generate
|
(defn- generate
|
||||||
[cfg claims]
|
[cfg claims]
|
||||||
(let [payload (t/encode claims)]
|
(let [payload (t/encode claims)]
|
||||||
|
@ -50,13 +41,6 @@
|
||||||
:params params))
|
:params params))
|
||||||
claims))
|
claims))
|
||||||
|
|
||||||
(s/def ::secret-key ::us/string)
|
|
||||||
(s/def ::props
|
|
||||||
(s/keys :req-un [::secret-key]))
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::tokens [_]
|
|
||||||
(s/keys :req-un [::props]))
|
|
||||||
|
|
||||||
(defn- generate-predefined
|
(defn- generate-predefined
|
||||||
[cfg {:keys [iss profile-id] :as params}]
|
[cfg {:keys [iss profile-id] :as params}]
|
||||||
(case iss
|
(case iss
|
||||||
|
@ -70,9 +54,14 @@
|
||||||
:code :not-implemented
|
:code :not-implemented
|
||||||
:hint "no predefined token")))
|
:hint "no predefined token")))
|
||||||
|
|
||||||
|
(s/def ::keys fn?)
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::tokens [_]
|
||||||
|
(s/keys :req-un [::keys]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::tokens
|
(defmethod ig/init-key ::tokens
|
||||||
[_ {:keys [props] :as cfg}]
|
[_ {:keys [keys] :as cfg}]
|
||||||
(let [secret (derive-tokens-secret (:secret-key props))
|
(let [secret (keys :salt "tokens" :size 32)
|
||||||
cfg (assoc cfg ::secret secret)]
|
cfg (assoc cfg ::secret secret)]
|
||||||
(fn [action params]
|
(fn [action params]
|
||||||
(case action
|
(case action
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
"A generic blob storage encoding. Mainly used for page data, page
|
"A generic blob storage encoding. Mainly used for page data, page
|
||||||
options and txlog payload storage."
|
options and txlog payload storage."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.transit :as t]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.util.transit :as t]
|
|
||||||
[taoensso.nippy :as n])
|
[taoensso.nippy :as n])
|
||||||
(:import
|
(:import
|
||||||
java.io.ByteArrayInputStream
|
java.io.ByteArrayInputStream
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
cdata (byte-array mlen)
|
cdata (byte-array mlen)
|
||||||
clen (Zstd/compressByteArray ^bytes cdata 0 mlen
|
clen (Zstd/compressByteArray ^bytes cdata 0 mlen
|
||||||
^bytes data 0 dlen
|
^bytes data 0 dlen
|
||||||
4)]
|
6)]
|
||||||
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
|
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
|
||||||
^DataOutputStream dos (DataOutputStream. baos)]
|
^DataOutputStream dos (DataOutputStream. baos)]
|
||||||
(.writeShort dos (short 3)) ;; version number
|
(.writeShort dos (short 3)) ;; version number
|
||||||
|
|
|
@ -10,13 +10,14 @@
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str])
|
[cuerdas.core :as str])
|
||||||
(:import
|
(:import
|
||||||
java.time.Instant
|
|
||||||
java.time.Duration
|
java.time.Duration
|
||||||
java.util.Date
|
java.time.Instant
|
||||||
java.time.ZonedDateTime
|
java.time.OffsetDateTime
|
||||||
java.time.ZoneId
|
java.time.ZoneId
|
||||||
|
java.time.ZonedDateTime
|
||||||
java.time.format.DateTimeFormatter
|
java.time.format.DateTimeFormatter
|
||||||
java.time.temporal.TemporalAmount
|
java.time.temporal.TemporalAmount
|
||||||
|
java.util.Date
|
||||||
org.apache.logging.log4j.core.util.CronExpression))
|
org.apache.logging.log4j.core.util.CronExpression))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -54,10 +55,17 @@
|
||||||
(obj->duration ms-or-obj)))
|
(obj->duration ms-or-obj)))
|
||||||
|
|
||||||
(defn duration-between
|
(defn duration-between
|
||||||
|
{:deprecated true}
|
||||||
[t1 t2]
|
[t1 t2]
|
||||||
(Duration/between t1 t2))
|
(Duration/between t1 t2))
|
||||||
|
|
||||||
(letfn [(conformer [v]
|
(defn diff
|
||||||
|
[t1 t2]
|
||||||
|
(Duration/between t1 t2))
|
||||||
|
|
||||||
|
(s/def ::duration
|
||||||
|
(s/conformer
|
||||||
|
(fn [v]
|
||||||
(cond
|
(cond
|
||||||
(duration? v) v
|
(duration? v) v
|
||||||
|
|
||||||
|
@ -69,13 +77,15 @@
|
||||||
|
|
||||||
:else
|
:else
|
||||||
::s/invalid))
|
::s/invalid))
|
||||||
(unformer [v]
|
(fn [v]
|
||||||
(subs (str v) 2))]
|
(subs (str v) 2))))
|
||||||
(s/def ::duration (s/conformer conformer unformer)))
|
|
||||||
|
|
||||||
(extend-protocol clojure.core/Inst
|
(extend-protocol clojure.core/Inst
|
||||||
java.time.Duration
|
java.time.Duration
|
||||||
(inst-ms* [v] (.toMillis ^Duration v)))
|
(inst-ms* [v] (.toMillis ^Duration v))
|
||||||
|
|
||||||
|
OffsetDateTime
|
||||||
|
(inst-ms* [v] (.toEpochMilli (.toInstant ^OffsetDateTime v))))
|
||||||
|
|
||||||
(defmethod print-method Duration
|
(defmethod print-method Duration
|
||||||
[mv ^java.io.Writer writer]
|
[mv ^java.io.Writer writer]
|
||||||
|
|
|
@ -436,7 +436,7 @@
|
||||||
(s/assert dt/cron? cron)
|
(s/assert dt/cron? cron)
|
||||||
(let [now (dt/now)
|
(let [now (dt/now)
|
||||||
next (dt/next-valid-instant-from cron now)]
|
next (dt/next-valid-instant-from cron now)]
|
||||||
(inst-ms (dt/duration-between now next))))
|
(inst-ms (dt/diff now next))))
|
||||||
|
|
||||||
(defn- schedule-task
|
(defn- schedule-task
|
||||||
[{:keys [scheduler] :as cfg} {:keys [cron] :as task}]
|
[{:keys [scheduler] :as cfg} {:keys [cron] :as task}]
|
||||||
|
|
|
@ -4,16 +4,16 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-bounces-handling
|
(ns app.bounce-handling-test
|
||||||
(:require
|
(:require
|
||||||
[clojure.pprint :refer [pprint]]
|
|
||||||
[app.http.awsns :as awsns]
|
|
||||||
[app.emails :as emails]
|
|
||||||
[app.tests.helpers :as th]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
[app.emails :as emails]
|
||||||
|
[app.http.awsns :as awsns]
|
||||||
|
[app.test-helpers :as th]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[mockery.core :refer [with-mocks]]
|
[clojure.pprint :refer [pprint]]
|
||||||
[clojure.test :as t]))
|
[clojure.test :as t]
|
||||||
|
[mockery.core :refer [with-mocks]]))
|
||||||
|
|
||||||
(t/use-fixtures :once th/state-init)
|
(t/use-fixtures :once th/state-init)
|
||||||
(t/use-fixtures :each th/database-reset)
|
(t/use-fixtures :each th/database-reset)
|
|
@ -4,13 +4,13 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-emails
|
(ns app.emails-test
|
||||||
(:require
|
(:require
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.emails :as emails]
|
[app.emails :as emails]
|
||||||
[app.tests.helpers :as th]))
|
[app.test-helpers :as th]))
|
||||||
|
|
||||||
(t/use-fixtures :once th/state-init)
|
(t/use-fixtures :once th/state-init)
|
||||||
(t/use-fixtures :each th/database-reset)
|
(t/use-fixtures :each th/database-reset)
|
|
@ -4,13 +4,14 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-services-files
|
(ns app.services-files-test
|
||||||
(:require
|
(:require
|
||||||
[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]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.tests.helpers :as th]
|
[app.test-helpers :as th]
|
||||||
|
[app.util.time :as dt]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[datoteka.core :as fs]))
|
[datoteka.core :as fs]))
|
||||||
|
|
||||||
|
@ -134,7 +135,7 @@
|
||||||
(t/deftest file-media-gc-task
|
(t/deftest file-media-gc-task
|
||||||
(letfn [(create-file-media-object [{:keys [profile-id file-id]}]
|
(letfn [(create-file-media-object [{:keys [profile-id file-id]}]
|
||||||
(let [mfile {:filename "sample.jpg"
|
(let [mfile {:filename "sample.jpg"
|
||||||
:tempfile (th/tempfile "app/tests/_files/sample.jpg")
|
:tempfile (th/tempfile "app/test_files/sample.jpg")
|
||||||
:content-type "image/jpeg"
|
:content-type "image/jpeg"
|
||||||
:size 312043}
|
:size 312043}
|
||||||
params {::th/type :upload-file-media-object
|
params {::th/type :upload-file-media-object
|
||||||
|
@ -337,3 +338,69 @@
|
||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-info? error))
|
||||||
(t/is (th/ex-of-type? error :not-found))))
|
(t/is (th/ex-of-type? error :not-found))))
|
||||||
|
|
||||||
|
(t/deftest deletion-test
|
||||||
|
(let [task (:app.tasks.objects-gc/handler th/*system*)
|
||||||
|
profile1 (th/create-profile* 1)
|
||||||
|
file (th/create-file* 1 {:project-id (:default-project-id profile1)
|
||||||
|
:profile-id (:id profile1)})]
|
||||||
|
;; file is not deleted because it does not meet all
|
||||||
|
;; conditions to be deleted.
|
||||||
|
(let [result (task {:max-age (dt/duration 0)})]
|
||||||
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
;; query the list of files
|
||||||
|
(let [data {::th/type :project-files
|
||||||
|
:project-id (:default-project-id profile1)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= 1 (count result)))))
|
||||||
|
|
||||||
|
;; Request file to be deleted
|
||||||
|
(let [params {::th/type :delete-file
|
||||||
|
:id (:id file)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/mutation! params)]
|
||||||
|
(t/is (nil? (:error out))))
|
||||||
|
|
||||||
|
;; query the list of files after soft deletion
|
||||||
|
(let [data {::th/type :project-files
|
||||||
|
:project-id (:default-project-id profile1)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= 0 (count result)))))
|
||||||
|
|
||||||
|
;; run permanent deletion (should be noop)
|
||||||
|
(let [result (task {:max-age (dt/duration {:minutes 1})})]
|
||||||
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
;; query the list of file libraries of a after hard deletion
|
||||||
|
(let [data {::th/type :file-libraries
|
||||||
|
:file-id (:id file)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= 0 (count result)))))
|
||||||
|
|
||||||
|
;; run permanent deletion
|
||||||
|
(let [result (task {:max-age (dt/duration 0)})]
|
||||||
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
;; query the list of file libraries of a after hard deletion
|
||||||
|
(let [data {::th/type :file-libraries
|
||||||
|
:file-id (:id file)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(let [error (:error out)
|
||||||
|
error-data (ex-data error)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (= (:type error-data) :not-found))))
|
||||||
|
))
|
|
@ -4,13 +4,13 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-services-fonts
|
(ns app.services-fonts-test
|
||||||
(:require
|
(:require
|
||||||
[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]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.tests.helpers :as th]
|
[app.test-helpers :as th]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[datoteka.core :as fs]))
|
[datoteka.core :as fs]))
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
proj-id (:default-project-id prof)
|
proj-id (:default-project-id prof)
|
||||||
font-id (uuid/custom 10 1)
|
font-id (uuid/custom 10 1)
|
||||||
|
|
||||||
ttfdata (-> (io/resource "app/tests/_files/font-1.ttf")
|
ttfdata (-> (io/resource "app/test_files/font-1.ttf")
|
||||||
(fs/slurp-bytes))
|
(fs/slurp-bytes))
|
||||||
|
|
||||||
params {::th/type :create-font-variant
|
params {::th/type :create-font-variant
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
proj-id (:default-project-id prof)
|
proj-id (:default-project-id prof)
|
||||||
font-id (uuid/custom 10 1)
|
font-id (uuid/custom 10 1)
|
||||||
|
|
||||||
data (-> (io/resource "app/tests/_files/font-1.woff")
|
data (-> (io/resource "app/test_files/font-1.woff")
|
||||||
(fs/slurp-bytes))
|
(fs/slurp-bytes))
|
||||||
|
|
||||||
params {::th/type :create-font-variant
|
params {::th/type :create-font-variant
|
|
@ -4,13 +4,13 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-services-management
|
(ns app.services-management-test
|
||||||
(:require
|
(:require
|
||||||
[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]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.tests.helpers :as th]
|
[app.test-helpers :as th]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[buddy.core.bytes :as b]
|
[buddy.core.bytes :as b]
|
||||||
[datoteka.core :as fs]))
|
[datoteka.core :as fs]))
|
|
@ -4,12 +4,12 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-services-media
|
(ns app.services-media-test
|
||||||
(:require
|
(:require
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.tests.helpers :as th]
|
[app.test-helpers :as th]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[datoteka.core :as fs]))
|
[datoteka.core :as fs]))
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
:project-id (:default-project-id prof)
|
:project-id (:default-project-id prof)
|
||||||
:is-shared false})
|
:is-shared false})
|
||||||
mfile {:filename "sample.jpg"
|
mfile {:filename "sample.jpg"
|
||||||
:tempfile (th/tempfile "app/tests/_files/sample.jpg")
|
:tempfile (th/tempfile "app/test_files/sample.jpg")
|
||||||
:content-type "image/jpeg"
|
:content-type "image/jpeg"
|
||||||
:size 312043}
|
:size 312043}
|
||||||
|
|
|
@ -4,16 +4,17 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-services-profile
|
(ns app.services-profile-test
|
||||||
(:require
|
(:require
|
||||||
[clojure.test :as t]
|
|
||||||
[clojure.java.io :as io]
|
|
||||||
[mockery.core :refer [with-mocks]]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[datoteka.core :as fs]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc.mutations.profile :as profile]
|
[app.rpc.mutations.profile :as profile]
|
||||||
[app.tests.helpers :as th]))
|
[app.test-helpers :as th]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[clojure.java.io :as io]
|
||||||
|
[clojure.test :as t]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[datoteka.core :as fs]
|
||||||
|
[mockery.core :refer [with-mocks]]))
|
||||||
|
|
||||||
;; TODO: profile deletion with teams
|
;; TODO: profile deletion with teams
|
||||||
;; TODO: profile deletion with owner teams
|
;; TODO: profile deletion with owner teams
|
||||||
|
@ -108,7 +109,7 @@
|
||||||
:profile-id (:id profile)
|
:profile-id (:id profile)
|
||||||
:file {:filename "sample.jpg"
|
:file {:filename "sample.jpg"
|
||||||
:size 123123
|
:size 123123
|
||||||
:tempfile "tests/app/tests/_files/sample.jpg"
|
:tempfile (th/tempfile "app/test_files/sample.jpg")
|
||||||
:content-type "image/jpeg"}}
|
:content-type "image/jpeg"}}
|
||||||
out (th/mutation! data)]
|
out (th/mutation! data)]
|
||||||
|
|
||||||
|
@ -117,7 +118,7 @@
|
||||||
))
|
))
|
||||||
|
|
||||||
(t/deftest profile-deletion-simple
|
(t/deftest profile-deletion-simple
|
||||||
(let [task (:app.tasks.delete-profile/handler th/*system*)
|
(let [task (:app.tasks.objects-gc/handler th/*system*)
|
||||||
prof (th/create-profile* 1)
|
prof (th/create-profile* 1)
|
||||||
file (th/create-file* 1 {:profile-id (:id prof)
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
:project-id (:default-project-id prof)
|
:project-id (:default-project-id prof)
|
||||||
|
@ -125,23 +126,14 @@
|
||||||
|
|
||||||
;; profile is not deleted because it does not meet all
|
;; profile is not deleted because it does not meet all
|
||||||
;; conditions to be deleted.
|
;; conditions to be deleted.
|
||||||
(let [result (task {:props {:profile-id (:id prof)}})]
|
(let [result (task {:max-age (dt/duration 0)})]
|
||||||
(t/is (nil? result)))
|
(t/is (nil? result)))
|
||||||
|
|
||||||
;; Request profile to be deleted
|
;; Request profile to be deleted
|
||||||
(with-mocks [mock {:target 'app.worker/submit! :return nil}]
|
|
||||||
(let [params {::th/type :delete-profile
|
(let [params {::th/type :delete-profile
|
||||||
:profile-id (:id prof)}
|
:profile-id (:id prof)}
|
||||||
out (th/mutation! params)]
|
out (th/mutation! params)]
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out))))
|
||||||
|
|
||||||
;; check the mock
|
|
||||||
(let [mock (deref mock)
|
|
||||||
mock-params (first (:call-args mock))]
|
|
||||||
(t/is (:called? mock))
|
|
||||||
(t/is (= 1 (:call-count mock)))
|
|
||||||
(t/is (= :delete-profile (:app.worker/task mock-params)))
|
|
||||||
(t/is (= (:id prof) (:profile-id mock-params))))))
|
|
||||||
|
|
||||||
;; query files after profile soft deletion
|
;; query files after profile soft deletion
|
||||||
(let [params {::th/type :files
|
(let [params {::th/type :files
|
||||||
|
@ -153,8 +145,8 @@
|
||||||
(t/is (= 1 (count (:result out)))))
|
(t/is (= 1 (count (:result out)))))
|
||||||
|
|
||||||
;; execute permanent deletion task
|
;; execute permanent deletion task
|
||||||
(let [result (task {:props {:profile-id (:id prof)}})]
|
(let [result (task {:max-age (dt/duration "-1m")})]
|
||||||
(t/is (true? result)))
|
(t/is (nil? result)))
|
||||||
|
|
||||||
;; query profile after delete
|
;; query profile after delete
|
||||||
(let [params {::th/type :profile
|
(let [params {::th/type :profile
|
||||||
|
@ -165,17 +157,6 @@
|
||||||
error-data (ex-data error)]
|
error-data (ex-data error)]
|
||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-info? error))
|
||||||
(t/is (= (:type error-data) :not-found))))
|
(t/is (= (:type error-data) :not-found))))
|
||||||
|
|
||||||
;; query files after profile soft deletion
|
|
||||||
(let [params {::th/type :files
|
|
||||||
:project-id (:default-project-id prof)
|
|
||||||
:profile-id (:id prof)}
|
|
||||||
out (th/query! params)]
|
|
||||||
;; (th/print-result! out)
|
|
||||||
(let [error (:error out)
|
|
||||||
error-data (ex-data error)]
|
|
||||||
(t/is (th/ex-info? error))
|
|
||||||
(t/is (= (:type error-data) :not-found))))
|
|
||||||
))
|
))
|
||||||
|
|
||||||
(t/deftest registration-domain-whitelist
|
(t/deftest registration-domain-whitelist
|
||||||
|
@ -187,126 +168,95 @@
|
||||||
(t/testing "not allowed email domain"
|
(t/testing "not allowed email domain"
|
||||||
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
|
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
|
||||||
|
|
||||||
(t/deftest test-register-with-no-terms-and-privacy
|
(t/deftest prepare-register-and-register-profile
|
||||||
(let [data {::th/type :register-profile
|
(let [data {::th/type :prepare-register-profile
|
||||||
:email "user@example.com"
|
:email "user@example.com"
|
||||||
:password "foobar"
|
:password "foobar"}
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy nil}
|
|
||||||
out (th/mutation! data)
|
out (th/mutation! data)
|
||||||
error (:error out)
|
token (get-in out [:result :token])]
|
||||||
edata (ex-data error)]
|
(t/is (string? token))
|
||||||
(t/is (th/ex-info? error))
|
|
||||||
(t/is (= (:type edata) :validation))
|
|
||||||
(t/is (= (:code edata) :spec-validation))))
|
|
||||||
|
|
||||||
(t/deftest test-register-with-bad-terms-and-privacy
|
|
||||||
|
;; try register without accepting terms
|
||||||
(let [data {::th/type :register-profile
|
(let [data {::th/type :register-profile
|
||||||
:email "user@example.com"
|
:token token
|
||||||
:password "foobar"
|
|
||||||
:fullname "foobar"
|
:fullname "foobar"
|
||||||
:terms-privacy false}
|
:accept-terms-and-privacy false}
|
||||||
out (th/mutation! data)
|
out (th/mutation! data)]
|
||||||
error (:error out)
|
(let [error (:error out)]
|
||||||
edata (ex-data error)]
|
|
||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-info? error))
|
||||||
(t/is (= (:type edata) :validation))
|
(t/is (th/ex-of-type? error :validation))
|
||||||
(t/is (= (:code edata) :invalid-terms-and-privacy))))
|
(t/is (th/ex-of-code? error :invalid-terms-and-privacy))))
|
||||||
|
|
||||||
(t/deftest test-register-when-registration-disabled
|
;; try register without token
|
||||||
|
(let [data {::th/type :register-profile
|
||||||
|
:fullname "foobar"
|
||||||
|
:accept-terms-and-privacy true}
|
||||||
|
out (th/mutation! data)]
|
||||||
|
(let [error (:error out)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :validation))
|
||||||
|
(t/is (th/ex-of-code? error :spec-validation))))
|
||||||
|
|
||||||
|
;; try correct register
|
||||||
|
(let [data {::th/type :register-profile
|
||||||
|
:token token
|
||||||
|
:fullname "foobar"
|
||||||
|
:accept-terms-and-privacy true
|
||||||
|
:accept-newsletter-subscription true}]
|
||||||
|
(let [{:keys [result error]} (th/mutation! data)]
|
||||||
|
(t/is (nil? error))
|
||||||
|
(t/is (true? (get-in result [:props :accept-newsletter-subscription])))
|
||||||
|
(t/is (true? (get-in result [:props :accept-terms-and-privacy])))))
|
||||||
|
))
|
||||||
|
|
||||||
|
(t/deftest prepare-register-with-registration-disabled
|
||||||
(with-mocks [mock {:target 'app.config/get
|
(with-mocks [mock {:target 'app.config/get
|
||||||
:return (th/mock-config-get-with
|
:return (th/mock-config-get-with
|
||||||
{:registration-enabled false})}]
|
{:registration-enabled false})}]
|
||||||
(let [data {::th/type :register-profile
|
|
||||||
:email "user@example.com"
|
|
||||||
:password "foobar"
|
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy true}
|
|
||||||
out (th/mutation! data)
|
|
||||||
error (:error out)
|
|
||||||
edata (ex-data error)]
|
|
||||||
(t/is (th/ex-info? error))
|
|
||||||
(t/is (= (:type edata) :restriction))
|
|
||||||
(t/is (= (:code edata) :registration-disabled)))))
|
|
||||||
|
|
||||||
(t/deftest test-register-existing-profile
|
(let [data {::th/type :prepare-register-profile
|
||||||
|
:email "user@example.com"
|
||||||
|
:password "foobar"}]
|
||||||
|
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :restriction))
|
||||||
|
(t/is (th/ex-of-code? error :registration-disabled))))))
|
||||||
|
|
||||||
|
(t/deftest prepare-register-with-existing-user
|
||||||
(let [profile (th/create-profile* 1)
|
(let [profile (th/create-profile* 1)
|
||||||
data {::th/type :register-profile
|
data {::th/type :prepare-register-profile
|
||||||
:email (:email profile)
|
:email (:email profile)
|
||||||
:password "foobar"
|
:password "foobar"}]
|
||||||
:fullname "foobar"
|
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||||
:terms-privacy true}
|
|
||||||
out (th/mutation! data)
|
|
||||||
error (:error out)
|
|
||||||
edata (ex-data error)]
|
|
||||||
(t/is (th/ex-info? error))
|
|
||||||
(t/is (= (:type edata) :validation))
|
|
||||||
(t/is (= (:code edata) :email-already-exists))))
|
|
||||||
|
|
||||||
(t/deftest test-register-profile
|
|
||||||
(with-mocks [mock {:target 'app.emails/send!
|
|
||||||
:return nil}]
|
|
||||||
(let [pool (:app.db/pool th/*system*)
|
|
||||||
data {::th/type :register-profile
|
|
||||||
:email "user@example.com"
|
|
||||||
:password "foobar"
|
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy true}
|
|
||||||
out (th/mutation! data)]
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(let [mock (deref mock)
|
(t/is (th/ex-info? error))
|
||||||
[params] (:call-args mock)]
|
(t/is (th/ex-of-type? error :validation))
|
||||||
;; (clojure.pprint/pprint params)
|
(t/is (th/ex-of-code? error :email-already-exists)))))
|
||||||
(t/is (:called? mock))
|
|
||||||
(t/is (= (:email data) (:to params)))
|
|
||||||
(t/is (contains? params :extra-data))
|
|
||||||
(t/is (contains? params :token)))
|
|
||||||
|
|
||||||
(let [result (:result out)]
|
|
||||||
(t/is (false? (:is-demo result)))
|
|
||||||
(t/is (= (:email data) (:email result)))
|
|
||||||
(t/is (= "penpot" (:auth-backend result)))
|
|
||||||
(t/is (= "foobar" (:fullname result)))
|
|
||||||
(t/is (not (contains? result :password)))))))
|
|
||||||
|
|
||||||
(t/deftest test-register-profile-with-bounced-email
|
(t/deftest test-register-profile-with-bounced-email
|
||||||
(with-mocks [mock {:target 'app.emails/send!
|
|
||||||
:return nil}]
|
|
||||||
(let [pool (:app.db/pool th/*system*)
|
(let [pool (:app.db/pool th/*system*)
|
||||||
data {::th/type :register-profile
|
data {::th/type :prepare-register-profile
|
||||||
:email "user@example.com"
|
:email "user@example.com"
|
||||||
:password "foobar"
|
:password "foobar"}]
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy true}
|
|
||||||
_ (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
|
||||||
out (th/mutation! data)]
|
|
||||||
;; (th/print-result! out)
|
|
||||||
|
|
||||||
(let [mock (deref mock)]
|
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
||||||
(t/is (false? (:called? mock))))
|
|
||||||
|
|
||||||
(let [error (:error out)
|
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||||
edata (ex-data error)]
|
|
||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-info? error))
|
||||||
(t/is (= (:type edata) :validation))
|
(t/is (th/ex-of-type? error :validation))
|
||||||
(t/is (= (:code edata) :email-has-permanent-bounces))))))
|
(t/is (th/ex-of-code? error :email-has-permanent-bounces)))))
|
||||||
|
|
||||||
(t/deftest test-register-profile-with-complained-email
|
(t/deftest test-register-profile-with-complained-email
|
||||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
|
||||||
(let [pool (:app.db/pool th/*system*)
|
(let [pool (:app.db/pool th/*system*)
|
||||||
data {::th/type :register-profile
|
data {::th/type :prepare-register-profile
|
||||||
:email "user@example.com"
|
:email "user@example.com"
|
||||||
:password "foobar"
|
:password "foobar"}]
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy true}
|
|
||||||
_ (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
|
|
||||||
out (th/mutation! data)]
|
|
||||||
|
|
||||||
(let [mock (deref mock)]
|
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
|
||||||
(t/is (true? (:called? mock))))
|
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||||
|
(t/is (nil? error))
|
||||||
(let [result (:result out)]
|
(t/is (string? (:token result))))))
|
||||||
(t/is (= (:email data) (:email result)))))))
|
|
||||||
|
|
||||||
(t/deftest test-email-change-request
|
(t/deftest test-email-change-request
|
||||||
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}
|
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}
|
|
@ -4,14 +4,14 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-services-projects
|
(ns app.services-projects-test
|
||||||
(:require
|
(:require
|
||||||
[clojure.test :as t]
|
[app.common.uuid :as uuid]
|
||||||
[promesa.core :as p]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http :as http]
|
[app.http :as http]
|
||||||
[app.tests.helpers :as th]
|
[app.test-helpers :as th]
|
||||||
[app.common.uuid :as uuid]))
|
[app.util.time :as dt]
|
||||||
|
[clojure.test :as t]))
|
||||||
|
|
||||||
(t/use-fixtures :once th/state-init)
|
(t/use-fixtures :once th/state-init)
|
||||||
(t/use-fixtures :each th/database-reset)
|
(t/use-fixtures :each th/database-reset)
|
||||||
|
@ -170,3 +170,71 @@
|
||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-info? error))
|
||||||
(t/is (th/ex-of-type? error :not-found))))
|
(t/is (th/ex-of-type? error :not-found))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest test-deletion
|
||||||
|
(let [task (:app.tasks.objects-gc/handler th/*system*)
|
||||||
|
profile1 (th/create-profile* 1)
|
||||||
|
project (th/create-project* 1 {:team-id (:default-team-id profile1)
|
||||||
|
:profile-id (:id profile1)})]
|
||||||
|
|
||||||
|
;; project is not deleted because it does not meet all
|
||||||
|
;; conditions to be deleted.
|
||||||
|
(let [result (task {:max-age (dt/duration 0)})]
|
||||||
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
;; query the list of projects
|
||||||
|
(let [data {::th/type :projects
|
||||||
|
:team-id (:default-team-id profile1)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= 2 (count result)))))
|
||||||
|
|
||||||
|
;; Request project to be deleted
|
||||||
|
(let [params {::th/type :delete-project
|
||||||
|
:id (:id project)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/mutation! params)]
|
||||||
|
(t/is (nil? (:error out))))
|
||||||
|
|
||||||
|
;; query the list of projects after soft deletion
|
||||||
|
(let [data {::th/type :projects
|
||||||
|
:team-id (:default-team-id profile1)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= 1 (count result)))))
|
||||||
|
|
||||||
|
;; run permanent deletion (should be noop)
|
||||||
|
(let [result (task {:max-age (dt/duration {:minutes 1})})]
|
||||||
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
;; query the list of files of a after soft deletion
|
||||||
|
(let [data {::th/type :project-files
|
||||||
|
:project-id (:id project)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= 0 (count result)))))
|
||||||
|
|
||||||
|
;; run permanent deletion
|
||||||
|
(let [result (task {:max-age (dt/duration 0)})]
|
||||||
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
;; query the list of files of a after hard deletion
|
||||||
|
(let [data {::th/type :project-files
|
||||||
|
:project-id (:id project)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(let [error (:error out)
|
||||||
|
error-data (ex-data error)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (= (:type error-data) :not-found))))
|
||||||
|
))
|
|
@ -4,16 +4,17 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-services-teams
|
(ns app.services-teams-test
|
||||||
(:require
|
(:require
|
||||||
[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]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.tests.helpers :as th]
|
[app.test-helpers :as th]
|
||||||
[mockery.core :refer [with-mocks]]
|
[app.util.time :as dt]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[datoteka.core :as fs]))
|
[datoteka.core :as fs]
|
||||||
|
[mockery.core :refer [with-mocks]]))
|
||||||
|
|
||||||
(t/use-fixtures :once th/state-init)
|
(t/use-fixtures :once th/state-init)
|
||||||
(t/use-fixtures :each th/database-reset)
|
(t/use-fixtures :each th/database-reset)
|
||||||
|
@ -80,6 +81,80 @@
|
||||||
|
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
(t/deftest test-deletion
|
||||||
|
(let [task (:app.tasks.objects-gc/handler th/*system*)
|
||||||
|
profile1 (th/create-profile* 1 {:is-active true})
|
||||||
|
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||||
|
pool (:app.db/pool th/*system*)
|
||||||
|
data {::th/type :delete-team
|
||||||
|
:team-id (:id team)
|
||||||
|
:profile-id (:id profile1)}]
|
||||||
|
|
||||||
|
;; team is not deleted because it does not meet all
|
||||||
|
;; conditions to be deleted.
|
||||||
|
(let [result (task {:max-age (dt/duration 0)})]
|
||||||
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
;; query the list of teams
|
||||||
|
(let [data {::th/type :teams
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= 2 (count result)))
|
||||||
|
(t/is (= (:id team) (get-in result [1 :id])))
|
||||||
|
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
|
||||||
|
|
||||||
|
;; Request team to be deleted
|
||||||
|
(let [params {::th/type :delete-team
|
||||||
|
:id (:id team)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/mutation! params)]
|
||||||
|
(t/is (nil? (:error out))))
|
||||||
|
|
||||||
|
;; query the list of teams after soft deletion
|
||||||
|
(let [data {::th/type :teams
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= 1 (count result)))
|
||||||
|
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
|
||||||
|
|
||||||
|
;; run permanent deletion (should be noop)
|
||||||
|
(let [result (task {:max-age (dt/duration {:minutes 1})})]
|
||||||
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
;; query the list of projects of a after hard deletion
|
||||||
|
(let [data {::th/type :projects
|
||||||
|
:team-id (:id team)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= 0 (count result)))))
|
||||||
|
|
||||||
|
;; run permanent deletion
|
||||||
|
(let [result (task {:max-age (dt/duration 0)})]
|
||||||
|
(t/is (nil? result)))
|
||||||
|
|
||||||
|
;; query the list of projects of a after hard deletion
|
||||||
|
(let [data {::th/type :projects
|
||||||
|
:team-id (:id team)
|
||||||
|
:profile-id (:id profile1)}
|
||||||
|
out (th/query! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(let [error (:error out)
|
||||||
|
error-data (ex-data error)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (= (:type error-data) :not-found))))
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-services-viewer
|
(ns app.services-viewer-test
|
||||||
(:require
|
(:require
|
||||||
[clojure.test :as t]
|
|
||||||
[datoteka.core :as fs]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.tests.helpers :as th]))
|
[app.test-helpers :as th]
|
||||||
|
[clojure.test :as t]
|
||||||
|
[datoteka.core :as fs]))
|
||||||
|
|
||||||
(t/use-fixtures :once th/state-init)
|
(t/use-fixtures :once th/state-init)
|
||||||
(t/use-fixtures :each th/database-reset)
|
(t/use-fixtures :each th/database-reset)
|
|
@ -4,12 +4,12 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.test-storage
|
(ns app.storage-test
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.tests.helpers :as th]
|
[app.test-helpers :as th]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
|
@ -22,7 +22,6 @@
|
||||||
th/database-reset
|
th/database-reset
|
||||||
th/clean-storage))
|
th/clean-storage))
|
||||||
|
|
||||||
;; TODO: add specific tests for DB backend.
|
|
||||||
|
|
||||||
(t/deftest put-and-retrieve-object
|
(t/deftest put-and-retrieve-object
|
||||||
(let [storage (:app.storage/storage th/*system*)
|
(let [storage (:app.storage/storage th/*system*)
|
||||||
|
@ -106,7 +105,7 @@
|
||||||
:project-id (:default-project-id prof)
|
:project-id (:default-project-id prof)
|
||||||
:is-shared false})
|
:is-shared false})
|
||||||
mfile {:filename "sample.jpg"
|
mfile {:filename "sample.jpg"
|
||||||
:tempfile (th/tempfile "app/tests/_files/sample.jpg")
|
:tempfile (th/tempfile "app/test_files/sample.jpg")
|
||||||
:content-type "image/jpeg"
|
:content-type "image/jpeg"
|
||||||
:size 312043}
|
:size 312043}
|
||||||
|
|
||||||
|
@ -167,7 +166,7 @@
|
||||||
:project-id (:default-project-id prof)
|
:project-id (:default-project-id prof)
|
||||||
:is-shared false})
|
:is-shared false})
|
||||||
mfile {:filename "sample.jpg"
|
mfile {:filename "sample.jpg"
|
||||||
:tempfile (th/tempfile "app/tests/_files/sample.jpg")
|
:tempfile (th/tempfile "app/test_files/sample.jpg")
|
||||||
:content-type "image/jpeg"
|
:content-type "image/jpeg"
|
||||||
:size 312043}
|
:size 312043}
|
||||||
|
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 305 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
@ -4,7 +4,7 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.tests.helpers
|
(ns app.test-helpers
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
|
@ -1,5 +1,5 @@
|
||||||
#kaocha/v1
|
#kaocha/v1
|
||||||
{:tests
|
{:tests
|
||||||
[{:id :unit
|
[{:id :unit
|
||||||
:test-paths ["tests" "src"]
|
:test-paths ["test" "src"]
|
||||||
:ns-patterns ["test-.*"]}]}
|
:ns-patterns [".*-test$"]}]}
|
||||||
|
|
|
@ -1,424 +0,0 @@
|
||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) UXBOX Labs SL
|
|
||||||
|
|
||||||
(ns app.common.geom.shapes.transforms
|
|
||||||
(:require
|
|
||||||
[app.common.attrs :as attrs]
|
|
||||||
[app.common.geom.matrix :as gmt]
|
|
||||||
[app.common.geom.point :as gpt]
|
|
||||||
[app.common.geom.shapes.common :as gco]
|
|
||||||
[app.common.geom.shapes.path :as gpa]
|
|
||||||
[app.common.geom.shapes.rect :as gpr]
|
|
||||||
[app.common.math :as mth]
|
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.text :as txt]))
|
|
||||||
|
|
||||||
;; --- Relative Movement
|
|
||||||
|
|
||||||
(defn move-selrect [selrect {dx :x dy :y}]
|
|
||||||
(-> selrect
|
|
||||||
(d/update-when :x + dx)
|
|
||||||
(d/update-when :y + dy)
|
|
||||||
(d/update-when :x1 + dx)
|
|
||||||
(d/update-when :y1 + dy)
|
|
||||||
(d/update-when :x2 + dx)
|
|
||||||
(d/update-when :y2 + dy)))
|
|
||||||
|
|
||||||
(defn move-points [points move-vec]
|
|
||||||
(->> points
|
|
||||||
(mapv #(gpt/add % move-vec))))
|
|
||||||
|
|
||||||
(defn move
|
|
||||||
"Move the shape relativelly to its current
|
|
||||||
position applying the provided delta."
|
|
||||||
[shape {dx :x dy :y}]
|
|
||||||
(let [dx (d/check-num dx)
|
|
||||||
dy (d/check-num dy)
|
|
||||||
move-vec (gpt/point dx dy)]
|
|
||||||
|
|
||||||
(-> shape
|
|
||||||
(update :selrect move-selrect move-vec)
|
|
||||||
(update :points move-points move-vec)
|
|
||||||
(d/update-when :x + dx)
|
|
||||||
(d/update-when :y + dy)
|
|
||||||
(cond-> (= :path (:type shape))
|
|
||||||
(update :content gpa/move-content move-vec)))))
|
|
||||||
|
|
||||||
;; --- Absolute Movement
|
|
||||||
|
|
||||||
(declare absolute-move-rect)
|
|
||||||
|
|
||||||
(defn absolute-move
|
|
||||||
"Move the shape to the exactly specified position."
|
|
||||||
[shape {:keys [x y]}]
|
|
||||||
(let [dx (- (d/check-num x) (-> shape :selrect :x))
|
|
||||||
dy (- (d/check-num y) (-> shape :selrect :y))]
|
|
||||||
(move shape (gpt/point dx dy))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn- modif-rotation [shape]
|
|
||||||
(let [cur-rotation (d/check-num (:rotation shape))
|
|
||||||
delta-angle (d/check-num (get-in shape [:modifiers :rotation]))]
|
|
||||||
(mod (+ cur-rotation delta-angle) 360)))
|
|
||||||
|
|
||||||
(defn transform-matrix
|
|
||||||
"Returns a transformation matrix without changing the shape properties.
|
|
||||||
The result should be used in a `transform` attribute in svg"
|
|
||||||
([shape] (transform-matrix shape nil))
|
|
||||||
([shape params] (transform-matrix shape params (or (gco/center-shape shape)
|
|
||||||
(gpt/point 0 0))))
|
|
||||||
([{:keys [flip-x flip-y] :as shape} {:keys [no-flip]} shape-center]
|
|
||||||
(-> (gmt/matrix)
|
|
||||||
(gmt/translate shape-center)
|
|
||||||
|
|
||||||
(gmt/multiply (:transform shape (gmt/matrix)))
|
|
||||||
(cond->
|
|
||||||
(and (not no-flip) flip-x) (gmt/scale (gpt/point -1 1))
|
|
||||||
(and (not no-flip) flip-y) (gmt/scale (gpt/point 1 -1)))
|
|
||||||
(gmt/translate (gpt/negate shape-center)))))
|
|
||||||
|
|
||||||
(defn inverse-transform-matrix
|
|
||||||
([shape]
|
|
||||||
(let [shape-center (or (gco/center-shape shape)
|
|
||||||
(gpt/point 0 0))]
|
|
||||||
(inverse-transform-matrix shape shape-center)))
|
|
||||||
([{:keys [flip-x flip-y] :as shape} center]
|
|
||||||
(let []
|
|
||||||
(-> (gmt/matrix)
|
|
||||||
(gmt/translate center)
|
|
||||||
(cond->
|
|
||||||
flip-x (gmt/scale (gpt/point -1 1))
|
|
||||||
flip-y (gmt/scale (gpt/point 1 -1)))
|
|
||||||
(gmt/multiply (:transform-inverse shape (gmt/matrix)))
|
|
||||||
(gmt/translate (gpt/negate center))))))
|
|
||||||
|
|
||||||
(defn transform-point-center
|
|
||||||
"Transform a point around the shape center"
|
|
||||||
[point center matrix]
|
|
||||||
(gpt/transform
|
|
||||||
point
|
|
||||||
(gmt/multiply (gmt/translate-matrix center)
|
|
||||||
matrix
|
|
||||||
(gmt/translate-matrix (gpt/negate center)))))
|
|
||||||
|
|
||||||
(defn transform-points
|
|
||||||
([points matrix]
|
|
||||||
(transform-points points nil matrix))
|
|
||||||
([points center matrix]
|
|
||||||
(let [prev (if center (gmt/translate-matrix center) (gmt/matrix))
|
|
||||||
post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix))
|
|
||||||
|
|
||||||
tr-point (fn [point]
|
|
||||||
(gpt/transform point (gmt/multiply prev matrix post)))]
|
|
||||||
(mapv tr-point points))))
|
|
||||||
|
|
||||||
(defn transform-rect
|
|
||||||
"Transform a rectangles and changes its attributes"
|
|
||||||
[rect matrix]
|
|
||||||
|
|
||||||
(let [points (-> (gpr/rect->points rect)
|
|
||||||
(transform-points matrix))]
|
|
||||||
(gpr/points->rect points)))
|
|
||||||
|
|
||||||
(defn normalize-scale
|
|
||||||
"We normalize the scale so it's not too close to 0"
|
|
||||||
[scale]
|
|
||||||
(cond
|
|
||||||
(and (< scale 0) (> scale -0.01)) -0.01
|
|
||||||
(and (>= scale 0) (< scale 0.01)) 0.01
|
|
||||||
:else scale))
|
|
||||||
|
|
||||||
(defn modifiers->transform
|
|
||||||
[center modifiers]
|
|
||||||
(let [ds-modifier (:displacement modifiers (gmt/matrix))
|
|
||||||
{res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1))
|
|
||||||
|
|
||||||
;; Normalize x/y vector coordinates because scale by 0 is infinite
|
|
||||||
res-x (normalize-scale res-x)
|
|
||||||
res-y (normalize-scale res-y)
|
|
||||||
resize (gpt/point res-x res-y)
|
|
||||||
|
|
||||||
origin (:resize-origin modifiers (gpt/point 0 0))
|
|
||||||
|
|
||||||
resize-transform (:resize-transform modifiers (gmt/matrix))
|
|
||||||
resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix))
|
|
||||||
rt-modif (or (:rotation modifiers) 0)
|
|
||||||
|
|
||||||
center (gpt/transform center ds-modifier)
|
|
||||||
|
|
||||||
transform (-> (gmt/matrix)
|
|
||||||
|
|
||||||
;; Applies the current resize transformation
|
|
||||||
(gmt/translate origin)
|
|
||||||
(gmt/multiply resize-transform)
|
|
||||||
(gmt/scale resize)
|
|
||||||
(gmt/multiply resize-transform-inverse)
|
|
||||||
(gmt/translate (gpt/negate origin))
|
|
||||||
|
|
||||||
;; Applies the stacked transformations
|
|
||||||
(gmt/translate center)
|
|
||||||
(gmt/multiply (gmt/rotate-matrix rt-modif))
|
|
||||||
(gmt/translate (gpt/negate center))
|
|
||||||
|
|
||||||
;; Displacement
|
|
||||||
(gmt/multiply ds-modifier))]
|
|
||||||
transform))
|
|
||||||
|
|
||||||
(defn- calculate-skew-angle
|
|
||||||
"Calculates the skew angle of the paralelogram given by the points"
|
|
||||||
[[p1 _ p3 p4]]
|
|
||||||
(let [v1 (gpt/to-vec p3 p4)
|
|
||||||
v2 (gpt/to-vec p4 p1)]
|
|
||||||
;; If one of the vectors is zero it's a rectangle with 0 height or width
|
|
||||||
;; We don't skew these
|
|
||||||
(if (or (gpt/almost-zero? v1)
|
|
||||||
(gpt/almost-zero? v2))
|
|
||||||
0
|
|
||||||
(- 90 (gpt/angle-with-other v1 v2)))))
|
|
||||||
|
|
||||||
(defn- calculate-height
|
|
||||||
"Calculates the height of a paralelogram given by the points"
|
|
||||||
[[p1 _ _ p4]]
|
|
||||||
(-> (gpt/to-vec p4 p1)
|
|
||||||
(gpt/length)))
|
|
||||||
|
|
||||||
(defn- calculate-width
|
|
||||||
"Calculates the width of a paralelogram given by the points"
|
|
||||||
[[p1 p2 _ _]]
|
|
||||||
(-> (gpt/to-vec p1 p2)
|
|
||||||
(gpt/length)))
|
|
||||||
|
|
||||||
(defn- calculate-rotation
|
|
||||||
"Calculates the rotation between two shapes given the resize vector direction"
|
|
||||||
[center points-shape1 points-shape2 flip-x flip-y]
|
|
||||||
|
|
||||||
(let [idx-1 0
|
|
||||||
idx-2 (cond (and flip-x (not flip-y)) 1
|
|
||||||
(and flip-x flip-y) 2
|
|
||||||
(and (not flip-x) flip-y) 3
|
|
||||||
:else 0)
|
|
||||||
p1 (nth points-shape1 idx-1)
|
|
||||||
p2 (nth points-shape2 idx-2)
|
|
||||||
v1 (gpt/to-vec center p1)
|
|
||||||
v2 (gpt/to-vec center p2)
|
|
||||||
|
|
||||||
rot-angle (gpt/angle-with-other v1 v2)
|
|
||||||
rot-sign (gpt/angle-sign v1 v2)]
|
|
||||||
(* rot-sign rot-angle)))
|
|
||||||
|
|
||||||
(defn- calculate-dimensions
|
|
||||||
[[p1 p2 p3 _]]
|
|
||||||
(let [width (gpt/distance p1 p2)
|
|
||||||
height (gpt/distance p2 p3)]
|
|
||||||
{:width width :height height}))
|
|
||||||
|
|
||||||
(defn calculate-adjust-matrix
|
|
||||||
"Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that
|
|
||||||
after applying them the end result is the `shape-pathn-temp`.
|
|
||||||
This is compose of three transformations: skew, resize and rotation"
|
|
||||||
([points-temp points-rec] (calculate-adjust-matrix points-temp points-rec false false))
|
|
||||||
([points-temp points-rec flip-x flip-y]
|
|
||||||
(let [center (gco/center-points points-temp)
|
|
||||||
|
|
||||||
stretch-matrix (gmt/matrix)
|
|
||||||
|
|
||||||
skew-angle (calculate-skew-angle points-temp)
|
|
||||||
|
|
||||||
;; When one of the axis is flipped we have to reverse the skew
|
|
||||||
;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
|
|
||||||
skew-angle (if (and (or flip-x flip-y)
|
|
||||||
(not (and flip-x flip-y))) (- skew-angle) skew-angle )
|
|
||||||
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
|
|
||||||
|
|
||||||
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
|
|
||||||
|
|
||||||
h1 (max 1 (calculate-height points-temp))
|
|
||||||
h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix)))
|
|
||||||
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1)
|
|
||||||
h3 (if (mth/nan? h3) 1 h3)
|
|
||||||
|
|
||||||
w1 (max 1 (calculate-width points-temp))
|
|
||||||
w2 (max 1 (calculate-width (transform-points points-rec center stretch-matrix)))
|
|
||||||
w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1)
|
|
||||||
w3 (if (mth/nan? w3) 1 w3)
|
|
||||||
|
|
||||||
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point w3 h3)))
|
|
||||||
|
|
||||||
rotation-angle (calculate-rotation
|
|
||||||
center
|
|
||||||
(transform-points points-rec (gco/center-points points-rec) stretch-matrix)
|
|
||||||
points-temp
|
|
||||||
flip-x
|
|
||||||
flip-y)
|
|
||||||
|
|
||||||
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
|
|
||||||
|
|
||||||
;; This is the inverse to be able to remove the transformation
|
|
||||||
stretch-matrix-inverse (-> (gmt/matrix)
|
|
||||||
(gmt/scale (gpt/point (/ 1 w3) (/ 1 h3)))
|
|
||||||
(gmt/skew (- skew-angle) 0)
|
|
||||||
(gmt/rotate (- rotation-angle)))]
|
|
||||||
[stretch-matrix stretch-matrix-inverse rotation-angle])))
|
|
||||||
|
|
||||||
(defn apply-transform
|
|
||||||
"Given a new set of points transformed, set up the rectangle so it keeps
|
|
||||||
its properties. We adjust de x,y,width,height and create a custom transform"
|
|
||||||
[shape transform round-coords?]
|
|
||||||
;;
|
|
||||||
(let [points (-> shape :points (transform-points transform))
|
|
||||||
center (gco/center-points points)
|
|
||||||
|
|
||||||
;; Reverse the current transformation stack to get the base rectangle
|
|
||||||
tr-inverse (:transform-inverse shape (gmt/matrix))
|
|
||||||
|
|
||||||
points-temp (transform-points points center tr-inverse)
|
|
||||||
points-temp-dim (calculate-dimensions points-temp)
|
|
||||||
|
|
||||||
;; This rectangle is the new data for the current rectangle. We want to change our rectangle
|
|
||||||
;; to have this width, height, x, y
|
|
||||||
rect-shape (-> (gco/make-centered-rect
|
|
||||||
center
|
|
||||||
(:width points-temp-dim)
|
|
||||||
(:height points-temp-dim))
|
|
||||||
(update :width max 1)
|
|
||||||
(update :height max 1))
|
|
||||||
|
|
||||||
rect-points (gpr/rect->points rect-shape)
|
|
||||||
|
|
||||||
[matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape))
|
|
||||||
|
|
||||||
rect-shape (cond-> rect-shape
|
|
||||||
round-coords?
|
|
||||||
(-> (update :x mth/round)
|
|
||||||
(update :y mth/round)
|
|
||||||
(update :width mth/round)
|
|
||||||
(update :height mth/round)))
|
|
||||||
|
|
||||||
shape (cond
|
|
||||||
(= :path (:type shape))
|
|
||||||
(-> shape
|
|
||||||
(update :content #(gpa/transform-content % transform)))
|
|
||||||
|
|
||||||
:else
|
|
||||||
(-> shape
|
|
||||||
(merge rect-shape)))
|
|
||||||
|
|
||||||
base-rotation (or (:rotation shape) 0)
|
|
||||||
modif-rotation (or (get-in shape [:modifiers :rotation]) 0)]
|
|
||||||
|
|
||||||
(as-> shape $
|
|
||||||
(update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix))
|
|
||||||
(update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix))))
|
|
||||||
(assoc $ :points (into [] points))
|
|
||||||
(assoc $ :selrect (gpr/rect->selrect rect-shape))
|
|
||||||
(assoc $ :rotation (mod (+ base-rotation modif-rotation) 360)))))
|
|
||||||
|
|
||||||
(defn set-flip [shape modifiers]
|
|
||||||
(let [rx (get-in modifiers [:resize-vector :x])
|
|
||||||
ry (get-in modifiers [:resize-vector :y])]
|
|
||||||
(cond-> shape
|
|
||||||
(and rx (< rx 0)) (-> (update :flip-x not)
|
|
||||||
(update :rotation -))
|
|
||||||
(and ry (< ry 0)) (-> (update :flip-y not)
|
|
||||||
(update :rotation -)))))
|
|
||||||
|
|
||||||
(defn apply-displacement [shape]
|
|
||||||
(let [modifiers (:modifiers shape)]
|
|
||||||
(if (contains? modifiers :displacement)
|
|
||||||
(let [mov-vec (-> (gpt/point 0 0)
|
|
||||||
(gpt/transform (:displacement modifiers)))
|
|
||||||
shape (move shape mov-vec)
|
|
||||||
modifiers (dissoc modifiers :displacement)]
|
|
||||||
(-> shape
|
|
||||||
(assoc :modifiers modifiers)
|
|
||||||
(cond-> (empty? modifiers)
|
|
||||||
(dissoc :modifiers))))
|
|
||||||
shape)))
|
|
||||||
|
|
||||||
(defn apply-text-resize
|
|
||||||
[shape orig-shape modifiers]
|
|
||||||
(if (and (= (:type shape) :text)
|
|
||||||
(:resize-scale-text modifiers))
|
|
||||||
(let [merge-attrs (fn [attrs]
|
|
||||||
(let [font-size (-> (get attrs :font-size 14)
|
|
||||||
(d/parse-double)
|
|
||||||
(* (-> modifiers :resize-vector :x))
|
|
||||||
(str)
|
|
||||||
)]
|
|
||||||
(attrs/merge attrs {:font-size font-size})))]
|
|
||||||
(update shape :content #(txt/transform-nodes
|
|
||||||
txt/is-text-node?
|
|
||||||
merge-attrs
|
|
||||||
%)))
|
|
||||||
shape))
|
|
||||||
|
|
||||||
(defn transform-shape
|
|
||||||
([shape]
|
|
||||||
(transform-shape shape nil))
|
|
||||||
|
|
||||||
([shape {:keys [round-coords?]
|
|
||||||
:or {round-coords? true}}]
|
|
||||||
(let [shape (apply-displacement shape)
|
|
||||||
center (gco/center-shape shape)
|
|
||||||
modifiers (:modifiers shape)]
|
|
||||||
(if (and modifiers center)
|
|
||||||
(let [transform (modifiers->transform center modifiers)]
|
|
||||||
(-> shape
|
|
||||||
(set-flip modifiers)
|
|
||||||
(apply-transform transform round-coords?)
|
|
||||||
(apply-text-resize shape modifiers)
|
|
||||||
(dissoc :modifiers)))
|
|
||||||
shape))))
|
|
||||||
|
|
||||||
(defn update-group-viewbox
|
|
||||||
"Updates the viewbox for groups imported from SVG's"
|
|
||||||
[{:keys [selrect svg-viewbox] :as group} new-selrect]
|
|
||||||
(let [;; Gets deltas for the selrect to update the svg-viewbox (for svg-imports)
|
|
||||||
deltas {:x (- (:x new-selrect 0) (:x selrect 0))
|
|
||||||
:y (- (:y new-selrect 0) (:y selrect 0))
|
|
||||||
:width (- (:width new-selrect 1) (:width selrect 1))
|
|
||||||
:height (- (:height new-selrect 1) (:height selrect 1))}]
|
|
||||||
|
|
||||||
(cond-> group
|
|
||||||
(and (some? svg-viewbox) (some? selrect) (some? new-selrect))
|
|
||||||
(update :svg-viewbox
|
|
||||||
#(-> %
|
|
||||||
(update :x + (:x deltas))
|
|
||||||
(update :y + (:y deltas))
|
|
||||||
(update :width + (:width deltas))
|
|
||||||
(update :height + (:height deltas)))))))
|
|
||||||
|
|
||||||
(defn update-group-selrect [group children]
|
|
||||||
(let [shape-center (gco/center-shape group)
|
|
||||||
transform (:transform group (gmt/matrix))
|
|
||||||
transform-inverse (:transform-inverse group (gmt/matrix))
|
|
||||||
|
|
||||||
;; Points for every shape inside the group
|
|
||||||
points (->> children (mapcat :points))
|
|
||||||
|
|
||||||
;; Invert to get the points minus the transforms applied to the group
|
|
||||||
base-points (transform-points points shape-center (:transform-inverse group (gmt/matrix)))
|
|
||||||
|
|
||||||
;; Defines the new selection rect with its transformations
|
|
||||||
new-points (-> (gpr/points->selrect base-points)
|
|
||||||
(gpr/rect->points)
|
|
||||||
(transform-points shape-center (:transform group (gmt/matrix))))
|
|
||||||
|
|
||||||
;; Calculte the new selrect
|
|
||||||
new-selrect (gpr/points->selrect base-points)]
|
|
||||||
|
|
||||||
;; Updates the shape and the applytransform-rect will update the other properties
|
|
||||||
(-> group
|
|
||||||
(update-group-viewbox new-selrect)
|
|
||||||
(assoc :selrect new-selrect)
|
|
||||||
(assoc :points new-points)
|
|
||||||
|
|
||||||
;; We're regenerating the selrect from its children so we
|
|
||||||
;; need to remove the flip flags
|
|
||||||
(assoc :flip-x false)
|
|
||||||
(assoc :flip-y false)
|
|
||||||
(apply-transform (gmt/matrix) true))))
|
|
||||||
|
|
72
common/deps.edn
Normal file
72
common/deps.edn
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
{:deps
|
||||||
|
{org.clojure/clojure {:mvn/version "1.10.3"}
|
||||||
|
org.clojure/data.json {:mvn/version "2.3.1"}
|
||||||
|
org.clojure/core.async {:mvn/version "1.3.618"}
|
||||||
|
org.clojure/tools.cli {:mvn/version "1.0.206"}
|
||||||
|
metosin/jsonista {:mvn/version "0.3.3"}
|
||||||
|
org.clojure/clojurescript {:mvn/version "1.10.844"}
|
||||||
|
|
||||||
|
;; Logging
|
||||||
|
org.clojure/tools.logging {:mvn/version "1.1.0"}
|
||||||
|
org.apache.logging.log4j/log4j-api {:mvn/version "2.14.1"}
|
||||||
|
org.apache.logging.log4j/log4j-core {:mvn/version "2.14.1"}
|
||||||
|
org.apache.logging.log4j/log4j-web {:mvn/version "2.14.1"}
|
||||||
|
org.apache.logging.log4j/log4j-jul {:mvn/version "2.14.1"}
|
||||||
|
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.14.1"}
|
||||||
|
org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"}
|
||||||
|
|
||||||
|
selmer/selmer {:mvn/version "1.12.40"}
|
||||||
|
expound/expound {:mvn/version "0.8.9"}
|
||||||
|
com.cognitect/transit-clj {:mvn/version "1.0.324"}
|
||||||
|
com.cognitect/transit-cljs {:mvn/version "0.8.269"}
|
||||||
|
java-http-clj/java-http-clj {:mvn/version "0.4.2"}
|
||||||
|
|
||||||
|
funcool/promesa {:mvn/version "6.0.1"}
|
||||||
|
funcool/cuerdas {:mvn/version "2021.05.29-0"}
|
||||||
|
|
||||||
|
lambdaisland/uri {:mvn/version "1.4.70"
|
||||||
|
:exclusions [org.clojure/data.json]}
|
||||||
|
|
||||||
|
frankiesardo/linked {:mvn/version "1.3.0"}
|
||||||
|
danlentz/clj-uuid {:mvn/version "0.1.9"}
|
||||||
|
commons-io/commons-io {:mvn/version "2.8.0"}
|
||||||
|
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
|
||||||
|
|
||||||
|
;; exception printing
|
||||||
|
io.aviso/pretty {:mvn/version "0.1.37"}
|
||||||
|
environ/environ {:mvn/version "1.2.0"}}
|
||||||
|
:paths ["src"]
|
||||||
|
:aliases
|
||||||
|
{:dev
|
||||||
|
{:extra-deps
|
||||||
|
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||||
|
org.clojure/test.check {:mvn/version "RELEASE"}
|
||||||
|
org.clojure/tools.deps.alpha {:mvn/version "RELEASE"}
|
||||||
|
thheller/shadow-cljs {:mvn/version "2.12.6"}
|
||||||
|
criterium/criterium {:mvn/version "RELEASE"}
|
||||||
|
mockery/mockery {:mvn/version "RELEASE"}}
|
||||||
|
:extra-paths ["test" "dev"]}
|
||||||
|
|
||||||
|
:repl
|
||||||
|
{:extra-deps
|
||||||
|
{com.bhauman/rebel-readline {:mvn/version "RELEASE"}}
|
||||||
|
:main-opts ["-m" "rebel-readline.main"]}
|
||||||
|
|
||||||
|
:kaocha
|
||||||
|
{:extra-deps {lambdaisland/kaocha {:mvn/version "RELEASE"}}
|
||||||
|
:main-opts ["-m" "kaocha.runner"]}
|
||||||
|
|
||||||
|
:test
|
||||||
|
{:extra-paths ["test"]
|
||||||
|
:extra-deps {io.github.cognitect-labs/test-runner
|
||||||
|
{:git/url "https://github.com/cognitect-labs/test-runner.git"
|
||||||
|
:sha "705ad25bbf0228b1c38d0244a36001c2987d7337"}}
|
||||||
|
:exec-fn cognitect.test-runner.api/test}
|
||||||
|
|
||||||
|
:shadow-cljs
|
||||||
|
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}
|
||||||
|
|
||||||
|
:outdated
|
||||||
|
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
|
||||||
|
:main-opts ["-m" "antq.core"]}}}
|
||||||
|
|
51
common/dev/user.clj
Normal file
51
common/dev/user.clj
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns user
|
||||||
|
(:require
|
||||||
|
[clojure.java.io :as io]
|
||||||
|
[clojure.pprint :refer [pprint print-table]]
|
||||||
|
[clojure.repl :refer :all]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[clojure.spec.gen.alpha :as sgen]
|
||||||
|
[clojure.test :as test]
|
||||||
|
[clojure.tools.namespace.repl :as repl]
|
||||||
|
[clojure.walk :refer [macroexpand-all]]
|
||||||
|
[criterium.core :refer [quick-bench bench with-progress-reporting]]))
|
||||||
|
|
||||||
|
;; --- Benchmarking Tools
|
||||||
|
|
||||||
|
(defmacro run-quick-bench
|
||||||
|
[& exprs]
|
||||||
|
`(with-progress-reporting (quick-bench (do ~@exprs) :verbose)))
|
||||||
|
|
||||||
|
(defmacro run-quick-bench'
|
||||||
|
[& exprs]
|
||||||
|
`(quick-bench (do ~@exprs)))
|
||||||
|
|
||||||
|
(defmacro run-bench
|
||||||
|
[& exprs]
|
||||||
|
`(with-progress-reporting (bench (do ~@exprs) :verbose)))
|
||||||
|
|
||||||
|
(defmacro run-bench'
|
||||||
|
[& exprs]
|
||||||
|
`(bench (do ~@exprs)))
|
||||||
|
|
||||||
|
;; --- Development Stuff
|
||||||
|
|
||||||
|
(defn- run-tests
|
||||||
|
([] (run-tests #"^app.common.tests.*"))
|
||||||
|
([o]
|
||||||
|
(repl/refresh)
|
||||||
|
(cond
|
||||||
|
(instance? java.util.regex.Pattern o)
|
||||||
|
(test/run-all-tests o)
|
||||||
|
|
||||||
|
(symbol? o)
|
||||||
|
(if-let [sns (namespace o)]
|
||||||
|
(do (require (symbol sns))
|
||||||
|
(test/test-vars [(resolve o)]))
|
||||||
|
(test/test-ns o)))))
|
13
common/package.json
Normal file
13
common/package.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "penpot-common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"luxon": "^1.27.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"source-map-support": "^0.5.19",
|
||||||
|
"ws": "^7.4.6"
|
||||||
|
}
|
||||||
|
}
|
17
common/shadow-cljs.edn
Normal file
17
common/shadow-cljs.edn
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{:deps {:aliases [:dev]}
|
||||||
|
;; :http {:port 3448}
|
||||||
|
;; :nrepl {:port 3447}
|
||||||
|
:jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"]
|
||||||
|
|
||||||
|
:builds
|
||||||
|
{:test
|
||||||
|
{:target :node-test
|
||||||
|
:output-to "target/tests.js"
|
||||||
|
:ns-regexp "^app.common.*-test$"
|
||||||
|
;; :autorun true
|
||||||
|
|
||||||
|
:compiler-options
|
||||||
|
{:output-feature-set :es-next
|
||||||
|
:output-wrapper false
|
||||||
|
:warnings {:fn-deprecated false}}}}}
|
||||||
|
|
|
@ -10,14 +10,16 @@
|
||||||
#?(:cljs
|
#?(:cljs
|
||||||
(:require-macros [app.common.data]))
|
(:require-macros [app.common.data]))
|
||||||
(:require
|
(:require
|
||||||
[linked.set :as lks]
|
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
|
[cljs.analyzer.api :as aapi]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
#?(:clj [cljs.analyzer.api :as aapi])
|
[cuerdas.core :as str]
|
||||||
#?(:cljs [cljs.reader :as r]
|
#?(:cljs [cljs.reader :as r]
|
||||||
:clj [clojure.edn :as r])
|
:clj [clojure.edn :as r])
|
||||||
#?(:cljs [cljs.core :as core]
|
#?(:cljs [cljs.core :as core]
|
||||||
:clj [clojure.core :as core]))
|
:clj [clojure.core :as core])
|
||||||
|
[linked.set :as lks])
|
||||||
|
|
||||||
#?(:clj
|
#?(:clj
|
||||||
(:import linked.set.LinkedSet)))
|
(:import linked.set.LinkedSet)))
|
||||||
|
|
||||||
|
@ -274,7 +276,7 @@
|
||||||
;; Data Parsing / Conversion
|
;; Data Parsing / Conversion
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn- nan?
|
(defn nan?
|
||||||
[v]
|
[v]
|
||||||
(not= v v))
|
(not= v v))
|
||||||
|
|
||||||
|
@ -456,13 +458,18 @@
|
||||||
kw (if (keyword? kw) (name kw) kw)]
|
kw (if (keyword? kw) (name kw) kw)]
|
||||||
(keyword (str prefix kw))))
|
(keyword (str prefix kw))))
|
||||||
|
|
||||||
|
|
||||||
(defn tap
|
(defn tap
|
||||||
"Simpilar to the tap in rxjs but for plain collections"
|
"Simpilar to the tap in rxjs but for plain collections"
|
||||||
[f coll]
|
[f coll]
|
||||||
(f coll)
|
(f coll)
|
||||||
coll)
|
coll)
|
||||||
|
|
||||||
|
(defn tap-r
|
||||||
|
"Same but with args reversed, for -> threads"
|
||||||
|
[coll f]
|
||||||
|
(f coll)
|
||||||
|
coll)
|
||||||
|
|
||||||
(defn map-diff
|
(defn map-diff
|
||||||
"Given two maps returns the diff of its attributes in a map where
|
"Given two maps returns the diff of its attributes in a map where
|
||||||
the keys will be the attributes that change and the values the previous
|
the keys will be the attributes that change and the values the previous
|
||||||
|
@ -478,12 +485,11 @@
|
||||||
:var [2 nil]}
|
:var [2 nil]}
|
||||||
:d [nil 10] }
|
:d [nil 10] }
|
||||||
|
|
||||||
If both maps are identical the result will be an empty map
|
If both maps are identical the result will be an empty map."
|
||||||
"
|
|
||||||
[m1 m2]
|
[m1 m2]
|
||||||
|
|
||||||
(let [m1ks (keys m1)
|
(let [m1ks (set (keys m1))
|
||||||
m2ks (keys m2)
|
m2ks (set (keys m2))
|
||||||
keys (set/union m1ks m2ks)
|
keys (set/union m1ks m2ks)
|
||||||
|
|
||||||
diff-attr
|
diff-attr
|
||||||
|
@ -503,3 +509,65 @@
|
||||||
|
|
||||||
(->> keys
|
(->> keys
|
||||||
(reduce diff-attr {}))))
|
(reduce diff-attr {}))))
|
||||||
|
|
||||||
|
(defn- extract-numeric-suffix
|
||||||
|
[basename]
|
||||||
|
(if-let [[_ p1 p2] (re-find #"(.*)-([0-9]+)$" basename)]
|
||||||
|
[p1 (+ 1 (parse-integer p2))]
|
||||||
|
[basename 1]))
|
||||||
|
|
||||||
|
(defn unique-name
|
||||||
|
"A unique name generator"
|
||||||
|
([basename used]
|
||||||
|
(unique-name basename used false))
|
||||||
|
|
||||||
|
([basename used prefix-first?]
|
||||||
|
(assert (string? basename))
|
||||||
|
(assert (set? used))
|
||||||
|
|
||||||
|
(let [[prefix initial] (extract-numeric-suffix basename)]
|
||||||
|
(if (and (not prefix-first?)
|
||||||
|
(not (contains? used basename)))
|
||||||
|
basename
|
||||||
|
(loop [counter initial]
|
||||||
|
(let [candidate (if (and (= 1 counter) prefix-first?)
|
||||||
|
(str prefix)
|
||||||
|
(str prefix "-" counter))]
|
||||||
|
(if (contains? used candidate)
|
||||||
|
(recur (inc counter))
|
||||||
|
candidate)))))))
|
||||||
|
|
||||||
|
(defn deep-mapm
|
||||||
|
"Applies a map function to an associative map and recurses over its children
|
||||||
|
when it's a vector or a map"
|
||||||
|
[mfn m]
|
||||||
|
(let [do-map
|
||||||
|
(fn [entry]
|
||||||
|
(let [[k v] (mfn entry)]
|
||||||
|
(cond
|
||||||
|
(or (vector? v) (map? v))
|
||||||
|
[k (deep-mapm mfn v)]
|
||||||
|
|
||||||
|
:else
|
||||||
|
(mfn [k v]))))]
|
||||||
|
(cond
|
||||||
|
(map? m)
|
||||||
|
(into {} (map do-map) m)
|
||||||
|
|
||||||
|
(vector? m)
|
||||||
|
(into [] (map (partial deep-mapm mfn)) m)
|
||||||
|
|
||||||
|
:else
|
||||||
|
m)))
|
||||||
|
|
||||||
|
(defn not-empty?
|
||||||
|
[coll]
|
||||||
|
(boolean (seq coll)))
|
||||||
|
|
||||||
|
(defn kebab-keys [m]
|
||||||
|
(->> m
|
||||||
|
(deep-mapm
|
||||||
|
(fn [[k v]]
|
||||||
|
(if (or (keyword? k) (string? k))
|
||||||
|
[(keyword (str/kebab (name k))) v]
|
||||||
|
[k v])))))
|
|
@ -46,7 +46,7 @@
|
||||||
(assoc-in stack [:items index] value))
|
(assoc-in stack [:items index] value))
|
||||||
|
|
||||||
(defn undo
|
(defn undo
|
||||||
[{index :index items :items :as stack}]
|
[stack]
|
||||||
(update stack :index dec))
|
(update stack :index dec))
|
||||||
|
|
||||||
(defn redo
|
(defn redo
|
||||||
|
@ -56,5 +56,5 @@
|
||||||
(update :index inc)))
|
(update :index inc)))
|
||||||
|
|
||||||
(defn size
|
(defn size
|
||||||
[{index :index items :items :as stack}]
|
[{index :index :as stack}]
|
||||||
(inc index))
|
(inc index))
|
467
common/src/app/common/file_builder.cljc
Normal file
467
common/src/app/common/file_builder.cljc
Normal file
|
@ -0,0 +1,467 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.common.file-builder
|
||||||
|
"A version parsing helper."
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.geom.matrix :as gmt]
|
||||||
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.pages.changes :as ch]
|
||||||
|
[app.common.pages.init :as init]
|
||||||
|
[app.common.pages.spec :as spec]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
(def root-frame uuid/zero)
|
||||||
|
(def conjv (fnil conj []))
|
||||||
|
(def conjs (fnil conj #{}))
|
||||||
|
|
||||||
|
;; This flag controls if we should execute spec validation after every commit
|
||||||
|
(def verify-on-commit? true)
|
||||||
|
|
||||||
|
(defn- commit-change
|
||||||
|
([file change]
|
||||||
|
(commit-change file change nil))
|
||||||
|
|
||||||
|
([file change {:keys [add-container?]
|
||||||
|
:or {add-container? false}}]
|
||||||
|
(let [component-id (:current-component-id file)
|
||||||
|
change (cond-> change
|
||||||
|
(and add-container? (some? component-id))
|
||||||
|
(assoc :component-id component-id)
|
||||||
|
|
||||||
|
(and add-container? (nil? component-id))
|
||||||
|
(assoc :page-id (:current-page-id file)
|
||||||
|
:frame-id (:current-frame-id file)))]
|
||||||
|
|
||||||
|
(when verify-on-commit?
|
||||||
|
(us/assert ::spec/change change))
|
||||||
|
(-> file
|
||||||
|
(update :changes conjv change)
|
||||||
|
(update :data ch/process-changes [change] verify-on-commit?)))))
|
||||||
|
|
||||||
|
(defn- lookup-objects
|
||||||
|
([file]
|
||||||
|
(if (some? (:current-component-id file))
|
||||||
|
(get-in file [:data :components (:current-component-id file) :objects])
|
||||||
|
(get-in file [:data :pages-index (:current-page-id file) :objects]))))
|
||||||
|
|
||||||
|
(defn- lookup-shape [file shape-id]
|
||||||
|
(-> (lookup-objects file)
|
||||||
|
(get shape-id)))
|
||||||
|
|
||||||
|
(defn- commit-shape [file obj]
|
||||||
|
(let [parent-id (-> file :parent-stack peek)]
|
||||||
|
(-> file
|
||||||
|
(commit-change
|
||||||
|
{:type :add-obj
|
||||||
|
:id (:id obj)
|
||||||
|
:obj obj
|
||||||
|
:parent-id parent-id}
|
||||||
|
|
||||||
|
{:add-container? true}))))
|
||||||
|
|
||||||
|
(defn setup-rect-selrect [obj]
|
||||||
|
(let [rect (select-keys obj [:x :y :width :height])
|
||||||
|
center (gsh/center-rect rect)
|
||||||
|
transform (:transform obj (gmt/matrix))
|
||||||
|
selrect (gsh/rect->selrect rect)
|
||||||
|
|
||||||
|
points (-> (gsh/rect->points rect)
|
||||||
|
(gsh/transform-points center transform))]
|
||||||
|
|
||||||
|
(-> obj
|
||||||
|
(assoc :selrect selrect)
|
||||||
|
(assoc :points points))))
|
||||||
|
|
||||||
|
(defn- setup-path-selrect
|
||||||
|
[obj]
|
||||||
|
(let [content (:content obj)
|
||||||
|
center (:center obj)
|
||||||
|
|
||||||
|
transform-inverse
|
||||||
|
(->> (:transform-inverse obj (gmt/matrix))
|
||||||
|
(gmt/transform-in center))
|
||||||
|
|
||||||
|
transform
|
||||||
|
(->> (:transform obj (gmt/matrix))
|
||||||
|
(gmt/transform-in center))
|
||||||
|
|
||||||
|
content' (gsh/transform-content content transform-inverse)
|
||||||
|
selrect (gsh/content->selrect content')
|
||||||
|
points (-> (gsh/rect->points selrect)
|
||||||
|
(gsh/transform-points transform))]
|
||||||
|
|
||||||
|
(-> obj
|
||||||
|
(dissoc :center)
|
||||||
|
(assoc :selrect selrect)
|
||||||
|
(assoc :points points))))
|
||||||
|
|
||||||
|
(defn- setup-selrect
|
||||||
|
[obj]
|
||||||
|
(if (= (:type obj) :path)
|
||||||
|
(setup-path-selrect obj)
|
||||||
|
(setup-rect-selrect obj)))
|
||||||
|
|
||||||
|
(defn- generate-name
|
||||||
|
[type data]
|
||||||
|
(if (= type :svg-raw)
|
||||||
|
(let [tag (get-in data [:content :tag])]
|
||||||
|
(str "svg-" (cond (string? tag) tag
|
||||||
|
(keyword? tag) (d/name tag)
|
||||||
|
(nil? tag) "node"
|
||||||
|
:else (str tag))))
|
||||||
|
(str/capital (d/name type))))
|
||||||
|
|
||||||
|
(defn- add-name
|
||||||
|
[file name]
|
||||||
|
(let [container-id (or (:current-component-id file)
|
||||||
|
(:current-page-id file))]
|
||||||
|
(-> file
|
||||||
|
(update-in [:unames container-id] conjs name))))
|
||||||
|
|
||||||
|
(defn- unique-name
|
||||||
|
[name file]
|
||||||
|
(let [container-id (or (:current-component-id file)
|
||||||
|
(:current-page-id file))
|
||||||
|
unames (get-in file [:unames container-id])]
|
||||||
|
(d/unique-name name (or unames #{}))))
|
||||||
|
|
||||||
|
(defn clear-names [file]
|
||||||
|
(dissoc file :unames))
|
||||||
|
|
||||||
|
(defn- check-name
|
||||||
|
"Given a tag returns its layer name"
|
||||||
|
[data file type]
|
||||||
|
|
||||||
|
(cond-> data
|
||||||
|
(nil? (:name data))
|
||||||
|
(assoc :name (generate-name type data))
|
||||||
|
|
||||||
|
:always
|
||||||
|
(update :name unique-name file)))
|
||||||
|
|
||||||
|
;; PUBLIC API
|
||||||
|
|
||||||
|
(defn create-file
|
||||||
|
([name]
|
||||||
|
(create-file (uuid/next) name))
|
||||||
|
|
||||||
|
([id name]
|
||||||
|
{:id id
|
||||||
|
:name name
|
||||||
|
:data (-> init/empty-file-data
|
||||||
|
(assoc :id id))
|
||||||
|
|
||||||
|
;; We keep the changes so we can send them to the backend
|
||||||
|
:changes []}))
|
||||||
|
|
||||||
|
(defn add-page
|
||||||
|
[file data]
|
||||||
|
|
||||||
|
(assert (nil? (:current-component-id file)))
|
||||||
|
(let [page-id (or (:id data) (uuid/next))
|
||||||
|
page (-> init/empty-page-data
|
||||||
|
(assoc :id page-id)
|
||||||
|
(d/deep-merge data))]
|
||||||
|
(-> file
|
||||||
|
(commit-change
|
||||||
|
{:type :add-page
|
||||||
|
:page page})
|
||||||
|
|
||||||
|
;; Current page being edited
|
||||||
|
(assoc :current-page-id page-id)
|
||||||
|
|
||||||
|
;; Current frame-id
|
||||||
|
(assoc :current-frame-id root-frame)
|
||||||
|
|
||||||
|
;; Current parent stack we'll be nesting
|
||||||
|
(assoc :parent-stack [root-frame])
|
||||||
|
|
||||||
|
;; Last object id added
|
||||||
|
(assoc :last-id nil))))
|
||||||
|
|
||||||
|
(defn close-page [file]
|
||||||
|
(assert (nil? (:current-component-id file)))
|
||||||
|
(-> file
|
||||||
|
(dissoc :current-page-id)
|
||||||
|
(dissoc :parent-stack)
|
||||||
|
(dissoc :last-id)
|
||||||
|
(clear-names)))
|
||||||
|
|
||||||
|
(defn add-artboard [file data]
|
||||||
|
(assert (nil? (:current-component-id file)))
|
||||||
|
(let [obj (-> (init/make-minimal-shape :frame)
|
||||||
|
(merge data)
|
||||||
|
(check-name file :frame)
|
||||||
|
(setup-selrect)
|
||||||
|
(d/without-nils))]
|
||||||
|
(-> file
|
||||||
|
(commit-shape obj)
|
||||||
|
(assoc :current-frame-id (:id obj))
|
||||||
|
(assoc :last-id (:id obj))
|
||||||
|
(add-name (:name obj))
|
||||||
|
(update :parent-stack conjv (:id obj)))))
|
||||||
|
|
||||||
|
(defn close-artboard [file]
|
||||||
|
(assert (nil? (:current-component-id file)))
|
||||||
|
(-> file
|
||||||
|
(assoc :current-frame-id root-frame)
|
||||||
|
(update :parent-stack pop)))
|
||||||
|
|
||||||
|
(defn add-group [file data]
|
||||||
|
(let [frame-id (:current-frame-id file)
|
||||||
|
selrect init/empty-selrect
|
||||||
|
name (:name data)
|
||||||
|
obj (-> (init/make-minimal-group frame-id selrect name)
|
||||||
|
(merge data)
|
||||||
|
(check-name file :group)
|
||||||
|
(d/without-nils))]
|
||||||
|
(-> file
|
||||||
|
(commit-shape obj)
|
||||||
|
(assoc :last-id (:id obj))
|
||||||
|
(add-name (:name obj))
|
||||||
|
(update :parent-stack conjv (:id obj)))))
|
||||||
|
|
||||||
|
(defn close-group [file]
|
||||||
|
(let [group-id (-> file :parent-stack peek)
|
||||||
|
group (lookup-shape file group-id)
|
||||||
|
children (->> group :shapes (mapv #(lookup-shape file %)))
|
||||||
|
|
||||||
|
file
|
||||||
|
(cond
|
||||||
|
(empty? children)
|
||||||
|
(commit-change
|
||||||
|
file
|
||||||
|
{:type :del-obj
|
||||||
|
:id group-id}
|
||||||
|
{:add-container? true})
|
||||||
|
|
||||||
|
(:masked-group? group)
|
||||||
|
(let [mask (first children)]
|
||||||
|
(commit-change
|
||||||
|
file
|
||||||
|
{:type :mod-obj
|
||||||
|
:id group-id
|
||||||
|
:operations
|
||||||
|
[{:type :set :attr :x :val (-> mask :selrect :x)}
|
||||||
|
{:type :set :attr :y :val (-> mask :selrect :y)}
|
||||||
|
{:type :set :attr :width :val (-> mask :selrect :width)}
|
||||||
|
{:type :set :attr :height :val (-> mask :selrect :height)}
|
||||||
|
{:type :set :attr :flip-x :val (-> mask :flip-x)}
|
||||||
|
{:type :set :attr :flip-y :val (-> mask :flip-y)}
|
||||||
|
{:type :set :attr :selrect :val (-> mask :selrect)}
|
||||||
|
{:type :set :attr :points :val (-> mask :points)}]}
|
||||||
|
{:add-container? true}))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(let [group' (gsh/update-group-selrect group children)]
|
||||||
|
(commit-change
|
||||||
|
file
|
||||||
|
{:type :mod-obj
|
||||||
|
:id group-id
|
||||||
|
:operations
|
||||||
|
[{:type :set :attr :selrect :val (:selrect group')}
|
||||||
|
{:type :set :attr :points :val (:points group')}
|
||||||
|
{:type :set :attr :x :val (-> group' :selrect :x)}
|
||||||
|
{:type :set :attr :y :val (-> group' :selrect :y)}
|
||||||
|
{:type :set :attr :width :val (-> group' :selrect :width)}
|
||||||
|
{:type :set :attr :height :val (-> group' :selrect :height)}]}
|
||||||
|
|
||||||
|
{:add-container? true})))]
|
||||||
|
|
||||||
|
(-> file
|
||||||
|
(update :parent-stack pop))))
|
||||||
|
|
||||||
|
(defn create-shape [file type data]
|
||||||
|
(let [frame-id (:current-frame-id file)
|
||||||
|
frame (when-not (= frame-id root-frame)
|
||||||
|
(lookup-shape file frame-id))
|
||||||
|
obj (-> (init/make-minimal-shape type)
|
||||||
|
(merge data)
|
||||||
|
(check-name file :type)
|
||||||
|
(setup-selrect)
|
||||||
|
(d/without-nils))
|
||||||
|
obj (cond-> obj
|
||||||
|
frame (gsh/translate-from-frame frame))]
|
||||||
|
(-> file
|
||||||
|
(commit-shape obj)
|
||||||
|
(assoc :last-id (:id obj))
|
||||||
|
(add-name (:name obj)))))
|
||||||
|
|
||||||
|
(defn create-rect [file data]
|
||||||
|
(create-shape file :rect data))
|
||||||
|
|
||||||
|
(defn create-circle [file data]
|
||||||
|
(create-shape file :circle data))
|
||||||
|
|
||||||
|
(defn create-path [file data]
|
||||||
|
(create-shape file :path data))
|
||||||
|
|
||||||
|
(defn create-text [file data]
|
||||||
|
(create-shape file :text data))
|
||||||
|
|
||||||
|
(defn create-image [file data]
|
||||||
|
(create-shape file :image data))
|
||||||
|
|
||||||
|
(declare close-svg-raw)
|
||||||
|
|
||||||
|
(defn create-svg-raw [file data]
|
||||||
|
(let [file (as-> file $
|
||||||
|
(create-shape $ :svg-raw data)
|
||||||
|
(update $ :parent-stack conjv (:last-id $)))
|
||||||
|
|
||||||
|
create-child
|
||||||
|
(fn [file child]
|
||||||
|
(-> file
|
||||||
|
(create-svg-raw (assoc data
|
||||||
|
:id (uuid/next)
|
||||||
|
:content child))
|
||||||
|
(close-svg-raw)))]
|
||||||
|
|
||||||
|
;; First :content is the the shape attribute, the other content is the
|
||||||
|
;; XML children
|
||||||
|
(reduce create-child file (get-in data [:content :content]))))
|
||||||
|
|
||||||
|
(defn close-svg-raw [file]
|
||||||
|
(-> file
|
||||||
|
(update :parent-stack pop)))
|
||||||
|
|
||||||
|
(defn add-interaction
|
||||||
|
[file from-id {:keys [action-type event-type destination]}]
|
||||||
|
|
||||||
|
(assert (some? (lookup-shape file from-id)) (str "Cannot locate shape with id " from-id))
|
||||||
|
(assert (some? (lookup-shape file destination)) (str "Cannot locate shape with id " destination))
|
||||||
|
|
||||||
|
(let [interactions (->> (lookup-shape file from-id)
|
||||||
|
:interactions
|
||||||
|
(filterv #(or (not= (:action-type %) action-type)
|
||||||
|
(not= (:event-type %) event-type))))
|
||||||
|
interactions (-> interactions
|
||||||
|
(conjv
|
||||||
|
{:action-type action-type
|
||||||
|
:event-type event-type
|
||||||
|
:destination destination}))]
|
||||||
|
(commit-change
|
||||||
|
file
|
||||||
|
{:type :mod-obj
|
||||||
|
:page-id (:current-page-id file)
|
||||||
|
:id from-id
|
||||||
|
|
||||||
|
:operations
|
||||||
|
[{:type :set :attr :interactions :val interactions}]})))
|
||||||
|
|
||||||
|
(defn generate-changes
|
||||||
|
[file]
|
||||||
|
(:changes file))
|
||||||
|
|
||||||
|
(defn add-library-color
|
||||||
|
[file color]
|
||||||
|
|
||||||
|
(let [id (or (:id color) (uuid/next))]
|
||||||
|
(-> file
|
||||||
|
(commit-change
|
||||||
|
{:type :add-color
|
||||||
|
:id id
|
||||||
|
:color (assoc color :id id)})
|
||||||
|
(assoc :last-id id))))
|
||||||
|
|
||||||
|
(defn add-library-typography
|
||||||
|
[file typography]
|
||||||
|
(let [id (or (:id typography) (uuid/next))]
|
||||||
|
(-> file
|
||||||
|
(commit-change
|
||||||
|
{:type :add-typography
|
||||||
|
:id id
|
||||||
|
:typography (assoc typography :id id)})
|
||||||
|
(assoc :last-id id))))
|
||||||
|
|
||||||
|
(defn add-library-media
|
||||||
|
[file media]
|
||||||
|
(let [id (or (:id media) (uuid/next))]
|
||||||
|
(-> file
|
||||||
|
(commit-change
|
||||||
|
{:type :add-media
|
||||||
|
:object (assoc media :id id)})
|
||||||
|
(assoc :last-id id))))
|
||||||
|
|
||||||
|
(defn start-component
|
||||||
|
[file data]
|
||||||
|
|
||||||
|
(let [selrect init/empty-selrect
|
||||||
|
name (:name data)
|
||||||
|
path (:path data)
|
||||||
|
obj (-> (init/make-minimal-group nil selrect name)
|
||||||
|
(merge data)
|
||||||
|
(check-name file :group)
|
||||||
|
(d/without-nils))]
|
||||||
|
(-> file
|
||||||
|
(commit-change
|
||||||
|
{:type :add-component
|
||||||
|
:id (:id obj)
|
||||||
|
:name name
|
||||||
|
:path path
|
||||||
|
:shapes [obj]})
|
||||||
|
|
||||||
|
(assoc :last-id (:id obj))
|
||||||
|
(update :parent-stack conjv (:id obj))
|
||||||
|
(assoc :current-component-id (:id obj)))))
|
||||||
|
|
||||||
|
(defn finish-component
|
||||||
|
[file]
|
||||||
|
(let [component-id (:current-component-id file)
|
||||||
|
component (lookup-shape file component-id)
|
||||||
|
children (->> component :shapes (mapv #(lookup-shape file %)))
|
||||||
|
|
||||||
|
file
|
||||||
|
(cond
|
||||||
|
(empty? children)
|
||||||
|
(commit-change
|
||||||
|
file
|
||||||
|
{:type :del-component
|
||||||
|
:id component-id})
|
||||||
|
|
||||||
|
(:masked-group? component)
|
||||||
|
(let [mask (first children)]
|
||||||
|
(commit-change
|
||||||
|
file
|
||||||
|
{:type :mod-obj
|
||||||
|
:id component-id
|
||||||
|
:operations
|
||||||
|
[{:type :set :attr :x :val (-> mask :selrect :x)}
|
||||||
|
{:type :set :attr :y :val (-> mask :selrect :y)}
|
||||||
|
{:type :set :attr :width :val (-> mask :selrect :width)}
|
||||||
|
{:type :set :attr :height :val (-> mask :selrect :height)}
|
||||||
|
{:type :set :attr :flip-x :val (-> mask :flip-x)}
|
||||||
|
{:type :set :attr :flip-y :val (-> mask :flip-y)}
|
||||||
|
{:type :set :attr :selrect :val (-> mask :selrect)}
|
||||||
|
{:type :set :attr :points :val (-> mask :points)}]}
|
||||||
|
|
||||||
|
{:add-container? true}))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(let [component' (gsh/update-group-selrect component children)]
|
||||||
|
(commit-change
|
||||||
|
file
|
||||||
|
{:type :mod-obj
|
||||||
|
:id component-id
|
||||||
|
:operations
|
||||||
|
[{:type :set :attr :selrect :val (:selrect component')}
|
||||||
|
{:type :set :attr :points :val (:points component')}
|
||||||
|
{:type :set :attr :x :val (-> component' :selrect :x)}
|
||||||
|
{:type :set :attr :y :val (-> component' :selrect :y)}
|
||||||
|
{:type :set :attr :width :val (-> component' :selrect :width)}
|
||||||
|
{:type :set :attr :height :val (-> component' :selrect :height)}]}
|
||||||
|
|
||||||
|
{:add-container? true})))]
|
||||||
|
|
||||||
|
(-> file
|
||||||
|
(dissoc :current-component-id)
|
||||||
|
(update :parent-stack pop))))
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
(ns app.common.geom.align
|
(ns app.common.geom.align
|
||||||
(:require
|
(:require
|
||||||
[clojure.spec.alpha :as s]
|
[app.common.data :as d]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.data :as d]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
;; --- Alignment
|
;; --- Alignment
|
||||||
|
|
|
@ -8,10 +8,9 @@
|
||||||
(:require
|
(:require
|
||||||
#?(:cljs [cljs.pprint :as pp]
|
#?(:cljs [cljs.pprint :as pp]
|
||||||
:clj [clojure.pprint :as pp])
|
:clj [clojure.pprint :as pp])
|
||||||
[cuerdas.core :as str]
|
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.math :as mth]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.point :as gpt]))
|
[app.common.math :as mth]))
|
||||||
|
|
||||||
;; --- Matrix Impl
|
;; --- Matrix Impl
|
||||||
|
|
||||||
|
@ -27,6 +26,15 @@
|
||||||
([a b c d e f]
|
([a b c d e f]
|
||||||
(Matrix. a b c d e f)))
|
(Matrix. a b c d e f)))
|
||||||
|
|
||||||
|
(def number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?")
|
||||||
|
|
||||||
|
(defn str->matrix
|
||||||
|
[matrix-str]
|
||||||
|
(let [params (->> (re-seq number-regex matrix-str)
|
||||||
|
(filter #(-> % first seq))
|
||||||
|
(map (comp d/parse-double first)))]
|
||||||
|
(apply matrix params)))
|
||||||
|
|
||||||
(defn multiply
|
(defn multiply
|
||||||
([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
|
([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
|
||||||
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
|
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
|
||||||
|
@ -40,6 +48,21 @@
|
||||||
([m1 m2 & others]
|
([m1 m2 & others]
|
||||||
(reduce multiply (multiply m1 m2) others)))
|
(reduce multiply (multiply m1 m2) others)))
|
||||||
|
|
||||||
|
(defn add-translate
|
||||||
|
"Given two TRANSLATE matrixes (only e and f have significative
|
||||||
|
values), combine them. Quicker than multiplying them, for this
|
||||||
|
precise case."
|
||||||
|
([{m1e :e m1f :f} {m2e :e m2f :f}]
|
||||||
|
(Matrix.
|
||||||
|
1
|
||||||
|
0
|
||||||
|
0
|
||||||
|
1
|
||||||
|
(+ m1e m2e)
|
||||||
|
(+ m1f m2f)))
|
||||||
|
([m1 m2 & others]
|
||||||
|
(reduce add-translate (add-translate m1 m2) others)))
|
||||||
|
|
||||||
(defn substract
|
(defn substract
|
||||||
[{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
|
[{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
|
||||||
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
|
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
|
|
@ -7,51 +7,12 @@
|
||||||
(ns app.common.geom.shapes
|
(ns app.common.geom.shapes
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.math :as mth]
|
|
||||||
[app.common.geom.matrix :as gmt]
|
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes.common :as gco]
|
[app.common.geom.shapes.common :as gco]
|
||||||
|
[app.common.geom.shapes.intersect :as gin]
|
||||||
[app.common.geom.shapes.path :as gsp]
|
[app.common.geom.shapes.path :as gsp]
|
||||||
[app.common.geom.shapes.rect :as gpr]
|
[app.common.geom.shapes.rect :as gpr]
|
||||||
[app.common.geom.shapes.transforms :as gtr]
|
[app.common.geom.shapes.transforms :as gtr]))
|
||||||
[app.common.geom.shapes.intersect :as gin]
|
|
||||||
[app.common.spec :as us]))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Resize (Dimensions)
|
|
||||||
(defn resize-modifiers
|
|
||||||
[shape attr value]
|
|
||||||
(us/assert map? shape)
|
|
||||||
(us/assert #{:width :height} attr)
|
|
||||||
(us/assert number? value)
|
|
||||||
(let [{:keys [proportion proportion-lock]} shape
|
|
||||||
size (select-keys (:selrect shape) [:width :height])
|
|
||||||
new-size (if-not proportion-lock
|
|
||||||
(assoc size attr value)
|
|
||||||
(if (= attr :width)
|
|
||||||
(-> size
|
|
||||||
(assoc :width value)
|
|
||||||
(assoc :height (/ value proportion)))
|
|
||||||
(-> size
|
|
||||||
(assoc :height value)
|
|
||||||
(assoc :width (* value proportion)))))
|
|
||||||
width (:width new-size)
|
|
||||||
height (:height new-size)
|
|
||||||
|
|
||||||
shape-transform (:transform shape (gmt/matrix))
|
|
||||||
shape-transform-inv (:transform-inverse shape (gmt/matrix))
|
|
||||||
shape-center (gco/center-shape shape)
|
|
||||||
{sr-width :width sr-height :height} (:selrect shape)
|
|
||||||
|
|
||||||
origin (-> (gpt/point (:selrect shape))
|
|
||||||
(gtr/transform-point-center shape-center shape-transform))
|
|
||||||
|
|
||||||
scalev (gpt/divide (gpt/point width height)
|
|
||||||
(gpt/point sr-width sr-height))]
|
|
||||||
{:resize-vector scalev
|
|
||||||
:resize-origin origin
|
|
||||||
:resize-transform shape-transform
|
|
||||||
:resize-transform-inverse shape-transform-inv}))
|
|
||||||
|
|
||||||
;; --- Setup (Initialize)
|
;; --- Setup (Initialize)
|
||||||
;; FIXME: Is this the correct place for these functions?
|
;; FIXME: Is this the correct place for these functions?
|
||||||
|
@ -101,6 +62,10 @@
|
||||||
[shape {:keys [x y]}]
|
[shape {:keys [x y]}]
|
||||||
(gtr/move shape (gpt/negate (gpt/point x y))) )
|
(gtr/move shape (gpt/negate (gpt/point x y))) )
|
||||||
|
|
||||||
|
(defn translate-from-frame
|
||||||
|
[shape {:keys [x y]}]
|
||||||
|
(gtr/move shape (gpt/point x y)) )
|
||||||
|
|
||||||
;; --- Helpers
|
;; --- Helpers
|
||||||
|
|
||||||
(defn fully-contained?
|
(defn fully-contained?
|
||||||
|
@ -161,15 +126,6 @@
|
||||||
(assoc :selrect selrect
|
(assoc :selrect selrect
|
||||||
:points points))))
|
:points points))))
|
||||||
|
|
||||||
(defn rotation-modifiers
|
|
||||||
[shape center angle]
|
|
||||||
(let [displacement (let [shape-center (gco/center-shape shape)]
|
|
||||||
(-> (gmt/matrix)
|
|
||||||
(gmt/rotate angle center)
|
|
||||||
(gmt/rotate (- angle) shape-center)))]
|
|
||||||
{:rotation angle
|
|
||||||
:displacement displacement}))
|
|
||||||
|
|
||||||
|
|
||||||
;; EXPORTS
|
;; EXPORTS
|
||||||
(d/export gco/center-shape)
|
(d/export gco/center-shape)
|
||||||
|
@ -184,16 +140,20 @@
|
||||||
(d/export gpr/points->rect)
|
(d/export gpr/points->rect)
|
||||||
(d/export gpr/center->rect)
|
(d/export gpr/center->rect)
|
||||||
|
|
||||||
(d/export gtr/transform-shape)
|
(d/export gtr/move)
|
||||||
|
(d/export gtr/absolute-move)
|
||||||
(d/export gtr/transform-matrix)
|
(d/export gtr/transform-matrix)
|
||||||
(d/export gtr/inverse-transform-matrix)
|
(d/export gtr/inverse-transform-matrix)
|
||||||
(d/export gtr/transform-point-center)
|
(d/export gtr/transform-point-center)
|
||||||
(d/export gtr/transform-rect)
|
|
||||||
(d/export gtr/update-group-selrect)
|
|
||||||
(d/export gtr/transform-points)
|
(d/export gtr/transform-points)
|
||||||
|
(d/export gtr/transform-rect)
|
||||||
(d/export gtr/calculate-adjust-matrix)
|
(d/export gtr/calculate-adjust-matrix)
|
||||||
(d/export gtr/move)
|
(d/export gtr/update-group-selrect)
|
||||||
(d/export gtr/absolute-move)
|
(d/export gtr/resize-modifiers)
|
||||||
|
(d/export gtr/rotation-modifiers)
|
||||||
|
(d/export gtr/merge-modifiers)
|
||||||
|
(d/export gtr/transform-shape)
|
||||||
|
(d/export gtr/calc-child-modifiers)
|
||||||
|
|
||||||
;; PATHS
|
;; PATHS
|
||||||
(d/export gsp/content->points)
|
(d/export gsp/content->points)
|
||||||
|
@ -204,3 +164,4 @@
|
||||||
(d/export gin/overlaps?)
|
(d/export gin/overlaps?)
|
||||||
(d/export gin/has-point?)
|
(d/export gin/has-point?)
|
||||||
(d/export gin/has-point-rect?)
|
(d/export gin/has-point-rect?)
|
||||||
|
(d/export gin/rect-contains-shape?)
|
|
@ -6,9 +6,8 @@
|
||||||
|
|
||||||
(ns app.common.geom.shapes.intersect
|
(ns app.common.geom.shapes.intersect
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.geom.point :as gpt]
|
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes.path :as gpp]
|
[app.common.geom.shapes.path :as gpp]
|
||||||
[app.common.geom.shapes.rect :as gpr]
|
[app.common.geom.shapes.rect :as gpr]
|
||||||
[app.common.math :as mth]))
|
[app.common.math :as mth]))
|
||||||
|
@ -113,11 +112,10 @@
|
||||||
;; Even-odd algorithm
|
;; Even-odd algorithm
|
||||||
;; Cast a ray from the point in any direction and count the intersections
|
;; Cast a ray from the point in any direction and count the intersections
|
||||||
;; if it's odd the point is inside the polygon
|
;; if it's odd the point is inside the polygon
|
||||||
(let []
|
|
||||||
(->> lines
|
(->> lines
|
||||||
(filter #(intersect-ray? p %))
|
(filter #(intersect-ray? p %))
|
||||||
(count)
|
(count)
|
||||||
(odd?))))
|
(odd?)))
|
||||||
|
|
||||||
(defn- next-windup
|
(defn- next-windup
|
||||||
"Calculates the next windup number for the nonzero algorithm"
|
"Calculates the next windup number for the nonzero algorithm"
|
||||||
|
@ -197,14 +195,14 @@
|
||||||
|
|
||||||
(let [center (gpt/point cx cy)
|
(let [center (gpt/point cx cy)
|
||||||
transform (gmt/transform-in center transform)
|
transform (gmt/transform-in center transform)
|
||||||
{px :x py :y} (gpt/transform point transform)]
|
{px :x py :y} (gpt/transform point transform)
|
||||||
;; Ellipse inequality formula
|
;; Ellipse inequality formula
|
||||||
;; https://en.wikipedia.org/wiki/Ellipse#Shifted_ellipse
|
;; https://en.wikipedia.org/wiki/Ellipse#Shifted_ellipse
|
||||||
(let [v (+ (/ (mth/sq (- px cx))
|
v (+ (/ (mth/sq (- px cx))
|
||||||
(mth/sq rx))
|
(mth/sq rx))
|
||||||
(/ (mth/sq (- py cy))
|
(/ (mth/sq (- py cy))
|
||||||
(mth/sq ry)))]
|
(mth/sq ry)))]
|
||||||
(<= v 1))))
|
(<= v 1)))
|
||||||
|
|
||||||
(defn intersects-line-ellipse?
|
(defn intersects-line-ellipse?
|
||||||
"Checks wether a single line intersects with the given ellipse"
|
"Checks wether a single line intersects with the given ellipse"
|
||||||
|
@ -304,3 +302,9 @@
|
||||||
(let [lines (points->lines (:points shape))]
|
(let [lines (points->lines (:points shape))]
|
||||||
;; TODO: Will only work for simple shapes
|
;; TODO: Will only work for simple shapes
|
||||||
(is-point-inside-evenodd? point lines)))
|
(is-point-inside-evenodd? point lines)))
|
||||||
|
|
||||||
|
(defn rect-contains-shape?
|
||||||
|
[rect shape]
|
||||||
|
(->> shape
|
||||||
|
:points
|
||||||
|
(every? (partial has-point-rect? rect))))
|
|
@ -6,10 +6,10 @@
|
||||||
|
|
||||||
(ns app.common.geom.shapes.path
|
(ns app.common.geom.shapes.path
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes.rect :as gpr]
|
[app.common.geom.shapes.rect :as gpr]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]))
|
||||||
[app.common.data :as d]))
|
|
||||||
|
|
||||||
(defn content->points [content]
|
(defn content->points [content]
|
||||||
(->> content
|
(->> content
|
||||||
|
@ -156,7 +156,8 @@
|
||||||
|
|
||||||
(mapv #(update % :params transform-params) content)))
|
(mapv #(update % :params transform-params) content)))
|
||||||
|
|
||||||
(defn transform-content [content transform]
|
(defn transform-content
|
||||||
|
[content transform]
|
||||||
(let [set-tr (fn [params px py]
|
(let [set-tr (fn [params px py]
|
||||||
(let [tr-point (-> (gpt/point (get params px) (get params py))
|
(let [tr-point (-> (gpt/point (get params px) (get params py))
|
||||||
(gpt/transform transform))]
|
(gpt/transform transform))]
|
693
common/src/app/common/geom/shapes/transforms.cljc
Normal file
693
common/src/app/common/geom/shapes/transforms.cljc
Normal file
|
@ -0,0 +1,693 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.common.geom.shapes.transforms
|
||||||
|
(:require
|
||||||
|
[app.common.attrs :as attrs]
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.geom.matrix :as gmt]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
|
[app.common.geom.shapes.common :as gco]
|
||||||
|
[app.common.geom.shapes.path :as gpa]
|
||||||
|
[app.common.geom.shapes.rect :as gpr]
|
||||||
|
[app.common.math :as mth]
|
||||||
|
[app.common.pages.spec :as spec]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.text :as txt]))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- Relative Movement
|
||||||
|
|
||||||
|
(defn- move-selrect [selrect {dx :x dy :y}]
|
||||||
|
(-> selrect
|
||||||
|
(d/update-when :x + dx)
|
||||||
|
(d/update-when :y + dy)
|
||||||
|
(d/update-when :x1 + dx)
|
||||||
|
(d/update-when :y1 + dy)
|
||||||
|
(d/update-when :x2 + dx)
|
||||||
|
(d/update-when :y2 + dy)))
|
||||||
|
|
||||||
|
(defn- move-points [points move-vec]
|
||||||
|
(->> points
|
||||||
|
(mapv #(gpt/add % move-vec))))
|
||||||
|
|
||||||
|
(defn move
|
||||||
|
"Move the shape relativelly to its current
|
||||||
|
position applying the provided delta."
|
||||||
|
[shape {dx :x dy :y}]
|
||||||
|
(let [dx (d/check-num dx)
|
||||||
|
dy (d/check-num dy)
|
||||||
|
move-vec (gpt/point dx dy)]
|
||||||
|
|
||||||
|
(-> shape
|
||||||
|
(update :selrect move-selrect move-vec)
|
||||||
|
(update :points move-points move-vec)
|
||||||
|
(d/update-when :x + dx)
|
||||||
|
(d/update-when :y + dy)
|
||||||
|
(cond-> (= :path (:type shape))
|
||||||
|
(update :content gpa/move-content move-vec)))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- Absolute Movement
|
||||||
|
|
||||||
|
(defn absolute-move
|
||||||
|
"Move the shape to the exactly specified position."
|
||||||
|
[shape {:keys [x y]}]
|
||||||
|
(let [dx (- (d/check-num x) (-> shape :selrect :x))
|
||||||
|
dy (- (d/check-num y) (-> shape :selrect :y))]
|
||||||
|
(move shape (gpt/point dx dy))))
|
||||||
|
|
||||||
|
|
||||||
|
; ---- Geometric operations
|
||||||
|
|
||||||
|
(defn- normalize-scale
|
||||||
|
"We normalize the scale so it's not too close to 0"
|
||||||
|
[scale]
|
||||||
|
(cond
|
||||||
|
(and (< scale 0) (> scale -0.01)) -0.01
|
||||||
|
(and (>= scale 0) (< scale 0.01)) 0.01
|
||||||
|
:else scale))
|
||||||
|
|
||||||
|
(defn- calculate-skew-angle
|
||||||
|
"Calculates the skew angle of the paralelogram given by the points"
|
||||||
|
[[p1 _ p3 p4]]
|
||||||
|
(let [v1 (gpt/to-vec p3 p4)
|
||||||
|
v2 (gpt/to-vec p4 p1)]
|
||||||
|
;; If one of the vectors is zero it's a rectangle with 0 height or width
|
||||||
|
;; We don't skew these
|
||||||
|
(if (or (gpt/almost-zero? v1)
|
||||||
|
(gpt/almost-zero? v2))
|
||||||
|
0
|
||||||
|
(- 90 (gpt/angle-with-other v1 v2)))))
|
||||||
|
|
||||||
|
(defn- calculate-height
|
||||||
|
"Calculates the height of a paralelogram given by the points"
|
||||||
|
[[p1 _ _ p4]]
|
||||||
|
(-> (gpt/to-vec p4 p1)
|
||||||
|
(gpt/length)))
|
||||||
|
|
||||||
|
(defn- calculate-width
|
||||||
|
"Calculates the width of a paralelogram given by the points"
|
||||||
|
[[p1 p2 _ _]]
|
||||||
|
(-> (gpt/to-vec p1 p2)
|
||||||
|
(gpt/length)))
|
||||||
|
|
||||||
|
(defn- calculate-rotation
|
||||||
|
"Calculates the rotation between two shapes given the resize vector direction"
|
||||||
|
[center points-shape1 points-shape2 flip-x flip-y]
|
||||||
|
|
||||||
|
(let [idx-1 0
|
||||||
|
idx-2 (cond (and flip-x (not flip-y)) 1
|
||||||
|
(and flip-x flip-y) 2
|
||||||
|
(and (not flip-x) flip-y) 3
|
||||||
|
:else 0)
|
||||||
|
p1 (nth points-shape1 idx-1)
|
||||||
|
p2 (nth points-shape2 idx-2)
|
||||||
|
v1 (gpt/to-vec center p1)
|
||||||
|
v2 (gpt/to-vec center p2)
|
||||||
|
|
||||||
|
rot-angle (gpt/angle-with-other v1 v2)
|
||||||
|
rot-sign (gpt/angle-sign v1 v2)]
|
||||||
|
(* rot-sign rot-angle)))
|
||||||
|
|
||||||
|
(defn- calculate-dimensions
|
||||||
|
[[p1 p2 p3 _]]
|
||||||
|
(let [width (gpt/distance p1 p2)
|
||||||
|
height (gpt/distance p2 p3)]
|
||||||
|
{:width width :height height}))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- Transformation matrix operations
|
||||||
|
|
||||||
|
(defn transform-matrix
|
||||||
|
"Returns a transformation matrix without changing the shape properties.
|
||||||
|
The result should be used in a `transform` attribute in svg"
|
||||||
|
([shape] (transform-matrix shape nil))
|
||||||
|
([shape params] (transform-matrix shape params (or (gco/center-shape shape)
|
||||||
|
(gpt/point 0 0))))
|
||||||
|
([{:keys [flip-x flip-y] :as shape} {:keys [no-flip]} shape-center]
|
||||||
|
(-> (gmt/matrix)
|
||||||
|
(gmt/translate shape-center)
|
||||||
|
|
||||||
|
(gmt/multiply (:transform shape (gmt/matrix)))
|
||||||
|
(cond->
|
||||||
|
(and (not no-flip) flip-x) (gmt/scale (gpt/point -1 1))
|
||||||
|
(and (not no-flip) flip-y) (gmt/scale (gpt/point 1 -1)))
|
||||||
|
(gmt/translate (gpt/negate shape-center)))))
|
||||||
|
|
||||||
|
(defn inverse-transform-matrix
|
||||||
|
([shape]
|
||||||
|
(let [shape-center (or (gco/center-shape shape)
|
||||||
|
(gpt/point 0 0))]
|
||||||
|
(inverse-transform-matrix shape shape-center)))
|
||||||
|
([{:keys [flip-x flip-y] :as shape} center]
|
||||||
|
(-> (gmt/matrix)
|
||||||
|
(gmt/translate center)
|
||||||
|
(cond->
|
||||||
|
flip-x (gmt/scale (gpt/point -1 1))
|
||||||
|
flip-y (gmt/scale (gpt/point 1 -1)))
|
||||||
|
(gmt/multiply (:transform-inverse shape (gmt/matrix)))
|
||||||
|
(gmt/translate (gpt/negate center)))))
|
||||||
|
|
||||||
|
(defn transform-point-center
|
||||||
|
"Transform a point around the shape center"
|
||||||
|
[point center matrix]
|
||||||
|
(gpt/transform
|
||||||
|
point
|
||||||
|
(gmt/multiply (gmt/translate-matrix center)
|
||||||
|
matrix
|
||||||
|
(gmt/translate-matrix (gpt/negate center)))))
|
||||||
|
|
||||||
|
(defn transform-points
|
||||||
|
([points matrix]
|
||||||
|
(transform-points points nil matrix))
|
||||||
|
([points center matrix]
|
||||||
|
(let [prev (if center (gmt/translate-matrix center) (gmt/matrix))
|
||||||
|
post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix))
|
||||||
|
|
||||||
|
tr-point (fn [point]
|
||||||
|
(gpt/transform point (gmt/multiply prev matrix post)))]
|
||||||
|
(mapv tr-point points))))
|
||||||
|
|
||||||
|
(defn transform-rect
|
||||||
|
"Transform a rectangles and changes its attributes"
|
||||||
|
[rect matrix]
|
||||||
|
|
||||||
|
(let [points (-> (gpr/rect->points rect)
|
||||||
|
(transform-points matrix))]
|
||||||
|
(gpr/points->rect points)))
|
||||||
|
|
||||||
|
(defn calculate-adjust-matrix
|
||||||
|
"Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that
|
||||||
|
after applying them the end result is the `shape-pathn-temp`.
|
||||||
|
This is compose of three transformations: skew, resize and rotation"
|
||||||
|
([points-temp points-rec] (calculate-adjust-matrix points-temp points-rec false false))
|
||||||
|
([points-temp points-rec flip-x flip-y]
|
||||||
|
(let [center (gco/center-points points-temp)
|
||||||
|
|
||||||
|
stretch-matrix (gmt/matrix)
|
||||||
|
|
||||||
|
skew-angle (calculate-skew-angle points-temp)
|
||||||
|
|
||||||
|
;; When one of the axis is flipped we have to reverse the skew
|
||||||
|
;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
|
||||||
|
skew-angle (if (and (or flip-x flip-y)
|
||||||
|
(not (and flip-x flip-y))) (- skew-angle) skew-angle )
|
||||||
|
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
|
||||||
|
|
||||||
|
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
|
||||||
|
|
||||||
|
h1 (max 1 (calculate-height points-temp))
|
||||||
|
h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix)))
|
||||||
|
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1)
|
||||||
|
h3 (if (mth/nan? h3) 1 h3)
|
||||||
|
|
||||||
|
w1 (max 1 (calculate-width points-temp))
|
||||||
|
w2 (max 1 (calculate-width (transform-points points-rec center stretch-matrix)))
|
||||||
|
w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1)
|
||||||
|
w3 (if (mth/nan? w3) 1 w3)
|
||||||
|
|
||||||
|
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point w3 h3)))
|
||||||
|
|
||||||
|
rotation-angle (calculate-rotation
|
||||||
|
center
|
||||||
|
(transform-points points-rec (gco/center-points points-rec) stretch-matrix)
|
||||||
|
points-temp
|
||||||
|
flip-x
|
||||||
|
flip-y)
|
||||||
|
|
||||||
|
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
|
||||||
|
|
||||||
|
;; This is the inverse to be able to remove the transformation
|
||||||
|
stretch-matrix-inverse (-> (gmt/matrix)
|
||||||
|
(gmt/scale (gpt/point (/ 1 w3) (/ 1 h3)))
|
||||||
|
(gmt/skew (- skew-angle) 0)
|
||||||
|
(gmt/rotate (- rotation-angle)))]
|
||||||
|
[stretch-matrix stretch-matrix-inverse rotation-angle])))
|
||||||
|
|
||||||
|
(defn- apply-transform
|
||||||
|
"Given a new set of points transformed, set up the rectangle so it keeps
|
||||||
|
its properties. We adjust de x,y,width,height and create a custom transform"
|
||||||
|
[shape transform round-coords?]
|
||||||
|
;;
|
||||||
|
(let [points (-> shape :points (transform-points transform))
|
||||||
|
center (gco/center-points points)
|
||||||
|
|
||||||
|
;; Reverse the current transformation stack to get the base rectangle
|
||||||
|
tr-inverse (:transform-inverse shape (gmt/matrix))
|
||||||
|
|
||||||
|
points-temp (transform-points points center tr-inverse)
|
||||||
|
points-temp-dim (calculate-dimensions points-temp)
|
||||||
|
|
||||||
|
;; This rectangle is the new data for the current rectangle. We want to change our rectangle
|
||||||
|
;; to have this width, height, x, y
|
||||||
|
rect-shape (-> (gco/make-centered-rect
|
||||||
|
center
|
||||||
|
(:width points-temp-dim)
|
||||||
|
(:height points-temp-dim))
|
||||||
|
(update :width max 1)
|
||||||
|
(update :height max 1))
|
||||||
|
|
||||||
|
rect-points (gpr/rect->points rect-shape)
|
||||||
|
|
||||||
|
[matrix matrix-inverse] (calculate-adjust-matrix points-temp rect-points (:flip-x shape) (:flip-y shape))
|
||||||
|
|
||||||
|
rect-shape (cond-> rect-shape
|
||||||
|
round-coords?
|
||||||
|
(-> (update :x mth/round)
|
||||||
|
(update :y mth/round)
|
||||||
|
(update :width mth/round)
|
||||||
|
(update :height mth/round)))
|
||||||
|
|
||||||
|
shape (cond
|
||||||
|
(= :path (:type shape))
|
||||||
|
(-> shape
|
||||||
|
(update :content #(gpa/transform-content % transform)))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(-> shape
|
||||||
|
(merge rect-shape)))
|
||||||
|
|
||||||
|
base-rotation (or (:rotation shape) 0)
|
||||||
|
modif-rotation (or (get-in shape [:modifiers :rotation]) 0)]
|
||||||
|
|
||||||
|
(as-> shape $
|
||||||
|
(update $ :transform #(gmt/multiply (or % (gmt/matrix)) matrix))
|
||||||
|
(update $ :transform-inverse #(gmt/multiply matrix-inverse (or % (gmt/matrix))))
|
||||||
|
(assoc $ :points (into [] points))
|
||||||
|
(assoc $ :selrect (gpr/rect->selrect rect-shape))
|
||||||
|
(assoc $ :rotation (mod (+ base-rotation modif-rotation) 360)))))
|
||||||
|
|
||||||
|
(defn- update-group-viewbox
|
||||||
|
"Updates the viewbox for groups imported from SVG's"
|
||||||
|
[{:keys [selrect svg-viewbox] :as group} new-selrect]
|
||||||
|
(let [;; Gets deltas for the selrect to update the svg-viewbox (for svg-imports)
|
||||||
|
deltas {:x (- (:x new-selrect 0) (:x selrect 0))
|
||||||
|
:y (- (:y new-selrect 0) (:y selrect 0))
|
||||||
|
:width (- (:width new-selrect 1) (:width selrect 1))
|
||||||
|
:height (- (:height new-selrect 1) (:height selrect 1))}]
|
||||||
|
|
||||||
|
(cond-> group
|
||||||
|
(and (some? svg-viewbox) (some? selrect) (some? new-selrect))
|
||||||
|
(update :svg-viewbox
|
||||||
|
#(-> %
|
||||||
|
(update :x + (:x deltas))
|
||||||
|
(update :y + (:y deltas))
|
||||||
|
(update :width + (:width deltas))
|
||||||
|
(update :height + (:height deltas)))))))
|
||||||
|
|
||||||
|
(defn update-group-selrect [group children]
|
||||||
|
(let [shape-center (gco/center-shape group)
|
||||||
|
;; Points for every shape inside the group
|
||||||
|
points (->> children (mapcat :points))
|
||||||
|
|
||||||
|
;; Invert to get the points minus the transforms applied to the group
|
||||||
|
base-points (transform-points points shape-center (:transform-inverse group (gmt/matrix)))
|
||||||
|
|
||||||
|
;; Defines the new selection rect with its transformations
|
||||||
|
new-points (-> (gpr/points->selrect base-points)
|
||||||
|
(gpr/rect->points)
|
||||||
|
(transform-points shape-center (:transform group (gmt/matrix))))
|
||||||
|
|
||||||
|
;; Calculte the new selrect
|
||||||
|
new-selrect (gpr/points->selrect base-points)]
|
||||||
|
|
||||||
|
;; Updates the shape and the applytransform-rect will update the other properties
|
||||||
|
(-> group
|
||||||
|
(update-group-viewbox new-selrect)
|
||||||
|
(assoc :selrect new-selrect)
|
||||||
|
(assoc :points new-points)
|
||||||
|
|
||||||
|
;; We're regenerating the selrect from its children so we
|
||||||
|
;; need to remove the flip flags
|
||||||
|
(assoc :flip-x false)
|
||||||
|
(assoc :flip-y false)
|
||||||
|
(apply-transform (gmt/matrix) true))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- Modifiers
|
||||||
|
|
||||||
|
;; The `modifiers` structure contains a list of transformations to
|
||||||
|
;; do make to a shape, in this order:
|
||||||
|
;;
|
||||||
|
;; - resize-origin (gpt/point) + resize-vector (gpt/point)
|
||||||
|
;; apply a scale vector to all points of the shapes, starting
|
||||||
|
;; from the origin point.
|
||||||
|
;;
|
||||||
|
;; - resize-origin-2 + resize-vector-2
|
||||||
|
;; same as the previous one, for cases in that we need to make
|
||||||
|
;; two vectors from different origin points.
|
||||||
|
;;
|
||||||
|
;; - displacement (gmt/matrix)
|
||||||
|
;; apply a translation matrix to the shape
|
||||||
|
;;
|
||||||
|
;; - rotation (gmt/matrix)
|
||||||
|
;; apply a rotation matrix to the shape
|
||||||
|
;;
|
||||||
|
;; - resize-transform (gmt/matrix) + resize-transform-inverse (gmt/matrix)
|
||||||
|
;; a copy of the rotation matrix currently applied to the shape;
|
||||||
|
;; this is needed temporarily to apply the resize vectors.
|
||||||
|
;;
|
||||||
|
;; - resize-scale-text (bool)
|
||||||
|
;; tells if the resize vectors must be applied to text shapes
|
||||||
|
;; or not.
|
||||||
|
|
||||||
|
(defn resize-modifiers
|
||||||
|
[shape attr value]
|
||||||
|
(us/assert map? shape)
|
||||||
|
(us/assert #{:width :height} attr)
|
||||||
|
(us/assert number? value)
|
||||||
|
(let [{:keys [proportion proportion-lock]} shape
|
||||||
|
size (select-keys (:selrect shape) [:width :height])
|
||||||
|
new-size (if-not proportion-lock
|
||||||
|
(assoc size attr value)
|
||||||
|
(if (= attr :width)
|
||||||
|
(-> size
|
||||||
|
(assoc :width value)
|
||||||
|
(assoc :height (/ value proportion)))
|
||||||
|
(-> size
|
||||||
|
(assoc :height value)
|
||||||
|
(assoc :width (* value proportion)))))
|
||||||
|
width (:width new-size)
|
||||||
|
height (:height new-size)
|
||||||
|
|
||||||
|
shape-transform (:transform shape (gmt/matrix))
|
||||||
|
shape-transform-inv (:transform-inverse shape (gmt/matrix))
|
||||||
|
shape-center (gco/center-shape shape)
|
||||||
|
{sr-width :width sr-height :height} (:selrect shape)
|
||||||
|
|
||||||
|
origin (-> (gpt/point (:selrect shape))
|
||||||
|
(transform-point-center shape-center shape-transform))
|
||||||
|
|
||||||
|
scalev (gpt/divide (gpt/point width height)
|
||||||
|
(gpt/point sr-width sr-height))]
|
||||||
|
{:resize-vector scalev
|
||||||
|
:resize-origin origin
|
||||||
|
:resize-transform shape-transform
|
||||||
|
:resize-transform-inverse shape-transform-inv}))
|
||||||
|
|
||||||
|
(defn rotation-modifiers
|
||||||
|
[shape center angle]
|
||||||
|
(let [displacement (let [shape-center (gco/center-shape shape)]
|
||||||
|
(-> (gmt/matrix)
|
||||||
|
(gmt/rotate angle center)
|
||||||
|
(gmt/rotate (- angle) shape-center)))]
|
||||||
|
{:rotation angle
|
||||||
|
:displacement displacement}))
|
||||||
|
|
||||||
|
(defn merge-modifiers
|
||||||
|
[objects modifiers]
|
||||||
|
|
||||||
|
(let [set-modifier
|
||||||
|
(fn [objects [id modifiers]]
|
||||||
|
(-> objects
|
||||||
|
(d/update-when id merge modifiers)))]
|
||||||
|
(->> modifiers
|
||||||
|
(reduce set-modifier objects))))
|
||||||
|
|
||||||
|
(defn- modifiers->transform
|
||||||
|
[center modifiers]
|
||||||
|
(let [ds-modifier (:displacement modifiers (gmt/matrix))
|
||||||
|
{res-x :x res-y :y} (:resize-vector modifiers (gpt/point 1 1))
|
||||||
|
{res-x-2 :x res-y-2 :y} (:resize-vector-2 modifiers (gpt/point 1 1))
|
||||||
|
|
||||||
|
;; Normalize x/y vector coordinates because scale by 0 is infinite
|
||||||
|
res-x (normalize-scale res-x)
|
||||||
|
res-y (normalize-scale res-y)
|
||||||
|
resize (gpt/point res-x res-y)
|
||||||
|
|
||||||
|
res-x-2 (normalize-scale res-x-2)
|
||||||
|
res-y-2 (normalize-scale res-y-2)
|
||||||
|
resize-2 (gpt/point res-x-2 res-y-2)
|
||||||
|
|
||||||
|
origin (:resize-origin modifiers (gpt/point 0 0))
|
||||||
|
origin-2 (:resize-origin-2 modifiers (gpt/point 0 0))
|
||||||
|
|
||||||
|
resize-transform (:resize-transform modifiers (gmt/matrix))
|
||||||
|
resize-transform-inverse (:resize-transform-inverse modifiers (gmt/matrix))
|
||||||
|
rt-modif (or (:rotation modifiers) 0)
|
||||||
|
|
||||||
|
center (gpt/transform center ds-modifier)
|
||||||
|
|
||||||
|
transform (-> (gmt/matrix)
|
||||||
|
|
||||||
|
;; Applies the current resize transformation
|
||||||
|
(gmt/translate origin)
|
||||||
|
(gmt/multiply resize-transform)
|
||||||
|
(gmt/scale resize)
|
||||||
|
(gmt/multiply resize-transform-inverse)
|
||||||
|
(gmt/translate (gpt/negate origin))
|
||||||
|
|
||||||
|
(gmt/translate origin-2)
|
||||||
|
(gmt/multiply resize-transform)
|
||||||
|
(gmt/scale resize-2)
|
||||||
|
(gmt/multiply resize-transform-inverse)
|
||||||
|
(gmt/translate (gpt/negate origin-2))
|
||||||
|
|
||||||
|
;; Applies the stacked transformations
|
||||||
|
(gmt/translate center)
|
||||||
|
(gmt/multiply (gmt/rotate-matrix rt-modif))
|
||||||
|
(gmt/translate (gpt/negate center))
|
||||||
|
|
||||||
|
;; Displacement
|
||||||
|
(gmt/multiply ds-modifier))]
|
||||||
|
transform))
|
||||||
|
|
||||||
|
(defn- set-flip [shape modifiers]
|
||||||
|
(let [rx (get-in modifiers [:resize-vector :x])
|
||||||
|
ry (get-in modifiers [:resize-vector :y])]
|
||||||
|
(cond-> shape
|
||||||
|
(and rx (< rx 0)) (-> (update :flip-x not)
|
||||||
|
(update :rotation -))
|
||||||
|
(and ry (< ry 0)) (-> (update :flip-y not)
|
||||||
|
(update :rotation -)))))
|
||||||
|
|
||||||
|
(defn- apply-displacement [shape]
|
||||||
|
(let [modifiers (:modifiers shape)]
|
||||||
|
(if (contains? modifiers :displacement)
|
||||||
|
(let [mov-vec (-> (gpt/point 0 0)
|
||||||
|
(gpt/transform (:displacement modifiers)))
|
||||||
|
shape (move shape mov-vec)
|
||||||
|
modifiers (dissoc modifiers :displacement)]
|
||||||
|
(-> shape
|
||||||
|
(assoc :modifiers modifiers)
|
||||||
|
(cond-> (empty? modifiers)
|
||||||
|
(dissoc :modifiers))))
|
||||||
|
shape)))
|
||||||
|
|
||||||
|
(defn- apply-text-resize
|
||||||
|
[shape modifiers]
|
||||||
|
(if (and (= (:type shape) :text)
|
||||||
|
(:resize-scale-text modifiers))
|
||||||
|
(let [merge-attrs (fn [attrs]
|
||||||
|
(let [font-size (-> (get attrs :font-size 14)
|
||||||
|
(d/parse-double)
|
||||||
|
(* (get-in modifiers [:resize-vector :x] 1))
|
||||||
|
(* (get-in modifiers [:resize-vector-2 :x] 1))
|
||||||
|
(str))]
|
||||||
|
(attrs/merge attrs {:font-size font-size})))]
|
||||||
|
(update shape :content #(txt/transform-nodes
|
||||||
|
txt/is-text-node?
|
||||||
|
merge-attrs
|
||||||
|
%)))
|
||||||
|
shape))
|
||||||
|
|
||||||
|
(defn transform-shape
|
||||||
|
([shape]
|
||||||
|
(transform-shape shape nil))
|
||||||
|
|
||||||
|
([shape {:keys [round-coords?]
|
||||||
|
:or {round-coords? true}}]
|
||||||
|
(let [shape (apply-displacement shape)
|
||||||
|
center (gco/center-shape shape)
|
||||||
|
modifiers (:modifiers shape)]
|
||||||
|
(if (and modifiers center)
|
||||||
|
(let [transform (modifiers->transform center modifiers)]
|
||||||
|
(-> shape
|
||||||
|
(set-flip modifiers)
|
||||||
|
(apply-transform transform round-coords?)
|
||||||
|
(apply-text-resize modifiers)
|
||||||
|
(dissoc :modifiers)))
|
||||||
|
shape))))
|
||||||
|
|
||||||
|
(defn calc-child-modifiers
|
||||||
|
"Given the modifiers to apply to the parent, calculate the corresponding
|
||||||
|
modifiers for the child, depending on the child constraints."
|
||||||
|
[parent child parent-modifiers]
|
||||||
|
(let [parent-rect (:selrect parent)
|
||||||
|
child-rect (:selrect child)
|
||||||
|
|
||||||
|
;; Apply the modifiers to the parent's selrect, to check the difference with
|
||||||
|
;; the original, and calculate child transformations from this.
|
||||||
|
;;
|
||||||
|
;; Note that a shape's selrect is always "horizontal" (i.e. without applying
|
||||||
|
;; the shape transform, that may include some rotation and skew). Thus, to
|
||||||
|
;; apply the modifiers, we first apply to them the transform-inverse.
|
||||||
|
parent-displacement (-> (gpt/point 0 0)
|
||||||
|
(gpt/transform (get parent-modifiers :displacement (gmt/matrix)))
|
||||||
|
(gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix)))
|
||||||
|
(gmt/translate-matrix))
|
||||||
|
parent-origin (-> (:resize-origin parent-modifiers)
|
||||||
|
((d/nilf transform-point-center)
|
||||||
|
(gco/center-shape parent)
|
||||||
|
(:resize-transform-inverse parent-modifiers (gmt/matrix))))
|
||||||
|
parent-origin-2 (-> (:resize-origin-2 parent-modifiers)
|
||||||
|
((d/nilf transform-point-center)
|
||||||
|
(gco/center-shape parent)
|
||||||
|
(:resize-transform-inverse parent-modifiers (gmt/matrix))))
|
||||||
|
parent-vector (get parent-modifiers :resize-vector (gpt/point 1 1))
|
||||||
|
parent-vector-2 (get parent-modifiers :resize-vector-2 (gpt/point 1 1))
|
||||||
|
|
||||||
|
transformed-parent-rect (-> parent-rect
|
||||||
|
(gpr/rect->points)
|
||||||
|
(transform-points parent-displacement)
|
||||||
|
(transform-points parent-origin (gmt/scale-matrix parent-vector))
|
||||||
|
(transform-points parent-origin-2 (gmt/scale-matrix parent-vector-2))
|
||||||
|
(gpr/points->selrect))
|
||||||
|
|
||||||
|
;; Calculate the modifiers in the horizontal and vertical directions
|
||||||
|
;; depending on the child constraints.
|
||||||
|
constraints-h (get child :constraints-h (spec/default-constraints-h child))
|
||||||
|
constraints-v (get child :constraints-v (spec/default-constraints-v child))
|
||||||
|
|
||||||
|
modifiers-h (case constraints-h
|
||||||
|
:left
|
||||||
|
(let [delta-left (- (:x1 transformed-parent-rect) (:x1 parent-rect))]
|
||||||
|
|
||||||
|
(if-not (mth/almost-zero? delta-left)
|
||||||
|
{:displacement (gpt/point delta-left 0)} ;; we convert to matrix below
|
||||||
|
{}))
|
||||||
|
|
||||||
|
:right
|
||||||
|
(let [delta-right (- (:x2 transformed-parent-rect) (:x2 parent-rect))]
|
||||||
|
(if-not (mth/almost-zero? delta-right)
|
||||||
|
{:displacement (gpt/point delta-right 0)}
|
||||||
|
{}))
|
||||||
|
|
||||||
|
:leftright
|
||||||
|
(let [delta-left (- (:x1 transformed-parent-rect) (:x1 parent-rect))
|
||||||
|
delta-width (- (:width transformed-parent-rect) (:width parent-rect))]
|
||||||
|
(if (or (not (mth/almost-zero? delta-left))
|
||||||
|
(not (mth/almost-zero? delta-width)))
|
||||||
|
{:displacement (gpt/point delta-left 0)
|
||||||
|
:resize-origin (-> (gpt/point (+ (:x1 child-rect) delta-left)
|
||||||
|
(:y1 child-rect))
|
||||||
|
(transform-point-center
|
||||||
|
(gco/center-rect child-rect)
|
||||||
|
(:transform child (gmt/matrix))))
|
||||||
|
:resize-vector (gpt/point (/ (+ (:width child-rect) delta-width)
|
||||||
|
(:width child-rect)) 1)}
|
||||||
|
{}))
|
||||||
|
|
||||||
|
:center
|
||||||
|
(let [parent-center (gco/center-rect parent-rect)
|
||||||
|
transformed-parent-center (gco/center-rect transformed-parent-rect)
|
||||||
|
delta-center (- (:x transformed-parent-center) (:x parent-center))]
|
||||||
|
(if-not (mth/almost-zero? delta-center)
|
||||||
|
{:displacement (gpt/point delta-center 0)}
|
||||||
|
{}))
|
||||||
|
|
||||||
|
:scale
|
||||||
|
(cond-> {}
|
||||||
|
(and (:resize-vector parent-modifiers)
|
||||||
|
(not (mth/close? (:x (:resize-vector parent-modifiers)) 1)))
|
||||||
|
(assoc :resize-origin (:resize-origin parent-modifiers)
|
||||||
|
:resize-vector (gpt/point (:x (:resize-vector parent-modifiers)) 1))
|
||||||
|
|
||||||
|
(and (:resize-vector-2 parent-modifiers)
|
||||||
|
(not (mth/close? (:x (:resize-vector-2 parent-modifiers)) 1)))
|
||||||
|
(assoc :resize-origin-2 (:resize-origin-2 parent-modifiers)
|
||||||
|
:resize-vector-2 (gpt/point (:x (:resize-vector-2 parent-modifiers)) 1))
|
||||||
|
|
||||||
|
(:displacement parent-modifiers)
|
||||||
|
(assoc :displacement
|
||||||
|
(gpt/point (-> (gpt/point 0 0)
|
||||||
|
(gpt/transform (:displacement parent-modifiers))
|
||||||
|
(gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix)))
|
||||||
|
(:x))
|
||||||
|
0)))
|
||||||
|
{})
|
||||||
|
|
||||||
|
modifiers-v (case constraints-v
|
||||||
|
:top
|
||||||
|
(let [delta-top (- (:y1 transformed-parent-rect) (:y1 parent-rect))]
|
||||||
|
(if-not (mth/almost-zero? delta-top)
|
||||||
|
{:displacement (gpt/point 0 delta-top)} ;; we convert to matrix below
|
||||||
|
{}))
|
||||||
|
|
||||||
|
:bottom
|
||||||
|
(let [delta-bottom (- (:y2 transformed-parent-rect) (:y2 parent-rect))]
|
||||||
|
(if-not (mth/almost-zero? delta-bottom)
|
||||||
|
{:displacement (gpt/point 0 delta-bottom)}
|
||||||
|
{}))
|
||||||
|
|
||||||
|
:topbottom
|
||||||
|
(let [delta-top (- (:y1 transformed-parent-rect) (:y1 parent-rect))
|
||||||
|
delta-height (- (:height transformed-parent-rect) (:height parent-rect))]
|
||||||
|
(if (or (not (mth/almost-zero? delta-top))
|
||||||
|
(not (mth/almost-zero? delta-height)))
|
||||||
|
{:displacement (gpt/point 0 delta-top)
|
||||||
|
:resize-origin (-> (gpt/point (:x1 child-rect)
|
||||||
|
(+ (:y1 child-rect) delta-top))
|
||||||
|
(transform-point-center
|
||||||
|
(gco/center-rect child-rect)
|
||||||
|
(:transform child (gmt/matrix))))
|
||||||
|
:resize-vector (gpt/point 1 (/ (+ (:height child-rect) delta-height)
|
||||||
|
(:height child-rect)))}
|
||||||
|
{}))
|
||||||
|
|
||||||
|
:center
|
||||||
|
(let [parent-center (gco/center-rect parent-rect)
|
||||||
|
transformed-parent-center (gco/center-rect transformed-parent-rect)
|
||||||
|
delta-center (- (:y transformed-parent-center) (:y parent-center))]
|
||||||
|
(if-not (mth/almost-zero? delta-center)
|
||||||
|
{:displacement (gpt/point 0 delta-center)}
|
||||||
|
{}))
|
||||||
|
|
||||||
|
:scale
|
||||||
|
(cond-> {}
|
||||||
|
(and (:resize-vector parent-modifiers)
|
||||||
|
(not (mth/close? (:y (:resize-vector parent-modifiers)) 1)))
|
||||||
|
(assoc :resize-origin (:resize-origin parent-modifiers)
|
||||||
|
:resize-vector (gpt/point 1 (:y (:resize-vector parent-modifiers))))
|
||||||
|
|
||||||
|
(and (:resize-vector-2 parent-modifiers)
|
||||||
|
(not (mth/close? (:y (:resize-vector-2 parent-modifiers)) 1)))
|
||||||
|
(assoc :resize-origin-2 (:resize-origin-2 parent-modifiers)
|
||||||
|
:resize-vector-2 (gpt/point 1 (:y (:resize-vector-2 parent-modifiers))))
|
||||||
|
|
||||||
|
(:displacement parent-modifiers)
|
||||||
|
(assoc :displacement
|
||||||
|
(gpt/point 0 (-> (gpt/point 0 0)
|
||||||
|
(gpt/transform (:displacement parent-modifiers))
|
||||||
|
(gpt/transform (:resize-transform-inverse parent-modifiers (gmt/matrix)))
|
||||||
|
(:y)))))
|
||||||
|
{})]
|
||||||
|
|
||||||
|
;; Build final child modifiers. Apply transform again to the result, to get the
|
||||||
|
;; real modifiers that need to be applied to the child, including rotation as needed.
|
||||||
|
(cond-> {}
|
||||||
|
(or (:displacement modifiers-h) (:displacement modifiers-v))
|
||||||
|
(assoc :displacement (gmt/translate-matrix
|
||||||
|
(-> (gpt/point (get (:displacement modifiers-h) :x 0)
|
||||||
|
(get (:displacement modifiers-v) :y 0))
|
||||||
|
(gpt/transform
|
||||||
|
(:resize-transform parent-modifiers (gmt/matrix))))))
|
||||||
|
|
||||||
|
(:resize-vector modifiers-h)
|
||||||
|
(assoc :resize-origin (:resize-origin modifiers-h)
|
||||||
|
:resize-vector (gpt/point (get (:resize-vector modifiers-h) :x 1)
|
||||||
|
(get (:resize-vector modifiers-h) :y 1)))
|
||||||
|
|
||||||
|
(:resize-vector modifiers-v)
|
||||||
|
(assoc :resize-origin-2 (:resize-origin modifiers-v)
|
||||||
|
:resize-vector-2 (gpt/point (get (:resize-vector modifiers-v) :x 1)
|
||||||
|
(get (:resize-vector modifiers-v) :y 1)))
|
||||||
|
|
||||||
|
(:resize-transform parent-modifiers)
|
||||||
|
(assoc :resize-transform (:resize-transform parent-modifiers)
|
||||||
|
:resize-transform-inverse (:resize-transform-inverse parent-modifiers)))))
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue