Compare commits

..

266 Commits

Author SHA1 Message Date
Andrey Antukh
5c71c57dd9 Merge tag '2.12.1' 2025-12-30 15:37:30 +01:00
Alejandro Alonso
48e3f35bb3 🐛 Fix setting a portion of text as bold or underline messes things up 2025-12-30 11:34:24 +01:00
Yamila Moreno
d3ee50daf5 🔧 Add ci for branch staging-render 2025-12-30 11:13:00 +01:00
Andrey Antukh
de052b5161 📎 Update changelog 2025-12-29 11:10:04 +01:00
Andrey Antukh
8a3b33797f 🐛 Fix error handling on password change form
Fixes https://github.com/penpot/penpot/issues/7978
2025-12-29 10:27:27 +01:00
Andrey Antukh
13fd20f76f Backport form error management improvements from develop 2025-12-29 10:27:27 +01:00
Andrey Antukh
01ecde3bfa Add the ability to add relations on penpot sdk (#7987)
*  Add the ability to add relations on penpot sdk

* 📎 Remove debug console log
2025-12-22 20:55:31 +01:00
Alonso Torres
4000ec8762 🐛 Fix problem resizing auto size layouts (#7995) 2025-12-22 20:17:11 +01:00
Andrey Antukh
bb5568e15a 🎉 Enable hindi translations on the application 2025-12-22 16:57:00 +01:00
Pablo Alba
5cbcec3db6 🐛 Fix "maximum call stack size exceeded" crash on variant 2025-12-22 16:57:00 +01:00
Alejandro Alonso
fe44c14bac Merge pull request #7982 from penpot/niwinz-staging-import-bucket
🐛 Prefill storage object bucket if it comes nil on import binfile
2025-12-22 12:17:16 +01:00
Andrey Antukh
336173645e 🐛 Fix regression on export shape on plungins API 2025-12-22 10:41:42 +01:00
Andrey Antukh
83bb4bf221 🐛 Prefill storage object bucket if it comes nil on import binfile 2025-12-19 09:32:51 +01:00
Alejandro Alonso
15ed25ca79 Merge pull request #7966 from penpot/niwinz-staging-abrreviate
🐛 Fix incorrect string truncation with abbreviate template filter
2025-12-12 13:53:33 +01:00
Andrey Antukh
9aa387a473 🐛 Fix incorrect string truncation with abbreviate template filter 2025-12-12 13:50:46 +01:00
Alejandro Alonso
67ba91b4b9 Merge pull request #7971 from penpot/niwinz-staging-bugfix-6
🐛 Fix tokens-lib encoding when value is nilable
2025-12-12 13:46:06 +01:00
Alejandro Alonso
f67f1a6a0e Merge pull request #7972 from penpot/niwinz-staging-bugfix-7
🐛 Fix exception on assinging gradient to shadow on multiple selection
2025-12-12 13:42:39 +01:00
Alejandro Alonso
82d3e2024e Merge pull request #7973 from penpot/niwinz-staging-worker-scheduler
🐛 Fix incorrect redis connection error handling
2025-12-12 13:23:49 +01:00
Alejandro Alonso
4bd846c16d Merge pull request #7969 from penpot/niwinz-staging-fix-ratelimit
🐛 Fix issue on reading rlimit config
2025-12-12 13:22:53 +01:00
Andrey Antukh
94f95ca6b8 🐛 Fix incorrect redis connection error handling 2025-12-12 12:33:38 +01:00
Andrey Antukh
5abc1aafb4 Merge tag '2.12.0-RC3' 2025-12-12 12:19:29 +01:00
Andrey Antukh
507bf7445b 🐛 Fix tokens-lib encoding when value is nilable 2025-12-12 11:42:15 +01:00
Andrey Antukh
81b72c5acd 🐛 Fix exception on assinging gradient to shadow on multiple selection 2025-12-12 11:24:53 +01:00
Andrey Antukh
974495e08f Reduce log level for profile picture download error
Because it is not blocking operation and does not provents user
to proceed.
2025-12-12 08:17:13 +01:00
Andrey Antukh
2ed39e43c3 🐛 Fix issue on reading rlimit config 2025-12-11 23:50:01 +01:00
Eva Marco
50dbe6ab12 🐛 Fix horizontal scroll on layer panel (#7956) 2025-12-11 21:34:18 +01:00
Andrey Antukh
2f46cbc0d4 Make render wasm import on worker http cache aware 2025-12-11 13:27:20 +01:00
Andrey Antukh
53be6f996b 🐛 Fix issues on build processs related to render-wasm 2025-12-11 12:41:19 +01:00
Andrey Antukh
5a260294a1 🔧 Update build-tag.yml github workflow 2025-12-11 12:00:42 +01:00
Andrey Antukh
3f6e44316e 🐛 Add missing node depes install on render-wasm 2025-12-11 11:51:47 +01:00
Eva Marco
77ef8e6fe6 🐛 Fix scroll on move library modal (#7952) 2025-12-11 10:46:54 +01:00
Alejandro Alonso
916b7709dc Update Pencil Penpot Design System System template in carousel (#7948) 2025-12-10 15:09:28 +01:00
Eva Marco
443e41fea4 🐛 Fix multiple selection with color tokens (#7941) 2025-12-10 14:36:08 +01:00
Alejandro Alonso
c7c9b04095 Merge pull request #7944 from penpot/niwinz-staging-exporter-fix
🐛 Fix incorrect resource lifetime handling on exporter
2025-12-10 14:35:20 +01:00
Eva Marco
c61a0c0332 📚 Add line to changelog (#7945) 2025-12-10 13:58:18 +01:00
Eva Marco
8707ff6511 🎉 Add spanish translation 2025-12-10 13:12:30 +01:00
Florian Schroedl
3d8a251741 🐛 Disallow font-family referencing composite token 2025-12-10 13:12:30 +01:00
Andrey Antukh
34e84ee3c8 🐛 Fix incorrect resource lifetime handling on exporter 2025-12-10 13:02:31 +01:00
Alejandro Alonso
e8201402a7 Merge pull request #7938 from penpot/niwinz-staging-bugfix-5
🐛 Fix several issues
2025-12-10 12:05:42 +01:00
Aitor Moreno
8a22477b96 Merge pull request #7932 from penpot/niwinz-staging-worker-wasm-load
🐛 Fix WASM loading strategy on worker
2025-12-10 11:47:31 +01:00
Alejandro Alonso
3e684ea54f ⬆️ Update svgo dependency on frontend (#7936) 2025-12-10 10:07:02 +01:00
Andrey Antukh
98039f13d8 🐛 Fix main toolbar z-index 2025-12-10 09:47:40 +01:00
Alejandro Alonso
40c27591f6 🐛 Fix svg import (#7925) 2025-12-10 08:36:54 +01:00
Andrey Antukh
91d20a46d1 💄 Add cosmetic changes to exports assets progress component 2025-12-10 08:23:05 +01:00
Andrey Antukh
50bead7c56 🐛 Fix react warning on having p inside p on assets export progress 2025-12-10 08:22:41 +01:00
Andrey Antukh
b75b999903 📎 Fix devenv jvm warning 2025-12-10 08:22:05 +01:00
Andrey Antukh
810f1721c8 🐛 Fix recursion render on subscription modal 2025-12-10 07:54:52 +01:00
Andrey Antukh
a4646373cf ♻️ Refactor wasm loading strategy on worker 2025-12-09 19:41:19 +01:00
Andrey Antukh
f111cbb2a4 Add better cache config on devenv nginx 2025-12-09 19:38:30 +01:00
Aitor Moreno
a614207f7e 🐛 Fix exporter failing with HTTPS 2025-12-09 16:08:20 +01:00
Luis de Dios
6ce3249c6d 🐛 Fix color format does not switch in the view mode (#7923)
* 🐛 Fix color format does not switch in the inspect mode of the view mode

* ♻️ Update components
2025-12-09 14:38:15 +01:00
Pablo Alba
b0351be724 🐛 Fix switch variants with paths 2025-12-09 11:08:55 +01:00
Andrey Antukh
b8392b3731 🐛 Fix regression on sending team invitations (#7912) 2025-12-05 12:36:06 +01:00
Andrey Antukh
935728aa39 🔧 Backport build-tag github workflow from develop 2025-12-05 10:26:01 +01:00
Andrey Antukh
77dba477ca 🔧 Backport build-tag github workflow from develop 2025-12-05 10:25:03 +01:00
Eva Marco
b6598d1f07 🐛 Fix scrollbar on color modal (#7906) 2025-12-05 09:55:41 +01:00
Xaviju
bf1dc21c75 💄 Hide themes & sets panels when none active (#7902) 2025-12-04 14:11:57 +01:00
Alejandro Alonso
46c20a993f Merge pull request #7904 from penpot/niwinz-staging-fix-invitation-resend
🐛 Fix exception on resending invitation
2025-12-04 11:56:07 +01:00
Andrey Antukh
0e0106f69a 🐛 Add correct assertion on create-invitation fn 2025-12-04 11:38:32 +01:00
Andrey Antukh
19bb69cc60 Improve invalid schema error report 2025-12-04 11:38:16 +01:00
Alejandro Alonso
504eb70988 Merge pull request #7885 from penpot/niwinz-staging-bugfix-2
🐛 Make workspace palette reposition on left sidebar collapse
2025-12-04 11:19:20 +01:00
Xaviju
75a2331edf 💄 Set low-emphasis color for both light/dark modes (#7884) 2025-12-04 11:04:07 +01:00
Alejandro Alonso
c2b4c9907d Merge pull request #7886 from penpot/niwinz-staging-bugfix-3
🐛 Fix casing on a translation of export files modal option
2025-12-04 10:59:51 +01:00
Alejandro Alonso
bd5bbcae26 Merge pull request #7894 from penpot/niwinz-staging-bugfix-4
🐛 Fix incorrect interaction betwen hower and scroll on assets sidebar
2025-12-04 10:58:54 +01:00
Andrey Antukh
84273508ad 🐛 Fix incorrect interaction betwen hower and scroll on assets sidebar 2025-12-04 10:56:29 +01:00
Andrey Antukh
9245ba6bc2 💄 Adapt component style for assets-local-library on sidebar assets 2025-12-04 10:55:57 +01:00
Andrey Antukh
4be046406d Pass direct args instead of a vector to toggle-values on sidebar assets 2025-12-04 10:55:57 +01:00
Alejandro Alonso
84c747cd31 Merge pull request #7883 from penpot/niwinz-staging-bugfix
🐛 Fix exception on paste text on comments input
2025-12-04 10:32:07 +01:00
Alejandro Alonso
0036a9a0cd Merge pull request #7865 from penpot/niwinz-staging-audit
 Add minor improvements to the audit module
2025-12-04 10:04:00 +01:00
Alejandro Alonso
2105c3a68c Merge pull request #7866 from penpot/niwinz-staging-fix-emails
🐛 Change internal ordering on how email parts are assembled
2025-12-04 09:56:22 +01:00
Belén Albeza
38efa88460 🐛 Fix unpublish library modal not scrolling file list (#7892)
* 🐛 Fix unpublish library modal not scrolling when the linked files list is too long

* 💄 Remove deprecated tokens in unpublish library modal

* 🔧 Update CHANGELOG
2025-12-03 22:41:20 +01:00
Pablo Alba
6e254c2cf4 🐛 Fix change of library on swap (#7898) 2025-12-03 22:40:23 +01:00
Andrey Antukh
6251fa6b22 🐛 Close other open context menus on open a context menu (#7895) 2025-12-03 18:50:00 +01:00
alonso.torres
aedd8cc11e 🐛 Fix problem when renaming variants in plugins 2025-12-03 17:42:17 +01:00
Alonso Torres
2f0853f5cc 🐛 Fix problem with variant plugins api (#7890) 2025-12-03 13:27:32 +01:00
Juan de la Cruz
648e660bcf 🎉 Add new content and images for the slides of 2.12 (#7874)
* 🎉 Add new slide's content

* 🎉 Add new slides images

* 📎 Fix clj fmt

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-12-03 13:26:55 +01:00
Slava "nerfur" Voronzoff
bee2f70bfa 🐛 Add missing amd64 option on arch detection code on mange.sh script
Signed-off-by: Slava "nerfur" Voronzoff <nerfur@gmail.com>
2025-12-03 13:09:49 +01:00
Pablo Alba
00f8eac8fa 🐛 Fix can't delete unsaved variant prop (#7878) 2025-12-03 13:03:17 +01:00
Dalai Felinto
df7caacb45 🐛 Fix crash in token grid view due to tooltip validation (#7887)
The color tokens in grid view have a tooltip which is a map.
This is done so the frontend can render:

```
Name: foo
Resolved value: #000000
```

However the validation scheme for tooltips was only accepting functions
and strings.

---

How to reproduce the original (unreported) crash:
* Create a color token
* Create a shape, add a fill
* Pick a color, chose the Token options
* Click on the Grid View

Crash: `{:hint "invalid props on component tooltip*\n\n  -> 'content'
    should be a string\n"}`

Signed-off-by: Dalai Felinto <dalai@blender.org>
Co-authored-by: Dalai Felinto <dalai@blender.org>
2025-12-03 13:01:36 +01:00
Marina López
49bbdfb257 🐛 Fix U and E icon displayed in project list (#7875)
* 🐛 Fix U and E icon displayed in project lis

* 🐛 Fix U and E icon displayed in project list
2025-12-03 12:50:51 +01:00
Andrey Antukh
94af978be8 🐛 Fix casing on a translation of export files modal option 2025-12-03 10:22:45 +01:00
Andrey Antukh
feababe2a8 🐛 Make workspace palette reposition on left sidebar collapse 2025-12-03 09:56:14 +01:00
Andrey Antukh
5ef06685fc 💄 Add cosmetic improvements to workspace palette component 2025-12-03 09:38:23 +01:00
Andrey Antukh
57fcec5afc 🐛 Make from-synthetic-clipboard-event function return always a stream
Causes an execption on steam processing when it returns nil
2025-12-03 08:32:38 +01:00
Andrey Antukh
58f82da61e 🐛 Fix exception on paste text on comments input 2025-12-03 08:20:58 +01:00
Andrey Antukh
a28c5b61ca 💄 Adapt viewport paste code codestyle
And remove some not necessary constructions
2025-12-03 08:09:13 +01:00
Andrey Antukh
9123d199b7 🐛 Fix scripts/fmt 2025-12-02 17:43:21 +01:00
Andrey Antukh
eeaf28bb25 📎 Disable caddy logging 2025-12-02 13:27:09 +01:00
Andrey Antukh
6b8091bb90 Make devenv https and http2 capable (#7871)
Making it more similar on how it runs on production
environments and improves large amount of files loading
thanks to http2.
2025-12-02 10:49:37 +01:00
Madalena Melo
bba02473d5 📚 Update subtitles in the new user guide cards (#7823)
Co-authored-by: Andres Gonzalez <andres.gonzalez79@gmail.com>
2025-12-02 09:21:05 +01:00
Andrey Antukh
77c9d8a2c8 🐛 Revert exporter dockerfile changes 2025-12-01 14:32:00 +01:00
Andrey Antukh
95b7784a42 🐛 Change internal ordering on how email parts are assembled
This fixes the html email rendering on gmail. Other clients (like proton,
emailcatcher) properly renders html independently of the order of parts
on the multipart email structure but gmail requires that html should be
the last one.
2025-12-01 14:27:21 +01:00
Andrey Antukh
4690f740b9 Add minor improvements to the audit module 2025-12-01 13:57:55 +01:00
Xaviju
529c4eb38a 💄 Avoid code tab overflow (#7854) 2025-12-01 11:37:37 +01:00
Andrey Antukh
c3a9919c4d 🐛 Fix typo on exporter dockerfile 2025-12-01 11:19:41 +01:00
Juanfran
10a2732a55 Merge pull request #7863 from penpot/niwinz-staging-improve-yarn-independency
 Use setup script on exporter instead of direct commands
2025-12-01 10:13:58 +01:00
Andrey Antukh
40e3617138 Use setup script on exporter instead of direct commands 2025-12-01 09:23:11 +01:00
Andrey Antukh
b18c421415 📎 Update .gitignore 2025-12-01 09:20:33 +01:00
Andrey Antukh
e7029f2182 Make automatic workflows not dependent on yarn 2025-12-01 08:17:52 +01:00
Alonso Torres
2c3becb408 🐛 Fix problem with plugins content attribute (#7835) 2025-11-28 13:41:27 +01:00
Xaviju
a4e6aa0588 💄 Limit inspect layer info message to avoid overflow (#7847) 2025-11-28 10:19:02 +01:00
Andrey Antukh
7fe20b65dc 🔧 Add more cache efficient configuration for devenv nginx 2025-11-27 17:59:12 +01:00
Andrey Antukh
e5638cd769 ⬆️ Update clojure tools version on devenv 2025-11-27 17:58:56 +01:00
Eva Marco
8e79dfcb82 🐛 Fix input variant 2025-11-27 17:54:11 +01:00
Eva Marco
508db99a57 🐛 Restore empty field error on dimension, text-case and color forms 2025-11-27 17:54:11 +01:00
Andrey Antukh
3c6c9894da 🐛 Restore empty value error on border radius token form 2025-11-27 17:54:11 +01:00
Andrey Antukh
972b23e6c0 🐛 Fix incorect pred build on ::sm/text schema 2025-11-27 17:54:11 +01:00
Andrey Antukh
28f550d533 🔥 Remove commented code 2025-11-27 17:54:11 +01:00
Elena Torró
2b20f75fd4 Merge pull request #7837 from penpot/ladybenko-12719-fix-editor-unicode-fonts
🐛 Fix editor not using fallback fonts for selected text
2025-11-27 17:37:00 +01:00
Belén Albeza
4d6d7a6a3d 🐛 Fix emoji font not being used as fallback in text editor dom 2025-11-27 17:23:20 +01:00
Andrey Antukh
db1ab7be69 📎 Run worker bundling serially on devenv 2025-11-27 16:09:15 +01:00
Andrey Antukh
fcbe9d92dc 🐛 Fix unexpected exception on rendering feedback email
Looks like a bug on selmer library
2025-11-27 16:09:15 +01:00
Andrey Antukh
9998ce0bb4 🔥 Remove fipps direct dependency 2025-11-27 16:09:15 +01:00
Andrey Antukh
6061391c89 Don't require cljs.analyzer api under cljs on data.macros
Reduces the final production bundle size
2025-11-27 16:09:15 +01:00
Andrey Antukh
eabf6e36ed Remove a level of indentation on subscriptions-dashboard tests 2025-11-27 16:09:15 +01:00
Andrey Antukh
04274e53fa 📎 Fix advanced compilation warnings related to jsdoc 2025-11-27 16:09:15 +01:00
Andrey Antukh
52dd9271a9 🐛 Encode header values as strings on audit archive task 2025-11-27 16:09:15 +01:00
andrés gonzález
8f5a81e179 📚 Add info about boolean variants (#7828) 2025-11-27 16:03:11 +01:00
Alonso Torres
a940c08da9 🐛 Fix problem with worker bundling in development (#7844) 2025-11-27 14:13:48 +01:00
Alejandro Alonso
3de4473251 Merge pull request #7845 from penpot/elenatorro-fix-case
🐛 Fix editor vertical align default case
2025-11-27 14:00:12 +01:00
Andrey Antukh
0735140f07 🔧 Change concurrency rules on tests github workflow 2025-11-27 13:46:48 +01:00
Elena Torro
dc8a07099d 🐛 Fix vertical align default case 2025-11-27 13:38:51 +01:00
Elena Torró
90dcf04fb0 Merge pull request #7841 from penpot/superalex-fix-boolean-operators-no-selection
🐛 Fix boolean operators no selection
2025-11-27 12:50:16 +01:00
Belén Albeza
f84c236e02 🐛 Fix text editor v2 not using fallback fonts for selected text 2025-11-27 12:26:39 +01:00
Alejandro Alonso
63959a22cc 🐛 Fix svg attrs 2025-11-27 12:23:46 +01:00
Alejandro Alonso
8840246425 🐛 Fix bleeding masks 2025-11-27 12:23:46 +01:00
Alejandro Alonso
62ec66cd15 🔧 Adding more e2e tests for nested frames with clipping 2025-11-27 12:23:46 +01:00
Alejandro Alonso
e3b87390f6 🐛 Fix nested shadows clipping 2025-11-27 12:23:46 +01:00
Alejandro Alonso
d9ab28e6ed 🐛 Fix nested clipping 2025-11-27 12:23:46 +01:00
Belén Albeza
9183dbbc43 🔧 Fix lint error (rust) 2025-11-27 11:51:05 +01:00
Andrey Antukh
74d00473e9 Add missing render-wasm to the ci workflow 2025-11-27 11:51:05 +01:00
Alejandro Alonso
1c70f5a36b 🐛 Fix boolean operatos shown when there is no selection 2025-11-27 11:22:15 +01:00
Andrey Antukh
b23e0c0642 Add tempfile storage bucket handler test case (#7839) 2025-11-27 10:27:57 +01:00
Marina López
db0cbbbc2e 🐛 Fix logic preventing incorrect trial flow in subscription modal (#7831) 2025-11-26 12:08:02 +01:00
alonso.torres
48304bd26f 🐛 Fix issue when exporting files 2025-11-26 12:04:34 +01:00
Elena Torro
60e32bbc71 🐛 Fix text editor vertical align 2025-11-26 11:46:47 +01:00
André Carvalhais
54451608dc 💄 Fix spelling of 'smtp' in email configuration section
Corrected the spelling of 'smtp' in the documentation.

Signed-off-by: André Carvalhais <carvalhais@live.com>
2025-11-26 08:11:27 +01:00
Alejandro Alonso
b7727122d5 Merge pull request #7829 from penpot/alotor-fixes
🐛 Fix problem with thumbnails in parallel
2025-11-26 07:21:49 +01:00
alonso.torres
8880f07a6a 🐛 Fix problem with thumbnails in parallel 2025-11-25 17:56:00 +01:00
andrés gonzález
aaca2c41d8 📚 Add metadescriptions to some help center pages (#7821) 2025-11-25 17:00:14 +01:00
Belén Albeza
33417a4b20 🐛 Fix svg attrs stroke-linecap stroke-linejoin fill-rule 2025-11-25 12:43:40 +01:00
Andrés Moya
2640889dc8 🐛 Fix backwards compatibility importing files with token themes 2025-11-25 10:56:33 +01:00
alonso.torres
dd5f3396d1 🐛 Fix problem with layout z-index 2025-11-24 17:48:58 +01:00
Andrey Antukh
dedeae8641 🐛 Fix incorrect subscription fetching after profile registration 2025-11-24 14:36:46 +01:00
Andrey Antukh
a7552d412a Add explicit network asingation and alias on devenv compose 2025-11-24 14:36:46 +01:00
Aitor Moreno
f58475a7c9 🐛 Fix pasting application/transit+json (#7812) 2025-11-24 14:36:24 +01:00
Marina López
00bbb0bfb6 ♻️ Add format and refactor payments 2025-11-24 11:41:03 +01:00
Andrey Antukh
d93fe89c12 📎 Backport CI github workflog from develop 2025-11-24 10:48:51 +01:00
Andrey Antukh
6e44330af4 Merge remote-tracking branch 'origin/develop' into staging 2025-11-24 09:42:45 +01:00
Andrey Antukh
624805fd6b Merge remote-tracking branch 'weblate/develop' into develop 2025-11-24 09:32:06 +01:00
Eva Marco
9b6bb77422 Materialize several tokens related flags (#7773)
* 📚 Add line to changelog

* ♻️ Remove typography types flag

* ♻️ Remove composite typography token flag

* ♻️ Remove token units flag

* 🎉 Activate by default two token flags

* ♻️ Update inspect tab tests to navigate to the right info tab

* 🐛 Fix test

---------

Co-authored-by: Xavier Julian <xavier.julian@kaleidos.net>
2025-11-24 09:26:05 +01:00
Yamila Moreno
9b8e04bb3c 🐳 Remove minio service from docker-compose.yml (#7809) 2025-11-24 08:15:36 +01:00
Edgars Andersons
2e919809c9 🌐 Add translations for: Latvian
Currently translated at 94.1% (1873 of 1990 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-11-23 12:51:20 +00:00
Nicola Bortoletto
645e123e3a 🌐 Add translations for: Italian
Currently translated at 98.8% (1967 of 1990 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-11-23 12:51:17 +00:00
Oğuz Ersen
cfb94d17b6 🌐 Add translations for: Turkish
Currently translated at 99.8% (1987 of 1990 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2025-11-22 10:51:22 +00:00
Keunes
e9cb409ca4 🌐 Add translations for: Dutch
Currently translated at 99.8% (1987 of 1990 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-11-22 10:51:19 +00:00
jonnysemon
8a0cd75257 🌐 Add translations for: Arabic
Currently translated at 56.6% (1128 of 1990 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2025-11-22 10:51:17 +00:00
Pablo Alba
fae488b15a 🐛 Fix after changing a variant property value, the value appears as empty (#7791) 2025-11-21 17:51:12 +01:00
Elena Torró
b82828632e Merge pull request #7807 from penpot/alotor-fix-hover-text
🐛 Fix hover text
2025-11-21 15:35:41 +01:00
alonso.torres
bf24e22588 🐛 Fix hover text 2025-11-21 14:27:15 +01:00
Alejandro Alonso
7399b4d423 📚 Remove wrong line on CHANGES 2025-11-21 14:21:14 +01:00
Alejandro Alonso
77b9eee6bd 🐛 Fix svg fills defined in svg-attrs with url or color format 2025-11-21 14:15:27 +01:00
Elena Torro
55896db49e 🔧 Check for emtpy/nil attrs when getting inline style 2025-11-21 14:10:23 +01:00
Elena Torró
f4c569d619 Merge pull request #7802 from penpot/alotor-fix-text-data-problem
🐛 Fix problems with text editor size
2025-11-21 13:41:38 +01:00
alonso.torres
ca2cf18a49 🐛 Fix problems with text editor size 2025-11-21 13:17:43 +01:00
Andrey Antukh
6e352c167c 🐛 Fix dev build of frontend 2025-11-21 13:02:44 +01:00
Andrey Antukh
3ec001de44 🔧 Add nitrate url to devenv nginx (#7800) 2025-11-21 12:30:49 +01:00
Elena Torró
a1f11c89f2 Merge pull request #7799 from penpot/alotor-fix-text-data-problem
🐛 Fix problem with text data serialization
2025-11-21 12:30:35 +01:00
alonso.torres
33d70f0e45 🐛 Fix problem with text data serialization 2025-11-21 12:07:01 +01:00
Elena Torró
4f24a8f5f1 Merge pull request #7770 from penpot/ladybenko-12587-fix-text-editor-crash-empty
🐛 Fix crash when using a font family with a number in its name
2025-11-21 12:02:40 +01:00
Andrey Antukh
b03cfffb9e Restore the dashboard thumbnail rendering using wasm (#7796)
* Revert "🐛 Rollback esm worker (#7792)"

This reverts commit 0120a5335b.

* 🐛 Fix incorrect manifest reading on building worker
2025-11-21 11:42:40 +01:00
Elena Torró
956ad88e51 Merge pull request #7795 from penpot/alotor-fix-paste-crash
🐛 Fix paste crash
2025-11-21 11:00:00 +01:00
Belén Albeza
76f5c73de6 Remove leftover console.log/trace 2025-11-21 10:59:15 +01:00
Belén Albeza
c6dd3e0eeb Add missing param to cut handler 2025-11-21 10:28:48 +01:00
alonso.torres
fde73f30b9 🐛 Fix paste crash 2025-11-21 09:51:54 +01:00
Edgars Andersons
9d35a4317c 🌐 Add translations for: Latvian
Currently translated at 93.6% (1864 of 1990 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-11-21 05:51:27 +00:00
jonnysemon
e7ccfeccbf 🌐 Add translations for: Arabic
Currently translated at 56.6% (1128 of 1990 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2025-11-21 05:51:26 +00:00
Stephan Paternotte
aa043d284f 🌐 Add translations for: Dutch
Currently translated at 99.8% (1987 of 1990 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-11-21 05:51:21 +00:00
Alejandro Alonso
537dd171c0 Merge pull request #7793 from penpot/alotor-tiles-improvement
 Improve cache rendering
2025-11-20 18:50:24 +01:00
alonso.torres
c2026918a4 Improve cache rendering 2025-11-20 17:33:37 +01:00
Alonso Torres
0120a5335b 🐛 Rollback esm worker (#7792) 2025-11-20 16:07:22 +01:00
Belén Albeza
d0d2f43ca1 🐛 Fix text editor crash with font families with a number in their name 2025-11-20 15:22:40 +01:00
Alejandro Alonso
7e33a7c1a7 Merge pull request #7666 from penpot/azazeln28-feat-allow-disabling-rich-paste
🎉 Add an option to enable and disable HTML paste
2025-11-20 14:17:16 +01:00
Elena Torró
c13b58f42a Merge pull request #7764 from penpot/superalex-fix-blurs
🐛 Fix shadows and blurs
2025-11-20 13:37:57 +01:00
alonso.torres
a5c9f9e454 📚 Adds contributor to the changelog 2025-11-20 13:35:43 +01:00
Aitor Moreno
d73be5832b 🎉 Add an option to enable and disable HTML paste 2025-11-20 13:33:51 +01:00
Alejandro Alonso
e1f2fca4af Merge pull request #7771 from penpot/elenatorro-12541-improve-text-selection-and-cursor
 Improve text shape selection
2025-11-20 13:33:48 +01:00
Diana Veiga
37d5a31589 Drop zoom snap (#7774)
*  Remove const `zoom-half-pixel-precision`

* ♻️ Adjust usages
2025-11-20 13:28:45 +01:00
Luis de Dios
177bdaa72c 🐛 Fix variant toggle does not work for uppercase or mixed case (#7716)
* 🐛 Fix variant toggle does not work for uppercase or mixed case

* 📎 PR changes
2025-11-20 13:27:04 +01:00
Aitor Moreno
38ab2c61b9 Merge pull request #7782 from penpot/alotor-wasm-thumbnails
 Render WASM dashboard thumbnails
2025-11-20 13:12:26 +01:00
Marina López
cc32b22e8a Add improvements to the payment flow (#7776)
*  Add improvements payment flow

* 📎 PR feedback

* 📎 Fix conflicts
2025-11-20 13:07:57 +01:00
Alejandro Alonso
d331c5ad83 Merge pull request #7769 from penpot/niwinz-develop-exporter-refactor
 Remove exporter dependency on shared-fs on scaling
2025-11-20 12:44:34 +01:00
iPagar
6c6c2c3012 📚 Update copyright year on doc (#7502)
Signed-off-by: iPagar <iPagar@users.noreply.github.com>
2025-11-20 12:38:31 +01:00
Andrey Antukh
81632a03dd ♻️ Make exporter upload resources using backend management api
Instead of custon shared fs approach. This commit fixes the main
scalability issue of exporter removing the need of shared-fs
for make it work with multiple instances.
2025-11-20 12:20:13 +01:00
Andrey Antukh
4fddf3d986 ♻️ Make management key derivable from secret key
Still preserves the ability to set management
2025-11-20 12:20:13 +01:00
Andrey Antukh
57aa9a585b 🔧 Add explicit network alias for minio on devenv 2025-11-20 12:20:13 +01:00
Andrey Antukh
f71f491590 🐛 Fix incorrect bearer token decoding 2025-11-20 12:20:13 +01:00
Andrey Antukh
6ae2401c5e ♻️ Change how shapes are validated after changes apply operation 2025-11-20 12:08:48 +01:00
Andrey Antukh
53d8a2d6d7 🔥 Remove obsolete code on :move-objects related to old components 2025-11-20 12:08:48 +01:00
Andrey Antukh
bd65f3932e 🐛 Fix a race condition on move-object
That happens when an in-flight move-object change tries
to move object to an already deleted parent
2025-11-20 12:08:48 +01:00
alonso.torres
59845b756f Render WASM dashboard thumbnails 2025-11-20 11:56:25 +01:00
Alejandro Alonso
b8c0c5c310 Merge pull request #7742 from penpot/alotor-plugins-improvements
 Plugin API improvements with images and indexes
2025-11-20 11:47:50 +01:00
Alejandro Alonso
cfa8c21ee6 Merge pull request #7788 from penpot/elenatorro-fix-insert-shape-on-empty-frame
🐛 Fix insert shape on empty frame
2025-11-20 11:44:13 +01:00
Elena Torro
624bdaec88 Show text cursor in the entire text rect 2025-11-20 11:42:07 +01:00
Alejandro Alonso
24745bed40 🐛 Fix shadows and blurs for high levels of zoom 2025-11-20 11:25:23 +01:00
Eva Marco
d26c08f8e2 ♻️ Replace token forms (#7759)
* 🎉 Create dimensions form

* 🎉 Create text-case form

* 🎉 Create color form

* ♻️ Remove unused code on form file
2025-11-20 11:04:39 +01:00
Elena Torro
36adbd9118 🐛 Fix insert shape on empty frame 2025-11-20 10:59:44 +01:00
Elena Torró
0a3fe9836a Merge pull request #7777 from penpot/superalex-fix-extrect-calculation
🐛 Fix extrect calculation
2025-11-20 09:57:59 +01:00
Andrey Antukh
fef0c11503 🔧 Update tests github flow 2025-11-20 09:37:38 +01:00
Alejandro Alonso
7e858784a1 Merge pull request #7785 from penpot/niwinz-develop-binary-fills
🐛 Fix invalid fills schema when binary fills are used
2025-11-20 09:06:45 +01:00
Miguel de Benito Delgado
203368c2ee Add parameter to openPage to toggle new window behaviour (#7753)
*  Add parameter to openPage() to toggle opening a new tab/window

* 💄 Fix formatting
2025-11-20 08:05:08 +01:00
Alejandro Alonso
4f54469629 Merge pull request #7747 from penpot/niwinz-develop-storage-changes
 Make the binfile exportation process more reliable
2025-11-20 07:58:57 +01:00
Andrey Antukh
5343e799f8 🐛 Fix invalid fills schema when binary fills are used 2025-11-20 07:45:37 +01:00
Andrey Antukh
51e54a6bad 🐛 Fix incorrect project restoration on restoring file (#7778) 2025-11-19 18:24:24 +01:00
Aitor Moreno
f609747322 🐛 Fix inert element error 2025-11-19 18:23:44 +01:00
Andrey Antukh
26ad039d99 ⬆️ Update playwright dependency on frontend 2025-11-19 18:23:44 +01:00
Andrey Antukh
3136096123 🔧 Add general improvements to integration tests
This commit marks as skip (temporal) several flaky/randomly-failing
tests.

It also moves the integration test execution from circleci to github
actions.
2025-11-19 18:23:44 +01:00
Andrey Antukh
122d3bc41c 💄 Add code formatting for js on frontend 2025-11-19 18:23:44 +01:00
Andrey Antukh
3b52051113 Fix closure compiler issues on clipboard js impl
With minor naming fixes
2025-11-19 18:23:44 +01:00
Aitor Moreno
32e1b55658 ♻️ Refactor clipboard 2025-11-19 18:23:44 +01:00
Andrey Antukh
e9d177eae3 Make the binfile export process more resilent to errors
The current binfile export process uses a streaming technique. The
major problem with the streaming approach is the case when an error
happens on the middle of generation, because we have no way to
notify the user about the error (because the response is already
is sent and contents are streaming directly to the user
client/browser).

This commit replaces the streaming with temporal files and SSE
encoded response for emit the export progress events; once the
exportation is finished, a temporal uri to the exported artifact
is emited to the user via "end" event and the frontend code
will automatically trigger the download.

Using the SSE approach removes possible transport timeouts on export
large files by sending progress data over the open connection.

This commit also removes obsolete code related to old binfile
formats.
2025-11-19 17:28:55 +01:00
Andrey Antukh
d42c65b9ca Improve logging on shape detach operation 2025-11-19 17:28:55 +01:00
Andrey Antukh
86ad56797b Simplify tempfile deletion handling
Mainly removes the jvm on-exit hook usage because it can lead
to slow stops and unnecesary memory consumption over the time
the jvm is running.
2025-11-19 17:28:55 +01:00
Andrey Antukh
63497b8930 Add tempfile bucket to the storage subsystem
This enables storing temporal files under storage subsystem. The
temporal objects (the objects that uses templfile bucket) will
always evaluate to "for deletion" after touched garbage collection;
and the deletion threshold will be 2 hours (the threshold is always
calculated from the instant when the touched garbage collector is
running).
2025-11-19 17:28:55 +01:00
Andrey Antukh
94719eebf8 ♻️ Make storage and other objects deletion task vclock aware
This simplifes the mental model on how it works and simplifies testing
of the related code.

This also normalizes storage object deletion in the same way as the
rest of objects in penpot (now future deletion date on storage object
also means storage object to be deleted).
2025-11-19 17:28:55 +01:00
Andrey Antukh
9532dea2c6 📎 Skip inspect integration tests (#7781) 2025-11-19 17:26:40 +01:00
Andrey Antukh
40e1e27bf0 🐛 Fix not covered case on schema decode fn on tokens-lib 2025-11-19 15:04:49 +01:00
Andrés Moya
4338f97e9f 🐛 Allow deleting the library in the undo change of add tokens-lib 2025-11-19 15:04:49 +01:00
Andrey Antukh
2c4ec43d5f 🐛 Fix invalid syntax on translation files 2025-11-19 15:03:26 +01:00
Andrey Antukh
3d782a322d 🐛 Fix issue related to labels.code on translations 2025-11-19 14:53:13 +01:00
Andrey Antukh
407d28d187 🌐 Rehash and sync translation files 2025-11-19 14:18:41 +01:00
Andrey Antukh
bf582ec55f 🌐 Add several fixes on weblate merge 2025-11-19 13:25:11 +01:00
Andrey Antukh
858bc05ed5 Merge remote-tracking branch 'weblate/develop' into develop 2025-11-19 13:11:07 +01:00
Andrey Antukh
cd01386210 📎 Set version 1.1.0 final to sdk/library 2025-11-19 13:04:29 +01:00
Xaviju
3b2bb5f225 ♻️ Follow translations guidelines on several inspect components (#7766)
Signed-off-by: Xaviju <xavier.julian@kaleidos.net>
2025-11-19 13:03:25 +01:00
Alejandro Alonso
fe3bc96d0d Merge pull request #7772 from penpot/niwinz-develop-auth-bugfix
 Improvements to the auth internal flows changes
2025-11-19 12:46:10 +01:00
Alejandro Alonso
28f23f397e 🐛 Fix extrect calculation 2025-11-19 12:38:01 +01:00
Andrey Antukh
a487dfe004 Add better approach for cookie token decoding
Remove unnecesary decoding for old tokens and add key identifier
and versioning to cookie tokens for handle future changes.
2025-11-19 07:47:52 +01:00
Andrey Antukh
4f29156929 📎 Add better formatting of public-uri on db report 2025-11-18 20:35:26 +01:00
Andrey Antukh
ce2d3d1652 🐛 Fix incorrect handling of session renewal
A regression introduced in the prev auth refactor.
2025-11-18 20:35:16 +01:00
Andrey Antukh
3639ff9dbc 🔧 Update devenv logging configuration 2025-11-18 20:34:17 +01:00
Andrey Antukh
ca5ec734a0 Merge remote-tracking branch 'origin/staging' into develop 2025-11-18 18:19:36 +01:00
Andrey Antukh
b08da4c3ff Merge remote-tracking branch 'origin/main' into staging 2025-11-18 18:19:11 +01:00
Yamila Moreno
c9bec3924d 🐳 Use the secret key both in the backend and the exporter (#7746) 2025-11-18 18:18:49 +01:00
Yamila Moreno
6e725a75e1 🐳 Use the secret key both in the backend and the exporter (#7746) 2025-11-18 18:17:34 +01:00
Anton Palmqvist
81c3b84972 🌐 Add translations for: Swedish
Currently translated at 99.8% (1953 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-11-18 14:52:28 +01:00
jonnysemon
5868f7f6b2 🌐 Add translations for: Arabic
Currently translated at 57.7% (1130 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2025-11-18 14:52:25 +01:00
Tiago José
653567d7de 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 71.6% (1402 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-11-18 14:51:51 +01:00
Alejandro Alonso
ce651fa0a9 Merge pull request #7767 from penpot/alotor-fix-problem-compatibility
🐛 Fix problem with tainted canvas in thumbnails
2025-11-18 14:15:06 +01:00
alonso.torres
e8a26ef83b 🐛 Fix problem with tainted canvas in thumbnails 2025-11-18 13:05:56 +01:00
alonso.torres
8fd17c9c84 🐛 Fix problem not checking feature flag 2025-11-18 13:05:29 +01:00
Anton Palmqvist
d03f5c10fb 🌐 Add translations for: Swedish
Currently translated at 99.8% (1953 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-11-15 20:51:48 +00:00
Anton Palmqvist
3eb0f1c225 🌐 Add translations for: Swedish
Currently translated at 88.9% (1740 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-11-14 18:51:24 +01:00
alonso.torres
48c9fb5690 Add methods to plugins for modifying indices 2025-11-12 17:07:38 +01:00
alonso.torres
4cdf1eed0c 🐛 Add method to retrieve image data in plugins 2025-11-12 17:07:38 +01:00
Ahmad HosseinBor
69c4a8932a 🌐 Add translations for: Persian
Currently translated at 40.2% (787 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2025-11-10 08:51:23 +01:00
Stas Haas
f6e77c09b3 🌐 Add translations for: German
Currently translated at 90.4% (1770 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-11-03 21:51:14 +01:00
Stas Haas
e7b8ad8ee2 🌐 Add translations for: German
Currently translated at 89.3% (1747 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-11-01 10:51:52 +00:00
Ingrid Pigueron
ccb7b41b3a 🌐 Add translations for: French
Currently translated at 98.3% (1923 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-10-27 12:02:59 +00:00
AlexTECPlayz
597fba79cc 🌐 Add translations for: Romanian
Currently translated at 99.8% (1953 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2025-10-17 13:07:28 +02:00
AlexTECPlayz
43b03b9714 🌐 Add translations for: Romanian
Currently translated at 99.8% (1953 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2025-10-16 12:07:26 +02:00
Stephan Paternotte
4739c4730c 🌐 Add translations for: Dutch
Currently translated at 99.8% (1953 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-10-14 19:08:02 +02:00
Edgars Andersons
603bb860ba 🌐 Add translations for: Latvian
Currently translated at 95.3% (1866 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-10-14 19:08:01 +02:00
Yaron Shahrabani
55d9ca1439 🌐 Add translations for: Hebrew
Currently translated at 99.4% (1945 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2025-10-14 19:07:59 +02:00
Oğuz Ersen
a2f397c329 🌐 Add translations for: Turkish
Currently translated at 99.8% (1953 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2025-10-14 19:07:57 +02:00
Roman D
ada4e72c27 🌐 Add translations for: Russian
Currently translated at 78.2% (1530 of 1956 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2025-10-14 19:07:54 +02:00
374 changed files with 33097 additions and 29193 deletions

View File

@@ -114,7 +114,7 @@ jobs:
# uses the same cache as this task so we prepopulate it
command: |
yarn install
yarn run playwright install chromium
yarn run playwright install chromium --with-deps
- run:
name: "lint scss on frontend"
@@ -207,51 +207,6 @@ jobs:
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on tcp:6006 && yarn test:storybook"
test-integration:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: large
environment:
JAVA_OPTS: -Xmx6g -Xms2g
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
# Build frontend
- run:
name: "frontend build"
working_directory: "./frontend"
command: |
yarn install
yarn run build:app:assets
yarn run build:app
yarn run build:app:libs
# Build the wasm bundle
- run:
name: "wasm build"
working_directory: "./render-wasm"
command: |
EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh
./build release
# Run integration tests
- run:
name: "integration tests"
working_directory: "./frontend"
command: |
yarn run playwright install chromium
yarn run test:e2e -x --workers=4
test-backend:
docker:
- image: penpotapp/devenv:latest
@@ -347,5 +302,4 @@ workflows:
- lint: success
- lint
- test-integration
- test-render-wasm

View File

@@ -0,0 +1,14 @@
name: _STAGING RENDER
on:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging-render"
build_wasm: "yes"
build_storybook: "yes"

View File

@@ -11,7 +11,7 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
build_wasm: "no"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
@@ -21,6 +21,22 @@ jobs:
with:
gh_ref: ${{ github.ref_name }}
notify:
name: Notifications
runs-on: ubuntu-24.04
needs: build-docker
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
🐳 *[PENPOT] Docker image available.*
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
publish-final-tag:
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
needs: build-docker

298
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,298 @@
name: "CI"
defaults:
run:
shell: bash
on:
pull_request:
types:
- opened
- synchronize
push:
branches:
- develop
- staging
concurrency:
group: ${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lint:
name: "Linter"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check clojure code format
run: |
./scripts/lint
test-common:
name: "Common Tests"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run tests on JVM
working-directory: ./common
run: |
clojure -M:dev:test
- name: Run tests on NODE
working-directory: ./common
run: |
./scripts/test
test-frontend:
name: "Frontend Tests"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Unit Tests
working-directory: ./frontend
run: |
./scripts/test
- name: Component Tests
working-directory: ./frontend
run: |
./scripts/test-components
test-render-wasm:
name: "Render WASM Tests"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Format
working-directory: ./render-wasm
run: |
cargo fmt --check
- name: Lint
working-directory: ./render-wasm
run: |
./lint
- name: Test
working-directory: ./render-wasm
run: |
./test
test-backend:
name: "Backend Tests"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
services:
postgres:
image: postgres:17
# Provide the password for postgres
env:
POSTGRES_USER: penpot_test
POSTGRES_PASSWORD: penpot_test
POSTGRES_DB: penpot_test
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: valkey/valkey:9
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run tests
working-directory: ./backend
env:
PENPOT_TEST_DATABASE_URI: "postgresql://postgres/penpot_test"
PENPOT_TEST_DATABASE_USERNAME: penpot_test
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
PENPOT_TEST_REDIS_URI: "redis://redis/1"
run: |
clojure -M:dev:test --reporter kaocha.report/documentation
test-library:
name: "Library Tests"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run tests
working-directory: ./library
run: |
./scripts/test
build-integration:
name: "Build Integration Bundle"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build Bundle
working-directory: ./frontend
run: |
corepack enable;
corepack install;
yarn install
yarn run build:app:assets
yarn run build:app
yarn run build:app:libs
- name: Build WASM
working-directory: "./render-wasm"
run: |
./build release
- name: Store Bundle Cache
uses: actions/cache@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration-1:
name: "Integration Tests 1/4"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Restore Cache
uses: actions/cache/restore@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="1/4";
- name: Upload test result
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-result-1
path: frontend/test-results/
overwrite: true
retention-days: 3
test-integration-2:
name: "Integration Tests 2/4"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Restore Cache
uses: actions/cache/restore@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="2/4";
- name: Upload test result
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-result-2
path: frontend/test-results/
overwrite: true
retention-days: 3
test-integration-3:
name: "Integration Tests 3/4"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Restore Cache
uses: actions/cache/restore@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="3/4";
- name: Upload test result
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-result-3
path: frontend/test-results/
overwrite: true
retention-days: 3
test-integration-4:
name: "Integration Tests 4/4"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Restore Cache
uses: actions/cache/restore@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="4/4";
- name: Upload test result
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-result-4
path: frontend/test-results/
overwrite: true
retention-days: 3

1
.gitignore vendored
View File

@@ -80,3 +80,4 @@ node_modules
/playwright/.cache/
/render-wasm/target/
/**/.yarn/*
/.pnpm-store

View File

@@ -1,13 +1,19 @@
# CHANGELOG
## 2.12.0 (Unreleased)
## 2.12.1
### :bug: Bugs fixed
- Fix setting a portion of text as bold or underline messes things up [Github #7980](https://github.com/penpot/penpot/issues/7980)
## 2.12.0
### :boom: Breaking changes & Deprecations
#### Backend RPC API changes
The backend RPC API URLS are changed from `/api/rpc/command/<name>` to
`/api/main/methods/<name>` (the previou PATH is preserved for backward
`/api/main/methods/<name>`. The previous PATH is preserved for backward
compatibility; however, if you are a user of this API, it is strongly
recommended that you adapt your code to use the new PATH.
@@ -35,7 +41,7 @@ If you have SSO/Social-Auth configured on your on-premise instance,
the following actions are required before update:
Update your OAuth or SSO provider configuration (e.g., Okta, Google,
Azure AD, etc.) to use the new callback URL. Failure to update may
Azure AD, etc.) to use the new callback URL. Failure to update may
result in authentication failures after upgrading.
**Reason for change:**
@@ -45,15 +51,35 @@ and makis it more modular, enabling the ability to configure SSO auth
provider dinamically.
#### Changes on default docker compose
We have updated the `docker/images/docker-compose.yaml` with a small
change related to the `PENPOT_SECRET_KEY`. Since this version, this
environment variable is also required on exporter. So if you are using
penpot on-premise you will need to apply the same changes on your own
`docker-compose.yaml` file.
We have removed the Minio server from the `docker/images/docker-compose.yml`
example. It's still usable as before, we just removed the example.
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
- Enable Hindi translations on the application
### :sparkles: New features & Enhancements
- Select boards to export as PDF [Taiga #12320](https://tree.taiga.io/project/penpot/issue/12320)
- Toggle for switching boolean property values [Taiga #12341](https://tree.taiga.io/project/penpot/us/12341)
- Add the ability to select boards to export as PDF [Taiga #12320](https://tree.taiga.io/project/penpot/issue/12320)
- Add toggle for switching boolean property values [Taiga #12341](https://tree.taiga.io/project/penpot/us/12341)
- Make the file export process more reliable [Taiga #12555](https://tree.taiga.io/project/penpot/us/12555)
- Add auth flow changes [Taiga #12333](https://tree.taiga.io/project/penpot/us/12333)
- Add new shape validation mechanism for shapes [Github #7696](https://github.com/penpot/penpot/pull/7696)
- Apply color tokens from sidebar [Taiga #11353](https://tree.taiga.io/project/penpot/us/11353)
- Display tokens in the inspect tab [Taiga #9313](https://tree.taiga.io/project/penpot/us/9313)
- Refactor clipboard behavior to assess some minor inconsistencies and make pasting binary data faster. [Taiga #12571](https://tree.taiga.io/project/penpot/task/12571)
### :bug: Bugs fixed
@@ -68,6 +94,15 @@ provider dinamically.
- Fix shortcut conflict in text editor (increase/decrease font size vs word selection)
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
- Fix problem with plugins content attribute [Plugins #209](https://github.com/penpot/penpot-plugins/issues/209)
- Fix U and E icon displayed in project list [Taiga #12806](https://tree.taiga.io/project/penpot/issue/12806)
- Fix unpublish library modal not scrolling a long file list [Taiga #12285](https://tree.taiga.io/project/penpot/issue/12285)
- Fix incorrect interaction betwen hower and scroll on assets sidebar [Taiga #12389](https://tree.taiga.io/project/penpot/issue/12389)
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
- Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492)
- Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843)
- Fix unicode handling on email template abbreviation filter [Github #7966](https://github.com/penpot/penpot/pull/7966)
## 2.11.1

View File

@@ -1,7 +1,8 @@
From: {{profile.fullname}} <{{profile.email}}> / {{profile.id}}
Subject: {{feedback-subject}}
Type: {{feedback-type}}
{%- if feedback-error-href %}
{% if feedback-error-href %}
HREF: {{feedback-error-href}}
{% endif -%}

View File

@@ -240,4 +240,4 @@
</div>
</body>
</html>
</html>

View File

@@ -3,7 +3,7 @@
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Tokens%20starter%20kit.penpot"}
{:id "penpot-design-system"
:name "Penpot Design System | Pencil"
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/penpot-app.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Pencil-Penpot-Design-System.penpot"}
{:id "wireframing-kit"
:name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}

View File

@@ -25,8 +25,7 @@
<Logger name="app.storage.tmp" level="info" />
<Logger name="app.worker" level="trace" />
<Logger name="app.msgbus" level="info" />
<Logger name="app.http.websocket" level="info" />
<Logger name="app.http.sse" level="info" />
<Logger name="app.http" level="info" />
<Logger name="app.util.websocket" level="info" />
<Logger name="app.redis" level="info" />
<Logger name="app.rpc.rlimit" level="info" />

View File

@@ -25,8 +25,7 @@
<Logger name="app.storage.tmp" level="info" />
<Logger name="app.worker" level="trace" />
<Logger name="app.msgbus" level="info" />
<Logger name="app.http.websocket" level="info" />
<Logger name="app.http.sse" level="info" />
<Logger name="app.http" level="info" />
<Logger name="app.util.websocket" level="info" />
<Logger name="app.redis" level="info" />
<Logger name="app.rpc.rlimit" level="info" />

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env bash
export PENPOT_MANAGEMENT_API_SHARED_KEY=super-secret-management-api-key
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
export PENPOT_HOST=devenv
export PENPOT_PUBLIC_URI=https://localhost:3449
export PENPOT_FLAGS="\
$PENPOT_FLAGS \

View File

@@ -255,6 +255,8 @@
(write-entry! output path params)
(events/tap :progress {:section :storage-object :id id})
(with-open [input (sto/get-object-data storage sobject)]
(.putNextEntry ^ZipOutputStream output (ZipEntry. (str "objects/" id ext)))
(io/copy input output :size (:size sobject))
@@ -279,6 +281,8 @@
thumbnails (bfc/get-file-object-thumbnails cfg file-id)]
(events/tap :progress {:section :file :id file-id})
(vswap! bfc/*state* update :files assoc file-id
{:id file-id
:name (:name file)
@@ -817,9 +821,10 @@
entries (keep (match-storage-entry-fn) entries)]
(doseq [{:keys [id entry]} entries]
(let [object (->> (read-entry input entry)
(decode-storage-object)
(validate-storage-object))
(let [object (-> (read-entry input entry)
(decode-storage-object)
(update :bucket d/nilv sto/default-bucket)
(validate-storage-object))
ext (cmedia/mtype->extension (:content-type object))
path (str "objects/" id ext)

View File

@@ -5,7 +5,6 @@
;; Copyright (c) KALEIDOS INC
(ns app.config
"A configuration management."
(:refer-clojure :exclude [get])
(:require
[app.common.data :as d]
@@ -103,7 +102,7 @@
[:http-server-io-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int]
[:management-api-shared-key {:optional true} :string]
[:management-api-key {:optional true} :string]
[:telemetry-uri {:optional true} :string]
[:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE

View File

@@ -106,17 +106,17 @@
(let [content-part (MimeBodyPart.)
alternative-mpart (MimeMultipart. "alternative")]
(when-let [content (get body "text/plain")]
(let [text-part (MimeBodyPart.)]
(.setText text-part ^String content ^String charset)
(.addBodyPart alternative-mpart text-part)))
(when-let [content (get body "text/html")]
(let [html-part (MimeBodyPart.)]
(.setContent html-part ^String content
(str "text/html; charset=" charset))
(.addBodyPart alternative-mpart html-part)))
(when-let [content (get body "text/plain")]
(let [text-part (MimeBodyPart.)]
(.setText text-part ^String content ^String charset)
(.addBodyPart alternative-mpart text-part)))
(.setContent content-part alternative-mpart)
(.addBodyPart mixed-mpart content-part))

View File

@@ -50,23 +50,27 @@
(db/tx-run! cfg handler request)))))})
(defmethod ig/init-key ::routes
[_ cfg]
["" {:middleware [[mw/shared-key-auth (cf/get :management-api-shared-key)]
[default-system cfg]
[transaction]]}
["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
[_ {:keys [::setup/props] :as cfg}]
["/get-customer"
{:handler get-customer
:transaction true
:allowed-methods #{:post}}]
(let [management-key (or (cf/get :management-api-key)
(get props :management-key))]
["/update-customer"
{:handler update-customer
:allowed-methods #{:post}
:transaction true}]])
["" {:middleware [[mw/shared-key-auth management-key]
[default-system cfg]
[transaction]]}
["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["/get-customer"
{:handler get-customer
:transaction true
:allowed-methods #{:post}}]
["/update-customer"
{:handler update-customer
:allowed-methods #{:post}
:transaction true}]]))
;; ---- HELPERS

View File

@@ -14,7 +14,9 @@
[app.config :as cf]
[app.http :as-alias http]
[app.http.errors :as errors]
[app.tokens :as tokens]
[app.util.pointer-map :as pmap]
[buddy.core.codecs :as bc]
[cuerdas.core :as str]
[yetti.adapter :as yt]
[yetti.middleware :as ymw]
@@ -242,7 +244,6 @@
(handler request)
{::yres/status 405}))))))})
(defn- wrap-auth
[handler decoders]
(let [token-re
@@ -272,9 +273,24 @@
process-request
(fn [request]
(if-let [{:keys [type token] :as auth} (get-token request)]
(if-let [decode-fn (get decoders type)]
(assoc request ::http/auth-data (assoc auth :claims (decode-fn token)))
(assoc request ::http/auth-data auth))
(let [decode-fn (get decoders type)]
(if (or (= type :cookie) (= type :bearer))
(let [metadata (tokens/decode-header token)]
;; NOTE: we only proceed to decode claims on new
;; cookie tokens. The old cookies dont need to be
;; decoded because they use the token string as ID
(if (and (= (:kid metadata) 1)
(= (:ver metadata) 1)
(some? decode-fn))
(assoc request ::http/auth-data (assoc auth
:claims (decode-fn token)
:metadata metadata))
(assoc request ::http/auth-data (assoc auth :metadata {:ver 0}))))
(if decode-fn
(assoc request ::http/auth-data (assoc auth :claims (decode-fn token)))
(assoc request ::http/auth-data auth))))
request))]
(fn [request]
@@ -287,11 +303,14 @@
(defn- wrap-shared-key-auth
[handler shared-key]
(if shared-key
(fn [request]
(let [key (yreq/get-header request "x-shared-key")]
(if (= key shared-key)
(handler request)
{::yres/status 403})))
(let [shared-key (if (string? shared-key)
shared-key
(bc/bytes->b64-str shared-key true))]
(fn [request]
(let [key (yreq/get-header request "x-shared-key")]
(if (= key shared-key)
(handler request)
{::yres/status 403}))))
(fn [_ _]
{::yres/status 403})))

View File

@@ -93,15 +93,15 @@
(update-session [_ session]
(let [modified-at (ct/now)]
(if (string? (:id session))
(let [params (-> session
(assoc :id (uuid/next))
(assoc :created-at modified-at)
(assoc :modified-at modified-at))]
(db/insert! pool :http-session-v2 params))
(db/insert! pool :http-session-v2
(-> session
(assoc :id (uuid/next))
(assoc :created-at modified-at)
(assoc :modified-at modified-at)))
(db/update! pool :http-session-v2
{:modified-at modified-at}
{:id (:id session)}))))
{:id (:id session)}
{::db/return-keys true}))))
(delete-session [_ id]
(if (string? id)
@@ -158,14 +158,15 @@
(defn- assign-token
[cfg session]
(let [token (tokens/generate cfg
{:iss "authentication"
:aud "penpot"
:sid (:id session)
:iat (:modified-at session)
:uid (:profile-id session)
:sso-provider-id (:sso-provider-id session)
:sso-session-id (:sso-session-id session)})]
(let [claims {:iss "authentication"
:aud "penpot"
:sid (:id session)
:iat (:modified-at session)
:uid (:profile-id session)
:sso-provider-id (:sso-provider-id session)
:sso-session-id (:sso-session-id session)}
header {:kid 1 :ver 1}
token (tokens/generate cfg claims header)]
(assoc session :token token)))
(defn create-fn
@@ -225,13 +226,14 @@
[handler {:keys [::manager] :as cfg}]
(assert (manager? manager) "expected valid session manager")
(fn [request]
(let [{:keys [type token claims]} (get request ::http/auth-data)]
(let [{:keys [type token claims metadata]} (get request ::http/auth-data)]
(cond
(= type :cookie)
(let [session (if-let [sid (:sid claims)]
(read-session manager sid)
(let [session (case (:ver metadata)
;; BACKWARD COMPATIBILITY WITH OLD TOKENS
(read-session manager token))
0 (read-session manager token)
1 (some->> (:sid claims) (read-session manager))
nil)
request (cond-> request
(some? session)
@@ -240,7 +242,7 @@
response (handler request)]
(if (renew-session? session)
(if (and session (renew-session? session))
(let [session (->> session
(update-session manager)
(assign-token cfg))]
@@ -248,11 +250,11 @@
response))
(= type :bearer)
(let [session (if-let [sid (:sid claims)]
(read-session manager sid)
(let [session (case (:ver metadata)
;; BACKWARD COMPATIBILITY WITH OLD TOKENS
(read-session manager token))
0 (read-session manager token)
1 (some->> (:sid claims) (read-session manager))
nil)
request (cond-> request
(some? session)
(-> (assoc ::profile-id (:profile-id session))

View File

@@ -79,18 +79,6 @@
(remove #(contains? reserved-props (key %))))
props))
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(let [context {:external-session-id (::rpc/external-session-id params)
:external-event-origin (::rpc/external-event-origin params)
:triggered-by (::rpc/handler-name params)}]
{::type "action"
::profile-id (::rpc/profile-id params)
::ip-addr (::rpc/ip-addr params)
::context (d/without-nils context)}))
(defn get-external-session-id
[request]
(when-let [session-id (yreq/get-header request "x-external-session-id")]
@@ -99,13 +87,24 @@
(str/blank? session-id))
session-id)))
(defn- get-external-event-origin
(defn- get-client-event-origin
[request]
(when-let [origin (yreq/get-header request "x-event-origin")]
(when-not (or (> (count origin) 256)
(= origin "null")
(when-not (or (= origin "null")
(str/blank? origin))
origin)))
(str/prune origin 200))))
(defn get-client-user-agent
[request]
(when-let [user-agent (yreq/get-header request "user-agent")]
(str/prune user-agent 500)))
(defn- get-client-version
[request]
(when-let [origin (yreq/get-header request "x-frontend-version")]
(when-not (or (= origin "null")
(str/blank? origin))
(str/prune origin 100))))
;; --- SPECS
@@ -134,6 +133,33 @@
(def ^:private check-event
(sm/check-fn schema:event))
(defn- prepare-context-from-request
[request]
(let [client-event-origin (get-client-event-origin request)
client-version (get-client-version request)
client-user-agent (get-client-user-agent request)
session-id (get-external-session-id request)
token-id (::actoken/id request)]
(d/without-nils
{:external-session-id session-id
:access-token-id (some-> token-id str)
:client-event-origin client-event-origin
:client-user-agent client-user-agent
:client-version client-version
:version (:full cf/version)})))
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(let [context (some-> params meta ::http/request prepare-context-from-request)
event {::type "action"
::profile-id (or (::rpc/profile-id params) uuid/zero)
::ip-addr (::rpc/ip-addr params)}]
(cond-> event
(some? context)
(assoc ::context context))))
(defn prepare-event
[cfg mdata params result]
(let [resultm (meta result)
@@ -148,18 +174,10 @@
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
(clean-props))
token-id (::actoken/id request)
context (-> (::context resultm)
(assoc :external-session-id
(get-external-session-id request))
(assoc :external-event-origin
(get-external-event-origin request))
(assoc :access-token-id (some-> token-id str))
(d/without-nils))
context (merge (::context resultm)
(prepare-context-from-request request))
ip-addr (inet/parse-request request)]
{::type (or (::type resultm)

View File

@@ -57,7 +57,7 @@
:uid uuid/zero})
body (t/encode {:events events})
headers {"content-type" "application/transit+json"
"origin" (cf/get :public-uri)
"origin" (str (cf/get :public-uri))
"cookie" (u/map->query-string {:auth-token token})}
params {:uri uri
:timeout 12000

View File

@@ -49,7 +49,7 @@
ctx (-> context
(assoc :tenant (cf/get :tenant))
(assoc :host (cf/get :host))
(assoc :public-uri (cf/get :public-uri))
(assoc :public-uri (str (cf/get :public-uri)))
(assoc :logger/name logger)
(assoc :logger/level level)
(dissoc :request/params :value :params :data))]

View File

@@ -295,7 +295,8 @@
[cfg]
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
(->> (sv/scan-ns
'app.rpc.management.subscription)
'app.rpc.management.subscription
'app.rpc.management.exporter)
(map (partial process-method cfg "management" wrap-management))
(into {}))))
@@ -346,14 +347,16 @@
(assert (valid-methods? (::management-methods params)) "expect valid methods map"))
(defmethod ig/init-key ::routes
[_ {:keys [::methods ::management-methods] :as cfg}]
[_ {:keys [::methods ::management-methods ::setup/props] :as cfg}]
(let [public-uri (cf/get :public-uri)
management-key (or (cf/get :management-api-key)
(get props :management-key))]
(let [public-uri (cf/get :public-uri)]
["/api"
["/management"
["/methods/:type"
{:middleware [[mw/shared-key-auth (cf/get :management-api-shared-key)]
{:middleware [[mw/shared-key-auth management-key]
[session/authz cfg]]
:handler (make-rpc-handler management-methods)}]

View File

@@ -307,7 +307,8 @@
:content-type (:mtype input)})]
(:id sobject))
(catch Throwable cause
(l/err :hint "unable to import profile picture"
(l/wrn :hint "unable to import profile picture"
:uri uri
:cause cause)
nil)))

View File

@@ -11,9 +11,9 @@
[app.binfile.v1 :as bf.v1]
[app.binfile.v3 :as bf.v3]
[app.common.features :as cfeat]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uri :as u]
[app.config :as cf]
[app.db :as db]
[app.http.sse :as sse]
@@ -25,10 +25,12 @@
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.tasks.file-gc]
[app.util.services :as sv]
[app.worker :as-alias wrk]))
[app.worker :as-alias wrk]
[datoteka.fs :as fs]))
(set! *warn-on-reflection* true)
@@ -38,52 +40,42 @@
schema:export-binfile
[:map {:title "export-binfile"}
[:file-id ::sm/uuid]
[:version {:optional true} ::sm/int]
[:include-libraries ::sm/boolean]
[:embed-assets ::sm/boolean]])
(defn stream-export-v1
[cfg {:keys [file-id include-libraries embed-assets] :as params}]
(rph/stream
(fn [_ output-stream]
(try
(-> cfg
(assoc ::bfc/ids #{file-id})
(assoc ::bfc/embed-assets embed-assets)
(assoc ::bfc/include-libraries include-libraries)
(bf.v1/export-files! output-stream))
(catch Throwable cause
(l/err :hint "exception on exporting file"
:file-id (str file-id)
:cause cause))))))
(defn- export-binfile
[{:keys [::sto/storage] :as cfg} {:keys [file-id include-libraries embed-assets]}]
(let [output (tmp/tempfile*)]
(try
(-> cfg
(assoc ::bfc/ids #{file-id})
(assoc ::bfc/embed-assets embed-assets)
(assoc ::bfc/include-libraries include-libraries)
(bf.v3/export-files! output))
(defn stream-export-v3
[cfg {:keys [file-id include-libraries embed-assets] :as params}]
(rph/stream
(fn [_ output-stream]
(try
(-> cfg
(assoc ::bfc/ids #{file-id})
(assoc ::bfc/embed-assets embed-assets)
(assoc ::bfc/include-libraries include-libraries)
(bf.v3/export-files! output-stream))
(catch Throwable cause
(l/err :hint "exception on exporting file"
:file-id (str file-id)
:cause cause))))))
(let [data (sto/content output)
object (sto/put-object! storage
{::sto/content data
::sto/touched-at (ct/in-future {:minutes 60})
:content-type "application/zip"
:bucket "tempfile"})]
(-> (cf/get :public-uri)
(u/join "/assets/by-id/")
(u/join (str (:id object)))))
(finally
(fs/delete output)))))
(sv/defmethod ::export-binfile
"Export a penpot file in a binary format."
{::doc/added "1.15"
::doc/changes [["2.12" "Remove version parameter, only one version is supported"]]
::webhooks/event? true
::sm/params schema:export-binfile}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id version file-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(files/check-read-permissions! pool profile-id file-id)
(let [version (or version 1)]
(case (int version)
1 (stream-export-v1 cfg params)
2 (throw (ex-info "not-implemented" {}))
3 (stream-export-v3 cfg params))))
(sse/response (partial export-binfile cfg params)))
;; --- Command: import-binfile

View File

@@ -39,7 +39,7 @@
fullname (str "Demo User " sem)
password (-> (bn/random-bytes 16)
(bc/bytes->b64u)
(bc/bytes->b64 true)
(bc/bytes->str))
params {:email email

View File

@@ -1209,7 +1209,7 @@
;; --- MUTATION COMMAND: restore-files-immediatelly
(def ^:private sql:resolve-editable-files
"SELECT f.id
"SELECT f.id, f.project_id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
JOIN team AS t ON (t.id = p.team_id)
@@ -1250,18 +1250,38 @@
{:file-id file-id}
{::db/return-keys false}))
(def ^:private sql:restore-projects
"UPDATE project SET deleted_at = null WHERE id = ANY(?::uuid[])")
(defn- restore-projects
[conn project-ids]
(let [project-ids (db/create-array conn "uuid" project-ids)]
(->> (db/exec-one! conn [sql:restore-projects project-ids])
(db/get-update-count))))
(defn- restore-deleted-team-files
[{:keys [::db/conn]} {:keys [::rpc/profile-id team-id ids]}]
(teams/check-edition-permissions! conn profile-id team-id)
(let [total-files
(count ids)
(reduce (fn [affected {:keys [id]}]
(let [index (inc (count affected))]
(events/tap :progress {:file-id id :index index :total (count ids)})
(restore-file conn id)
(conj affected id)))
#{}
(db/plan conn [sql:resolve-editable-files team-id
(db/create-array conn "uuid" ids)])))
{:keys [files projects]}
(reduce (fn [result {:keys [id project-id]}]
(let [index (-> result :files count)]
(events/tap :progress {:file-id id :index index :total total-files})
(restore-file conn id)
(-> result
(update :files conj id)
(update :projects conj project-id))))
{:files #{} :projectes #{}}
(db/plan conn [sql:resolve-editable-files team-id
(db/create-array conn "uuid" ids)]))]
(restore-projects conn projects)
files))
(def ^:private schema:restore-deleted-team-files
[:map {:title "restore-deleted-team-files"}
@@ -1269,8 +1289,8 @@
[:ids [::sm/set ::sm/uuid]]])
(sv/defmethod ::restore-deleted-team-files
"Removes the deletion mark from the specified files (and respective projects)."
"Removes the deletion mark from the specified files (and respective
projects) on the specified team."
{::doc/added "2.12"
::sse/stream? true
::sm/params schema:restore-deleted-team-files}

View File

@@ -96,7 +96,7 @@
;; loading all pages into memory for find the frame set for thumbnail.
(defn get-file-data-for-thumbnail
[{:keys [::db/conn] :as cfg} {:keys [data id] :as file}]
[{:keys [::db/conn] :as cfg} {:keys [data id] :as file} strip-frames-with-thumbnails]
(letfn [;; function responsible on finding the frame marked to be
;; used as thumbnail; the returned frame always have
;; the :page-id set to the page that it belongs.
@@ -173,7 +173,7 @@
;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecessary data.
:always
strip-frames-with-thumbnails
(update :objects assoc-thumbnails page-id thumbs)))))
(def ^:private
@@ -186,7 +186,8 @@
[:map {:title "PartialFile"}
[:id ::sm/uuid]
[:revn {:min 0} ::sm/int]
[:page [:map-of :keyword ::sm/any]]])
[:page [:map-of :keyword ::sm/any]]
[:strip-frames-with-thumbnails {:optional true} ::sm/boolean]])
(sv/defmethod ::get-file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used
@@ -195,7 +196,7 @@
::doc/module :files
::sm/params schema:get-file-data-for-thumbnail
::sm/result schema:partial-file}
[cfg {:keys [::rpc/profile-id file-id] :as params}]
[cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id)
@@ -205,14 +206,18 @@
file (bfc/get-file cfg file-id
:realize? true
:read-only? true)]
:read-only? true)
strip-frames-with-thumbnails
(or (nil? strip-frames-with-thumbnails) ;; if not present, default to true
(true? strip-frames-with-thumbnails))]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file)))
{:file-id file-id
:revn (:revn file)
:page (get-file-data-for-thumbnail cfg file)}))))
:page (get-file-data-for-thumbnail cfg file strip-frames-with-thumbnails)}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS

View File

@@ -169,12 +169,19 @@
;; --- MUTATION: Create Project
(defn- create-project
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}]
(let [project (teams/create-project conn params)]
[{:keys [::db/conn] :as cfg} {:keys [::rpc/request-at profile-id team-id] :as params}]
(assert (ct/inst? request-at) "expect request-at assigned")
(let [params (-> params
(assoc :created-at request-at)
(assoc :modified-at request-at))
project (teams/create-project conn params)
timestamp (::rpc/request-at params)]
(teams/create-project-role conn profile-id (:id project) :owner)
(db/insert! conn :team-project-profile-rel
{:project-id (:id project)
:profile-id profile-id
:created-at timestamp
:modified-at timestamp
:team-id team-id
:is-pinned false})
(assoc project :is-pinned false)))

View File

@@ -39,9 +39,8 @@
(defn- encode
[s]
(-> s
bh/blake2b-256
bc/bytes->b64u
bc/bytes->str))
(bh/blake2b-256)
(bc/bytes->b64-str true)))
(defn- fmt-key
[s]

View File

@@ -0,0 +1,49 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.management.exporter
(:require
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uri :as u]
[app.config :as cf]
[app.media :refer [schema:upload]]
[app.rpc :as-alias rpc]
[app.rpc.doc :as doc]
[app.storage :as sto]
[app.util.services :as sv]))
;; ---- RPC METHOD: UPLOAD-TEMPFILE
(def ^:private
schema:upload-tempfile-params
[:map {:title "upload-templfile-params"}
[:content schema:upload]])
(def ^:private
schema:upload-tempfile-result
[:map {:title "upload-templfile-result"}])
(sv/defmethod ::upload-tempfile
{::doc/added "2.12"
::sm/params schema:upload-tempfile-params
::sm/result schema:upload-tempfile-result}
[cfg {:keys [::rpc/profile-id content]}]
(let [storage (sto/resolve cfg)
hash (sto/calculate-hash (:path content))
data (-> (sto/content (:path content))
(sto/wrap-with-hash hash))
content {::sto/content data
::sto/deduplicate? true
::sto/touched-at (ct/in-future {:minutes 10})
:profile-id profile-id
:content-type (:mtype content)
:bucket "tempfile"}
object (sto/put-object! storage content)]
{:id (:id object)
:uri (-> (cf/get :public-uri)
(u/join "/assets/by-id/")
(u/join (str (:id object))))}))

View File

@@ -104,28 +104,29 @@
(def ^:private schema:limit
[:and
[:map
[::name :any]
[::name :keyword]
[::strategy schema:strategy]
[::key :string]
[::opts :string]]
[:or
[:map
[::capacity ::sm/int]
[::rate ::sm/int]
[::internal ::ct/duration]
[::params [::sm/vec :any]]]
[:map
[::nreq ::sm/int]
[::unit [:enum :days :hours :minutes :seconds :weeks]]]]])
[::opts :string]
[::capacity {:optional true} ::sm/int]
[::rate {:optional true} ::sm/int]
[::interval {:optional true} ::ct/duration]
[::params {:optional true} [::sm/vec :any]]
[::permits {:optional true} ::sm/int]
[::unit {:optional true} [:enum :days :hours :minutes :seconds :weeks]]]
[:fn (fn [attrs]
(let [contains-fn (partial contains? attrs)]
(or (every? contains-fn [::capacity ::rate ::interval])
(every? contains-fn [::permits ::unit]))))]])
(def ^:private schema:limits
[:map-of :keyword [::sm/vec schema:limit]])
(def ^:private valid-limit-tuple?
(sm/lazy-validator schema:limit-tuple))
(sm/validator schema:limit-tuple))
(def ^:private valid-rlimit-instance?
(sm/lazy-validator ::rpc/rlimit))
(sm/validator ::rpc/rlimit))
(defmethod parse-limit :window
[[name strategy opts :as vlimit]]
@@ -134,16 +135,16 @@
(merge
{::name name
::strategy strategy}
(if-let [[_ nreq unit] (re-find window-opts-re opts)]
(let [nreq (parse-long nreq)]
{::nreq nreq
(if-let [[_ permits unit] (re-find window-opts-re opts)]
(let [permits (parse-long permits)]
{::permits permits
::unit (case unit
"d" :days
"h" :hours
"m" :minutes
"s" :seconds
"w" :weeks)
::key (str "ratelimit.window." (d/name name))
::key (str "penpot.rlimit." (cf/get :tenant) ".window." (d/name name))
::opts opts})
(ex/raise :type :validation
:code :invalid-window-limit-opts
@@ -164,15 +165,15 @@
::interval interval
::opts opts
::params [(->seconds interval) rate capacity]
::key (str "ratelimit.bucket." (d/name name))})
::key (str "penpot.rlimit." (cf/get :tenant) ".bucket." (d/name name))})
(ex/raise :type :validation
:code :invalid-bucket-limit-opts
:hint (str/ffmt "looks like '%' does not have a valid format" opts))))
(defmethod process-limit :bucket
[rconn user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
[rconn profile-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
(let [script (-> bucket-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id)])
(assoc ::rscript/keys [(str key "." service "." profile-id)])
(assoc ::rscript/vals (conj params (->seconds now))))
result (rds/eval rconn script)
allowed? (boolean (nth result 0))
@@ -192,18 +193,18 @@
(assoc ::lresult/remaining remaining))))
(defmethod process-limit :window
[rconn user-id now {:keys [::nreq ::unit ::key ::service] :as limit}]
[rconn profile-id now {:keys [::permits ::unit ::key ::service] :as limit}]
(let [ts (ct/truncate now unit)
ttl (ct/diff now (ct/plus ts {unit 1}))
script (-> window-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id "." (ct/format-inst ts))])
(assoc ::rscript/vals [nreq (->seconds ttl)]))
(assoc ::rscript/keys [(str key "." service "." profile-id "." (ct/format-inst ts))])
(assoc ::rscript/vals [permits (->seconds ttl)]))
result (rds/eval rconn script)
allowed? (boolean (nth result 0))
remaining (nth result 1)]
(l/trace :hint "limit processed"
:service service
:limit (name (::name limit))
:name (name (::name limit))
:strategy (name (::strategy limit))
:opts (::opts limit)
:allowed allowed?
@@ -214,8 +215,8 @@
(assoc ::lresult/reset (ct/plus ts {unit 1})))))
(defn- process-limits
[rconn user-id limits now]
(let [results (into [] (map (partial process-limit rconn user-id now)) limits)
[rconn profile-id limits now]
(let [results (into [] (map (partial process-limit rconn profile-id now)) limits)
remaining (->> results
(d/index-by ::name ::lresult/remaining)
(uri/map->query-string))
@@ -227,7 +228,7 @@
(when rejected
(l/warn :hint "rejected rate limit"
:user-id (str user-id)
:profile-id (str profile-id)
:limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name)))
@@ -371,12 +372,9 @@
(defn- on-refresh-error
[_ cause]
(when-not (instance? java.util.concurrent.RejectedExecutionException cause)
(if-let [explain (-> cause ex-data ex/explain)]
(l/warn ::l/raw (str "unable to refresh config, invalid format:\n" explain)
::l/sync? true)
(l/warn :hint "unexpected exception on loading config"
:cause cause
::l/sync? true))))
(l/warn :hint "unexpected exception on loading config"
:cause cause
::l/sync? true)))
(defn- get-config-path
[]

View File

@@ -25,9 +25,9 @@ local allowed = filled >= requested
local newTokens = filled
if allowed then
newTokens = filled - requested
redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp)
end
redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp)
redis.call("expire", tokensKey, ttl)
return { allowed, newTokens }

View File

@@ -22,8 +22,7 @@
(defn- generate-random-key
[]
(-> (bn/random-bytes 64)
(bc/bytes->b64u)
(bc/bytes->str)))
(bc/bytes->b64-str true)))
(defn- get-all-props
[conn]
@@ -85,12 +84,11 @@
(l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
"all sessions on each restart, it is highly recommended setting up the "
"PENPOT_SECRET_KEY environment variable")))
(let [secret (or key (generate-random-key))]
(-> (get-all-props conn)
(assoc :secret-key secret)
(assoc :tokens-key (keys/derive secret :salt "tokens"))
(assoc :management-key (keys/derive secret :salt "management"))
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
;; FIXME
(sm/register! ::props :any)
(sm/register! ::props [:map-of :keyword ::sm/any])

View File

@@ -8,13 +8,13 @@
"Keys derivation service."
(:refer-clojure :exclude [derive])
(:require
[app.common.spec :as us]
[buddy.core.kdf :as bk]))
(defn derive
"Derive a key from secret-key"
[secret-key & {:keys [salt size] :or {size 32}}]
(us/assert! ::us/not-empty-string secret-key)
(assert (string? secret-key) "expect string")
(assert (seq secret-key) "expect string")
(let [engine (bk/engine {:key secret-key
:salt salt
:alg :hkdf

View File

@@ -35,12 +35,16 @@
:assets-s3 :s3
nil)))
(def default-bucket
"file-media-object")
(def valid-buckets
#{"file-media-object"
"team-font-variant"
"file-object-thumbnail"
"file-thumbnail"
"profile"
"tempfile"
"file-data"
"file-data-fragment"
"file-change"})
@@ -163,9 +167,6 @@
backend
(:metadata result))))
(def ^:private sql:retrieve-storage-object
"select * from storage_object where id = ? and (deleted_at is null or deleted_at > now())")
(defn row->storage-object [res]
(let [mdata (or (some-> (:metadata res) (db/decode-transit-pgobject)) {})]
(impl/storage-object
@@ -177,9 +178,15 @@
(keyword (:backend res))
mdata)))
(defn- retrieve-database-object
(def ^:private sql:get-storage-object
"SELECT *
FROM storage_object
WHERE id = ?
AND (deleted_at IS NULL)")
(defn- get-database-object
[conn id]
(some-> (db/exec-one! conn [sql:retrieve-storage-object id])
(some-> (db/exec-one! conn [sql:get-storage-object id])
(row->storage-object)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -202,7 +209,7 @@
(defn get-object
[{:keys [::db/connectable] :as storage} id]
(assert (valid-storage? storage))
(retrieve-database-object connectable id))
(get-database-object connectable id))
(defn put-object!
"Creates a new object with the provided content."

View File

@@ -37,7 +37,6 @@
(into #{} (map :id))
(not-empty))))
(def ^:private sql:delete-sobjects
"DELETE FROM storage_object
WHERE id = ANY(?::uuid[])")
@@ -77,47 +76,37 @@
(d/group-by (comp keyword :backend) :id #{} items))
(def ^:private sql:get-deleted-sobjects
"SELECT s.* FROM storage_object AS s
"SELECT s.*
FROM storage_object AS s
WHERE s.deleted_at IS NOT NULL
AND s.deleted_at < now() - ?::interval
AND s.deleted_at <= ?
ORDER BY s.deleted_at ASC")
(defn- get-buckets
[conn min-age]
(let [age (db/interval min-age)]
[conn]
(let [now (ct/now)]
(sequence
(comp (partition-all 25)
(mapcat group-by-backend))
(db/cursor conn [sql:get-deleted-sobjects age]))))
(db/cursor conn [sql:get-deleted-sobjects now]))))
(defn- clean-deleted!
[{:keys [::db/conn ::min-age] :as cfg}]
[{:keys [::db/conn] :as cfg}]
(reduce (fn [total [backend-id ids]]
(let [deleted (delete-in-bulk! cfg backend-id ids)]
(+ total (or deleted 0))))
0
(get-buckets conn min-age)))
(get-buckets conn)))
(defmethod ig/assert-key ::handler
[_ params]
(assert (sto/valid-storage? (::sto/storage params)) "expect valid storage")
(assert (db/pool? (::db/pool params)) "expect valid storage"))
(defmethod ig/expand-key ::handler
[k v]
{k (assoc v ::min-age (ct/duration {:hours 2}))})
(defmethod ig/init-key ::handler
[_ {:keys [::min-age] :as cfg}]
(fn [{:keys [props] :as task}]
(let [min-age (ct/duration (or (:min-age props) min-age))]
(db/tx-run! cfg (fn [cfg]
(let [cfg (assoc cfg ::min-age min-age)
total (clean-deleted! cfg)]
(l/inf :hint "task finished"
:min-age (ct/format-duration min-age)
:total total)
{:deleted total}))))))
[_ cfg]
(fn [_]
(db/tx-run! cfg (fn [cfg]
(let [total (clean-deleted! cfg)]
(l/inf :hint "task finished" :total total)
{:deleted total})))))

View File

@@ -22,8 +22,10 @@
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.time :as ct]
[app.config :as cf]
[app.db :as db]
[app.storage :as-alias sto]
[app.storage :as sto]
[app.storage.impl :as impl]
[integrant.core :as ig]))
@@ -101,14 +103,15 @@
(def ^:private sql:mark-delete-in-bulk
"UPDATE storage_object
SET deleted_at = now(),
SET deleted_at = ?,
touched_at = NULL
WHERE id = ANY(?::uuid[])")
(defn- mark-delete-in-bulk!
[conn ids]
(let [ids (db/create-array conn "uuid" ids)]
(db/exec-one! conn [sql:mark-delete-in-bulk ids])))
[conn deletion-delay ids]
(let [ids (db/create-array conn "uuid" ids)
now (ct/plus (ct/now) deletion-delay)]
(db/exec-one! conn [sql:mark-delete-in-bulk now ids])))
;; NOTE: A getter that retrieves the key which will be used for group
;; ids; previously we have no value, then we introduced the
@@ -127,7 +130,7 @@
[{:keys [metadata]}]
(or (some-> metadata :bucket)
(some-> metadata :reference d/name)
"file-media-object"))
sto/default-bucket))
(defn- process-objects!
[conn has-refs? bucket objects]
@@ -137,18 +140,20 @@
(if-let [{:keys [id] :as object} (first objects)]
(if (has-refs? conn object)
(do
(l/debug :id (str id)
:status "freeze"
:bucket bucket)
(l/dbg :id (str id)
:status "freeze"
:bucket bucket)
(recur (conj to-freeze id) to-delete (rest objects)))
(do
(l/debug :id (str id)
:status "delete"
:bucket bucket)
(l/dbg :id (str id)
:status "delete"
:bucket bucket)
(recur to-freeze (conj to-delete id) (rest objects))))
(do
(let [deletion-delay (if (= bucket "tempfile")
(ct/duration {:hours 2})
(cf/get-deletion-delay))]
(some->> (seq to-freeze) (mark-freeze-in-bulk! conn))
(some->> (seq to-delete) (mark-delete-in-bulk! conn))
(some->> (seq to-delete) (mark-delete-in-bulk! conn deletion-delay))
[(count to-freeze) (count to-delete)]))))
(defn- process-bucket!
@@ -160,6 +165,7 @@
"file-thumbnail" (process-objects! conn has-file-thumbnails-refs? bucket objects)
"profile" (process-objects! conn has-profile-refs? bucket objects)
"file-data" (process-objects! conn has-file-data-refs? bucket objects)
"tempfile" (process-objects! conn (constantly false) bucket objects)
(ex/raise :type :internal
:code :unexpected-unknown-reference
:hint (dm/fmt "unknown reference '%'" bucket))))
@@ -173,27 +179,27 @@
[0 0]
(d/group-by lookup-bucket identity #{} chunk)))
(def ^:private
sql:get-touched-storage-objects
(def ^:private sql:get-touched-storage-objects
"SELECT so.*
FROM storage_object AS so
WHERE so.touched_at IS NOT NULL
AND so.touched_at <= ?
ORDER BY touched_at ASC
FOR UPDATE
SKIP LOCKED
LIMIT 10")
(defn get-chunk
[conn]
(->> (db/exec! conn [sql:get-touched-storage-objects])
[conn timestamp]
(->> (db/exec! conn [sql:get-touched-storage-objects timestamp])
(map impl/decode-row)
(not-empty)))
(defn- process-touched!
[{:keys [::db/pool] :as cfg}]
[{:keys [::db/pool ::timestamp] :as cfg}]
(loop [freezed 0
deleted 0]
(if-let [chunk (get-chunk pool)]
(if-let [chunk (get-chunk pool timestamp)]
(let [[nfo ndo] (db/tx-run! cfg process-chunk! chunk)]
(recur (long (+ freezed nfo))
(long (+ deleted ndo))))
@@ -209,5 +215,6 @@
(defmethod ig/init-key ::handler
[_ cfg]
(fn [_] (process-touched! cfg)))
(fn [_]
(process-touched! (assoc cfg ::timestamp (ct/now)))))

View File

@@ -79,14 +79,17 @@
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn tempfile
[& {:keys [suffix prefix min-age]
(defn tempfile*
[& {:keys [suffix prefix]
:or {prefix "penpot."
suffix ".tmp"}}]
(let [attrs (fs/make-permissions "rw-r--r--")
path (fs/join default-tmp-dir (str prefix (uuid/next) suffix))
path (Files/createFile path attrs)]
(fs/delete-on-exit! path)
path (fs/join default-tmp-dir (str prefix (uuid/next) suffix))]
(Files/createFile path attrs)))
(defn tempfile
[& {:keys [min-age] :as opts}]
(let [path (tempfile* opts)]
(sp/offer! queue [path (some-> min-age ct/duration)])
path))

View File

@@ -18,15 +18,15 @@
(def ^:private sql:get-profiles
"SELECT id, photo_id FROM profile
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
AND deleted_at <= ?
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-profiles!
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-profiles deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-profiles timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id photo-id]}]
(l/trc :obj "profile" :id (str id))
@@ -41,15 +41,15 @@
(def ^:private sql:get-teams
"SELECT deleted_at, id, photo_id FROM team
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
AND deleted_at <= ?
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-teams!
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-teams deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-teams timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id photo-id deleted-at]}]
(l/trc :obj "team"
:id (str id)
@@ -68,15 +68,15 @@
"SELECT id, team_id, deleted_at, woff1_file_id, woff2_file_id, otf_file_id, ttf_file_id
FROM team_font_variant
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
AND deleted_at <= ?
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-fonts!
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-fonts deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-fonts timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id team-id deleted-at] :as font}]
(l/trc :obj "font-variant"
:id (str id)
@@ -98,15 +98,15 @@
"SELECT id, deleted_at, team_id
FROM project
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
AND deleted_at <= ?
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-projects!
[{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-projects deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-projects timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id team-id deleted-at]}]
(l/trc :obj "project"
:id (str id)
@@ -124,15 +124,15 @@
f.project_id
FROM file AS f
WHERE f.deleted_at IS NOT NULL
AND f.deleted_at < now() + ?::interval
AND f.deleted_at <= ?
ORDER BY f.deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-files!
[{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-files deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-files timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id deleted-at project-id] :as file}]
(l/trc :obj "file"
:id (str id)
@@ -148,15 +148,15 @@
"SELECT file_id, revn, media_id, deleted_at
FROM file_thumbnail
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
AND deleted_at <= ?
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn delete-file-thumbnails!
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-file-thumbnails deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-file-thumbnails timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id revn media-id deleted-at]}]
(l/trc :obj "file-thumbnail"
:file-id (str file-id)
@@ -175,15 +175,15 @@
"SELECT file_id, object_id, media_id, deleted_at
FROM file_tagged_object_thumbnail
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
AND deleted_at <= ?
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn delete-file-object-thumbnails!
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-file-object-thumbnails deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-file-object-thumbnails timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id object-id media-id deleted-at]}]
(l/trc :obj "file-object-thumbnail"
:file-id (str file-id)
@@ -203,15 +203,15 @@
"SELECT id, file_id, media_id, thumbnail_id, deleted_at
FROM file_media_object
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
AND deleted_at <= ?
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-media-objects!
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-file-media-objects deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-file-media-objects timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id file-id deleted-at] :as fmo}]
(l/trc :obj "file-media-object"
:id (str id)
@@ -231,16 +231,15 @@
"SELECT file_id, id, type, deleted_at, metadata, backend
FROM file_data
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
AND deleted_at <= ?
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-data!
[{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-file-data deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-file-data timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id id type deleted-at metadata backend]}]
(some->> metadata
@@ -266,15 +265,15 @@
"SELECT id, file_id, deleted_at
FROM file_change
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
AND deleted_at <= ?
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-changes!
[{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-file-change deletion-threshold chunk-size] {:fetch-size 5})
[{:keys [::db/conn ::timestamp ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-file-change timestamp chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id file-id deleted-at] :as xlog}]
(l/trc :obj "file-change"
:id (str id)
@@ -322,9 +321,8 @@
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [props] :as task}]
(let [threshold (ct/duration (get props :deletion-threshold 0))
cfg (assoc cfg ::deletion-threshold (db/interval threshold))]
(fn [_]
(let [cfg (assoc cfg ::timestamp (ct/now))]
(loop [procs (map deref deletion-proc-vars)
total 0]
(if-let [proc-fn (first procs)]

View File

@@ -15,19 +15,25 @@
[buddy.sign.jwe :as jwe]))
(defn generate
[{:keys [::setup/props] :as cfg} claims]
(assert (contains? cfg ::setup/props))
([cfg claims] (generate cfg claims nil))
([{:keys [::setup/props] :as cfg} claims header]
(assert (contains? props :tokens-key) "expect props to have tokens-key")
(let [tokens-key
(get props :tokens-key)
(let [tokens-key
(get props :tokens-key)
payload
(-> claims
(update :iat (fn [v] (or v (ct/now))))
(d/without-nils)
(t/encode))]
payload
(-> claims
(update :iat (fn [v] (or v (ct/now))))
(d/without-nils)
(t/encode))]
(jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm})))
(jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm :header header}))))
(defn decode-header
[token]
(ex/ignoring
(jwe/decode-header token)))
(defn decode
[{:keys [::setup/props] :as cfg} token]

View File

@@ -27,7 +27,7 @@
(throw (IllegalArgumentException. "Missing arguments on `defmethod` macro.")))
(let [mdata (assoc mdata
::docstring (some-> docs str/<<-)
::docstring (some-> docs str/unindent)
::spec sname
::name (name sname))

View File

@@ -7,9 +7,17 @@
(ns app.util.template
(:require
[app.common.exceptions :as ex]
[cuerdas.core :as str]
[selmer.filters :as sf]
[selmer.parser :as sp]))
(sp/cache-off!)
;; (sp/cache-off!)
(sf/add-filter! :abbreviate
(fn [s n]
(let [n (parse-long n)]
(str/abbreviate s n))))
(defn render
[path context]

View File

@@ -137,33 +137,34 @@ RETURNING task.id, task.queue")
::wait)))
(run-batch []
(let [rconn (rds/connect cfg)]
(try
(-> cfg
(assoc ::rds/conn rconn)
(db/tx-run! run-batch'))
(try
(let [rconn (rds/connect cfg)]
(try
(-> cfg
(assoc ::rds/conn rconn)
(db/tx-run! run-batch'))
(finally
(.close ^AutoCloseable rconn))))
(catch InterruptedException cause
(throw cause))
(catch Exception cause
(cond
(rds/exception? cause)
(do
(l/wrn :hint "redis exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
(catch InterruptedException cause
(throw cause))
(db/sql-exception? cause)
(do
(l/wrn :hint "database exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
(catch Exception cause
(cond
(rds/exception? cause)
(do
(l/wrn :hint "redis exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
:else
(do
(l/err :hint "unhandled exception (will retry in an instant)" :cause cause)
(px/sleep timeout))))
(db/sql-exception? cause)
(do
(l/wrn :hint "database exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
(finally
(.close ^AutoCloseable rconn)))))
:else
(do
(l/err :hint "unhandled exception (will retry in an instant)" :cause cause)
(px/sleep timeout))))))
(dispatcher []
(l/inf :hint "started")
@@ -176,7 +177,7 @@ RETURNING task.id, task.queue")
(catch InterruptedException _
(l/trc :hint "interrupted"))
(catch Throwable cause
(l/err :hint " unexpected exception" :cause cause))
(l/err :hint "unexpected exception" :cause cause))
(finally
(l/inf :hint "terminated"))))]

View File

@@ -30,6 +30,7 @@
[app.rpc.commands.files :as files]
[app.rpc.commands.files-create :as files.create]
[app.rpc.commands.files-update :as files.update]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.helpers :as rph]
[app.util.blob :as blob]
@@ -185,15 +186,17 @@
(defn create-project*
([i params] (create-project* *system* i params))
([system i {:keys [profile-id team-id] :as params}]
(us/assert uuid? profile-id)
(us/assert uuid? team-id)
(db/run! system
(fn [{:keys [::db/conn]}]
(->> (merge {:id (mk-uuid "project" i)
:name (str "project" i)}
params)
(#'teams/create-project conn))))))
(assert (uuid? profile-id))
(assert (uuid? team-id))
(let [timestamp (ct/now)]
(db/run! system
(fn [cfg]
(->> (merge {:id (mk-uuid "project" i)
:name (str "project" i)}
params
{::rpc/request-at timestamp})
(#'projects/create-project cfg)))))))
(defn create-file*
([i params]

View File

@@ -9,6 +9,7 @@
[app.common.features :as cfeat]
[app.common.pprint :as pp]
[app.common.thumbnails :as thc]
[app.common.time :as ct]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -16,6 +17,7 @@
[app.db.sql :as sql]
[app.http :as http]
[app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto]
[backend-tests.helpers :as th]
[clojure.test :as t]
@@ -132,9 +134,10 @@
;; this will run pending task triggered by deleting user snapshot
(th/run-pending-tasks!)
(let [res (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
;; delete 2 snapshots and 2 file data entries
(t/is (= 4 (:processed res))))))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})]
;; delete 2 snapshots and 2 file data entries
(t/is (= 4 (:processed res)))))))))
(t/deftest snapshots-locking
(let [profile-1 (th/create-profile* 1 {:is-active true})

View File

@@ -313,7 +313,7 @@
;; freeze because of the deduplication (we have uploaded 2 times
;; the same files).
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 2 (:freeze res)))
(t/is (= 0 (:delete res))))
@@ -372,14 +372,14 @@
(th/db-exec! ["update file_change set deleted_at = now() where file_id = ? and label is not null" (:id file)])
(th/db-exec! ["update file set has_media_trimmed = false where id = ?" (:id file)])
(let [res (th/run-task! :objects-gc {:deletion-threshold 0})]
(let [res (th/run-task! :objects-gc {})]
;; this will remove the file change and file data entries for two snapshots
(t/is (= 4 (:processed res))))
;; Rerun the file-gc and objects-gc
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [res (th/run-task! :objects-gc {:deletion-threshold 0})]
(let [res (th/run-task! :objects-gc {})]
;; this will remove the file media objects marked as deleted
;; on prev file-gc
(t/is (= 2 (:processed res))))
@@ -387,7 +387,7 @@
;; Now that file-gc have deleted the file-media-object usage,
;; lets execute the touched-gc task, we should see that two of
;; them are marked to be deleted
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res))))
@@ -572,7 +572,7 @@
;; Now that file-gc have deleted the file-media-object usage,
;; lets execute the touched-gc task, we should see that two of
;; them are marked to be deleted.
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res))))
@@ -665,7 +665,7 @@
;; because of the deduplication (we have uploaded 2 times the
;; same files).
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 1 (:freeze res)))
(t/is (= 0 (:delete res))))
@@ -715,7 +715,7 @@
;; Now that objects-gc have deleted the object thumbnail lets
;; execute the touched-gc task
(let [res (th/run-task! "storage-gc-touched" {:min-age 0})]
(let [res (th/run-task! "storage-gc-touched" {})]
(t/is (= 1 (:freeze res))))
;; check file media objects
@@ -750,7 +750,7 @@
;; Now that file-gc have deleted the object thumbnail lets
;; execute the touched-gc task
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 1 (:delete res))))
;; check file media objects
@@ -922,8 +922,9 @@
(t/is (= 0 (:processed result))))
;; run permanent deletion
(let [result (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
(t/is (= 3 (:processed result))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed result)))))
;; query the list of file libraries of a after hard deletion
(let [data {::th/type :get-file-libraries
@@ -1134,7 +1135,7 @@
(th/sleep 300)
;; run the task
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; check that object thumbnails are still here
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
@@ -1163,7 +1164,7 @@
(t/is (= 2 (count rows))))
;; run the task again
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; check that we have all object thumbnails
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
@@ -1226,7 +1227,7 @@
(t/is (= 2 (count rows)))))
(t/testing "gc task"
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [rows (th/db-query :file-thumbnail {:file-id (:id file)})]
(t/is (= 2 (count rows)))
@@ -1273,7 +1274,7 @@
;; The FileGC task will schedule an inner taskq
(th/run-pending-tasks!)
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 2 (:freeze res)))
(t/is (= 0 (:delete res))))
@@ -1367,7 +1368,7 @@
;; we ensure that once object-gc is passed and marked two storage
;; objects to delete
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res))))
@@ -1489,7 +1490,7 @@
(t/is (some? (not-empty (:objects component))))))
;; Re-run the file-gc task
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [row (th/db-get :file {:id (:id file)})]
(t/is (true? (:has-media-trimmed row))))
@@ -1519,7 +1520,7 @@
;; Now, we have deleted the usage of component if we pass file-gc,
;; that component should be deleted
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; Check that component is properly removed
(let [data {::th/type :get-file
@@ -1610,8 +1611,8 @@
:component-id c-id})}])
;; Run the file-gc on file and library
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-1)})))
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-2)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file-1)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file-2)})))
;; Check that component exists
(let [data {::th/type :get-file
@@ -1684,7 +1685,7 @@
;; Now, we have deleted the usage of component if we pass file-gc,
;; that component should be deleted
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-1)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file-1)})))
;; Check that component is properly removed
(let [data {::th/type :get-file
@@ -1833,8 +1834,8 @@
(t/is (not= (:id fill) (:id fmedia)))))
;; Run the file-gc on file and library
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-1)})))
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-2)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file-1)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file-2)})))
;; Now proceed to delete file and absorb it
(let [data {::th/type :delete-file
@@ -1925,7 +1926,7 @@
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
(t/is (= (:deleted-at row) now)))))))
(t/deftest deleted-files-restore
(t/deftest restore-deleted-files
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
@@ -1988,3 +1989,78 @@
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
(t/is (nil? (:deleted-at row)))))))
(t/deftest restore-deleted-files-and-projets
(let [profile (th/create-profile* 1 {:is-active true})
team-id (:default-team-id profile)
now (ct/inst "2025-10-31T00:00:00Z")]
(binding [ct/*clock* (clock/fixed now)]
(let [project (th/create-project* 1 {:profile-id (:id profile)
:team-id team-id})
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:id project)})
data {::th/type :delete-project
:id (:id project)
::rpc/profile-id (:id profile)}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))
(th/run-pending-tasks!)
;; get deleted files
(let [data {::th/type :get-team-deleted-files
::rpc/profile-id (:id profile)
:team-id team-id}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [[row1 :as result] (:result out)]
(t/is (= 1 (count result)))
(t/is (= (:will-be-deleted-at row1) #penpot/inst "2025-11-07T00:00:00Z"))
(t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z"))
(t/is (= (:modified-at row1) #penpot/inst "2025-10-31T00:00:00Z"))))
;; Check if project is deleted
(let [[row1 :as rows] (th/db-query :project {:id (:id project)})]
;; (pp/pprint rows)
(t/is (= 1 (count rows)))
(t/is (= (:deleted-at row1) #penpot/inst "2025-11-07T00:00:00Z"))
(t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z"))
(t/is (= (:modified-at row1) #penpot/inst "2025-10-31T00:00:00Z")))
;; Restore files
(let [data {::th/type :restore-deleted-team-files
::rpc/profile-id (:id profile)
:team-id team-id
:ids #{(:id file)}}
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (fn? result))
(let [events (th/consume-sse result)]
;; (pp/pprint events)
(t/is (= 2 (count events)))
(t/is (= :end (first (last events))))
(t/is (= (:ids data) (last (last events)))))))
(let [[row1 :as rows] (th/db-query :file {:project-id (:id project)})]
;; (pp/pprint rows)
(t/is (= 1 (count rows)))
(t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z"))
(t/is (nil? (:deleted-at row1))))
;; Check if project is restored
(let [[row1 :as rows] (th/db-query :project {:id (:id project)})]
;; (pp/pprint rows)
(t/is (= 1 (count rows)))
(t/is (= (:created-at row1) #penpot/inst "2025-10-31T00:00:00Z"))
(t/is (nil? (:deleted-at row1))))))))

View File

@@ -8,12 +8,14 @@
(:require
[app.common.pprint :as pp]
[app.common.thumbnails :as thc]
[app.common.time :as ct]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as cauth]
[app.setup.clock :as clock]
[app.storage :as sto]
[app.tokens :as tokens]
[backend-tests.helpers :as th]
@@ -83,7 +85,8 @@
(t/is (map? (:result out))))
;; run the task again
(let [res (th/run-task! "storage-gc-touched" {:min-age 0})]
(let [res (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 31}))]
(th/run-task! "storage-gc-touched" {}))]
(t/is (= 2 (:freeze res))))
(let [[row1 row2 :as rows] (th/db-query :file-tagged-object-thumbnail
@@ -114,9 +117,9 @@
;; Run the File GC task that should remove unused file object
;; thumbnails
(th/run-task! :file-gc {:min-age 0 :file-id (:id file)})
(th/run-task! :file-gc {:file-id (:id file)})
(let [result (th/run-task! :objects-gc {:min-age 0})]
(let [result (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed result))))
;; check if row2 related thumbnail row still exists
@@ -133,7 +136,8 @@
(t/is (some? (sto/get-object storage (:media-id row2))))
;; run the task again
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 31}))]
(th/run-task! :storage-gc-touched {}))]
(t/is (= 1 (:delete res)))
(t/is (= 0 (:freeze res))))
@@ -143,8 +147,9 @@
;; Run the storage gc deleted task, it should permanently delete
;; all storage objects related to the deleted thumbnails
(let [result (th/run-task! :storage-gc-deleted {:min-age 0})]
(t/is (= 1 (:deleted result))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted res)))))
(t/is (nil? (sto/get-object storage (:media-id row1))))
(t/is (some? (sto/get-object storage (:media-id row2))))
@@ -216,9 +221,9 @@
;; Run the File GC task that should remove unused file object
;; thumbnails
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [result (th/run-task! :objects-gc {:min-age 0})]
(let [result (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed result))))
;; check if row1 related thumbnail row still exists
@@ -230,7 +235,7 @@
(t/is (= (:object-id data1) (:object-id row)))
(t/is (uuid? (:media-id row1))))
(let [result (th/run-task! :storage-gc-touched {:min-age 0})]
(let [result (th/run-task! :storage-gc-touched {})]
(t/is (= 1 (:delete result))))
;; Check if storage objects still exists after file-gc
@@ -242,8 +247,9 @@
;; Run the storage gc deleted task, it should permanently delete
;; all storage objects related to the deleted thumbnails
(let [result (th/run-task! :storage-gc-deleted {:min-age 0})]
(t/is (= 1 (:deleted result))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted result)))))
(t/is (some? (sto/get-object storage (:media-id row2)))))))

View File

@@ -6,11 +6,13 @@
(ns backend-tests.rpc-font-test
(:require
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http :as http]
[app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto]
[backend-tests.helpers :as th]
[clojure.test :as t]
@@ -129,7 +131,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 6 (:freeze res))))
(let [params {::th/type :delete-font
@@ -141,16 +143,17 @@
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res))))
(let [res (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
(t/is (= 2 (:processed res))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 6 (:delete res))))))
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 6 (:delete res)))))))
(t/deftest font-deletion-2
(let [prof (th/create-profile* 1 {:is-active true})
@@ -189,7 +192,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 6 (:freeze res))))
(let [params {::th/type :delete-font
@@ -201,16 +204,17 @@
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res))))
(let [res (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
(t/is (= 1 (:processed res))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed res))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 3 (:delete res))))))
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 3 (:delete res)))))))
(t/deftest font-deletion-3
(let [prof (th/create-profile* 1 {:is-active true})
@@ -248,7 +252,7 @@
(t/is (nil? (:error out1)))
(t/is (nil? (:error out2)))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 6 (:freeze res))))
(let [params {::th/type :delete-font-variant
@@ -260,13 +264,14 @@
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res))))
(let [res (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
(t/is (= 1 (:processed res))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed res))))
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 3 (:delete res))))))
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 3 (:delete res)))))))

View File

@@ -6,11 +6,13 @@
(ns backend-tests.rpc-project-test
(:require
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http :as http]
[app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[backend-tests.helpers :as th]
[clojure.test :as t]))
@@ -226,8 +228,9 @@
(t/is (= 0 (count result)))))
;; run permanent deletion
(let [result (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
(t/is (= 1 (:processed result))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed result)))))
;; query the list of files of a after hard deletion
(let [data {::th/type :get-project-files

View File

@@ -13,6 +13,7 @@
[app.db :as db]
[app.http :as http]
[app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto]
[app.tokens :as tokens]
[backend-tests.helpers :as th]
@@ -525,8 +526,9 @@
(t/is (= :not-found (:type edata)))))
;; run permanent deletion
(let [result (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
(t/is (= 2 (:processed result))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed result)))))
;; query the list of projects of a after hard deletion
(let [data {::th/type :get-projects
@@ -581,8 +583,9 @@
(t/is (= 1 (count rows)))
(t/is (ct/inst? (:deleted-at (first rows)))))
(let [result (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
(t/is (= 7 (:processed result))))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})]
(t/is (= 7 (:processed result)))))))
(t/deftest create-team-access-request
(with-mocks [mock {:target 'app.email/send! :return nil}]

View File

@@ -11,6 +11,7 @@
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto]
[backend-tests.helpers :as th]
[clojure.test :as t]
@@ -53,19 +54,13 @@
(configure-storage-backend))
content (sto/content "content")
object (sto/put-object! storage {::sto/content content
::sto/expired-at (ct/in-future {:seconds 1})
::sto/expired-at (ct/in-future {:hours 1})
:content-type "text/plain"})]
(t/is (sto/object? object))
(t/is (ct/inst? (:expired-at object)))
(t/is (ct/is-after? (:expired-at object) (ct/now)))
(t/is (= object (sto/get-object storage (:id object))))
(th/sleep 1000)
(t/is (nil? (sto/get-object storage (:id object))))
(t/is (nil? (sto/get-object-data storage object)))
(t/is (nil? (sto/get-object-url storage object)))
(t/is (nil? (sto/get-object-path storage object)))))
(t/is (nil? (sto/get-object storage (:id object))))))
(t/deftest put-and-delete-object
(let [storage (-> (:app.storage/storage th/*system*)
@@ -98,20 +93,25 @@
::sto/expired-at (ct/now)
:content-type "text/plain"})
object2 (sto/put-object! storage {::sto/content content2
::sto/expired-at (ct/in-past {:hours 2})
::sto/expired-at (ct/in-future {:hours 2})
:content-type "text/plain"})
object3 (sto/put-object! storage {::sto/content content3
::sto/expired-at (ct/in-past {:hours 1})
::sto/expired-at (ct/in-future {:hours 1})
:content-type "text/plain"})]
(th/sleep 200)
(let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted res))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 0}))]
(let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted res)))))
(let [res (th/db-exec-one! ["select count(*) from storage_object;"])]
(t/is (= 2 (:count res))))))
(t/is (= 2 (:count res))))
(binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 61}))]
(let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted res)))))
(let [res (th/db-exec-one! ["select count(*) from storage_object;"])]
(t/is (= 1 (:count res))))))
(t/deftest touched-gc-task-1
(let [storage (-> (:app.storage/storage th/*system*)
@@ -158,7 +158,7 @@
{:id (:id result-1)})
;; run the objects gc task for permanent deletion
(let [res (th/run-task! :objects-gc {:min-age 0})]
(let [res (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed res))))
;; check that we still have all the storage objects
@@ -182,7 +182,6 @@
(let [res (th/db-exec-one! ["select count(*) from storage_object where deleted_at is not null"])]
(t/is (= 0 (:count res)))))))
(t/deftest touched-gc-task-2
(let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend))
@@ -243,11 +242,12 @@
{:id (:id result-2)})
;; run the objects gc task for permanent deletion
(let [res (th/run-task! :objects-gc {:min-age 0})]
(let [res (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed res))))
;; revert touched state to all storage objects
(th/db-exec-one! ["update storage_object set touched_at=now()"])
(th/db-exec-one! ["update storage_object set touched_at=?" (ct/now)])
;; Run the task again
(let [res (th/run-task! :storage-gc-touched {})]
@@ -293,10 +293,10 @@
result-2 (:result out2)]
;; now we proceed to manually mark all storage objects touched
(th/db-exec! ["update storage_object set touched_at=now()"])
(th/db-exec! ["update storage_object set touched_at=?" (ct/now)])
;; run the touched gc task
(let [res (th/run-task! "storage-gc-touched" {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 2 (:freeze res)))
(t/is (= 0 (:delete res))))
@@ -305,16 +305,48 @@
(t/is (= 2 (count rows)))))
;; now we proceed to manually delete all file_media_object
(th/db-exec! ["update file_media_object set deleted_at = now()"])
(th/db-exec! ["update file_media_object set deleted_at = ?" (ct/now)])
(let [res (th/run-task! "objects-gc" {:min-age 0})]
(let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res))))
;; run the touched gc task
(let [res (th/run-task! "storage-gc-touched" {:min-age 0})]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res))))
;; check that we have all no objects
(let [rows (th/db-exec! ["select * from storage_object where deleted_at is null"])]
(t/is (= 0 (count rows))))))
(t/deftest tempfile-bucket-test
(let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend))
content1 (sto/content "content1")
now (ct/now)
object1 (sto/put-object! storage {::sto/content content1
::sto/touched-at (ct/plus now {:minutes 1})
:bucket "tempfile"
:content-type "text/plain"})]
(binding [ct/*clock* (clock/fixed now)]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res)))))
(binding [ct/*clock* (clock/fixed (ct/plus now {:minutes 1}))]
(let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res)))
(t/is (= 1 (:delete res)))))
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 1}))]
(let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 0 (:deleted res)))))
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 2}))]
(let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 0 (:deleted res)))))))

View File

@@ -17,7 +17,7 @@
org.slf4j/slf4j-api {:mvn/version "2.0.17"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.40"}
selmer/selmer {:mvn/version "1.12.62"}
selmer/selmer {:mvn/version "1.12.69"}
criterium/criterium {:mvn/version "0.4.6"}
metosin/jsonista {:mvn/version "0.3.13"}
@@ -30,7 +30,7 @@
integrant/integrant {:mvn/version "1.0.0"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2025.06.16-414"}
funcool/cuerdas {:mvn/version "2026.415"}
funcool/promesa
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"
:git/url "https://github.com/funcool/promesa"}
@@ -48,12 +48,8 @@
com.sun.mail/jakarta.mail {:mvn/version "2.0.2"}
org.la4j/la4j {:mvn/version "0.6.0"}
;; exception printing
fipp/fipp {:mvn/version "0.6.29"}
me.flowthing/pp {:mvn/version "2024-11-13.77"}
io.aviso/pretty {:mvn/version "1.4.4"}
environ/environ {:mvn/version "1.2.0"}}
:paths ["src" "vendor" "target/classes"]

7
common/scripts/test Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -ex
corepack enable;
corepack install;
yarn install;
yarn run test;

View File

@@ -9,10 +9,10 @@
(:refer-clojure :exclude [get-in select-keys str with-open max])
#?(:cljs (:require-macros [app.common.data.macros]))
(:require
#?(:clj [cljs.analyzer.api :as aapi])
#?(:clj [clojure.core :as c]
:cljs [cljs.core :as c])
[app.common.data :as d]
[cljs.analyzer.api :as aapi]
[cuerdas.core :as str]))
(defmacro select-keys
@@ -44,42 +44,43 @@
[& params]
`(str/concat ~@params))
(defmacro export
"A helper macro that allows reexport a var in a current namespace."
[v]
(if (boolean (:ns &env))
#?(:clj
(defmacro export
"A helper macro that allows reexport a var in a current namespace."
[v]
(if (boolean (:ns &env))
;; Code for ClojureScript
(let [mdata (aapi/resolve &env v)
arglists (second (get-in mdata [:meta :arglists]))
sym (symbol (c/name v))
andsym (symbol "&")
procarg #(if (= % andsym) % (gensym "param"))]
(if (pos? (count arglists))
`(def
~(with-meta sym (:meta mdata))
(fn ~@(for [args arglists]
(let [args (map procarg args)]
(if (some #(= andsym %) args)
(let [[sargs dargs] (split-with #(not= andsym %) args)]
`([~@sargs ~@dargs] (apply ~v ~@sargs ~@(rest dargs))))
`([~@args] (~v ~@args)))))))
`(def ~(with-meta sym (:meta mdata)) ~v)))
;; Code for ClojureScript
(let [mdata (aapi/resolve &env v)
arglists (second (get-in mdata [:meta :arglists]))
sym (symbol (c/name v))
andsym (symbol "&")
procarg #(if (= % andsym) % (gensym "param"))]
(if (pos? (count arglists))
`(def
~(with-meta sym (:meta mdata))
(fn ~@(for [args arglists]
(let [args (map procarg args)]
(if (some #(= andsym %) args)
(let [[sargs dargs] (split-with #(not= andsym %) args)]
`([~@sargs ~@dargs] (apply ~v ~@sargs ~@(rest dargs))))
`([~@args] (~v ~@args)))))))
`(def ~(with-meta sym (:meta mdata)) ~v)))
;; Code for Clojure
(let [vr (resolve v)
m (meta vr)
n (:name m)
n (with-meta n
(cond-> {}
(:dynamic m) (assoc :dynamic true)
(:protocol m) (assoc :protocol (:protocol m))))]
`(let [m# (meta ~vr)]
(def ~n (deref ~vr))
(alter-meta! (var ~n) merge (dissoc m# :name))
;; (when (:macro m#)
;; (.setMacro (var ~n)))
~vr))))
;; Code for Clojure
(let [vr (resolve v)
m (meta vr)
n (:name m)
n (with-meta n
(cond-> {}
(:dynamic m) (assoc :dynamic true)
(:protocol m) (assoc :protocol (:protocol m))))]
`(let [m# (meta ~vr)]
(def ~n (deref ~vr))
(alter-meta! (var ~n) merge (dissoc m# :name))
;; (when (:macro m#)
;; (.setMacro (var ~n)))
~vr)))))
(defmacro fmt
"String interpolation helper. Can only be used with strings known at

View File

@@ -53,6 +53,7 @@
"plugins/runtime"
"tokens/numeric-input"
"design-tokens/v1"
"text-editor/v2-html-paste"
"text-editor/v2"
"render-wasm/v1"
"variants/v1"})
@@ -75,6 +76,7 @@
(def frontend-only-features
#{"styles/v2"
"plugins/runtime"
"text-editor/v2-html-paste"
"text-editor/v2"
"tokens/numeric-input"
"render-wasm/v1"})
@@ -124,6 +126,7 @@
:feature-plugins "plugins/runtime"
:feature-design-tokens "design-tokens/v1"
:feature-text-editor-v2 "text-editor/v2"
:feature-text-editor-v2-html-paste "text-editor/v2-html-paste"
:feature-render-wasm "render-wasm/v1"
:feature-variants "variants/v1"
:feature-token-input "tokens/numeric-input"

View File

@@ -371,7 +371,7 @@
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib ctob/schema:tokens-lib]]]
[:tokens-lib [:maybe ctob/schema:tokens-lib]]]]
[:set-token
[:map {:title "SetTokenChange"}
@@ -463,35 +463,16 @@
;; Changes Processing Impl
(defn validate-shapes!
[data-old data-new items]
(letfn [(validate-shape! [[page-id id]]
(let [shape-old (dm/get-in data-old [:pages-index page-id :objects id])
shape-new (dm/get-in data-new [:pages-index page-id :objects id])]
;; If object has changed or is new verify is correct
(when (and (some? shape-new)
(not= shape-old shape-new))
(when-not (and (cts/valid-shape? shape-new)
(cts/shape? shape-new))
(ex/raise :type :assertion
:code :data-validation
:hint (str "invalid shape found after applying changes on file "
(:id data-new))
:file-id (:id data-new)
::sm/explain (cts/explain-shape shape-new))))))]
(->> (into #{} (map :page-id) items)
(mapcat (fn [page-id]
(filter #(= page-id (:page-id %)) items)))
(mapcat (fn [{:keys [type id page-id] :as item}]
(sequence
(map (partial vector page-id))
(case type
(:add-obj :mod-obj :del-obj) (cons id nil)
(:mov-objects :reg-objects) (:shapes item)
nil))))
(run! validate-shape!))))
#_:clj-kondo/ignore
(defn- validate-shape
[{:keys [id] :as shape} page-id]
(when-not (cts/valid-shape? shape)
(ex/raise :type :assertion
:code :data-validation
:hint (str "invalid shape found '" id "'")
:page-id page-id
:shape-id id
::sm/explain (cts/explain-shape shape))))
(defn- process-touched-change
[data {:keys [id page-id component-id]}]
@@ -518,14 +499,8 @@
(check-changes items))
(binding [*touched-changes* (volatile! #{})]
(let [result (reduce #(or (process-change %1 %2) %1) data items)
result (reduce process-touched-change result @*touched-changes*)]
;; Validate result shapes (only on the backend)
;;
;; TODO: (PERF) add changed shapes tracking and only validate
;; the tracked changes instead of iterate over all shapes
#?(:clj (validate-shapes! data result items))
result))))
(let [result (reduce #(or (process-change %1 %2) %1) data items)]
(reduce process-touched-change result @*touched-changes*)))))
;; --- Comment Threads
@@ -613,9 +588,10 @@
(defmethod process-change :add-obj
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}]
(let [update-container
(fn [container]
(ctst/add-shape id obj container frame-id parent-id index ignore-touched))]
;; NOTE: we only perform hard validation on backend
#?(:clj (validate-shape obj page-id))
(let [update-container #(ctst/add-shape id obj % frame-id parent-id index ignore-touched)]
(when *state*
(swap! *state* collect-shape-media-refs obj page-id))
@@ -638,6 +614,9 @@
(when (and *state* page-id)
(swap! *state* collect-shape-media-refs shape page-id))
;; NOTE: we only perform hard validation on backend
#?(:clj (validate-shape shape page-id))
(assoc objects id shape))
objects))
@@ -692,8 +671,6 @@
(d/update-in-when data [:pages-index page-id] fix-container)
(d/update-in-when data [:components component-id] fix-container))))
;; FIXME: remove, seems like this method is already unused
;; reg-objects operation "regenerates" the geometry and selrect of the parent groups
(defmethod process-change :reg-objects
[data {:keys [page-id component-id shapes]}]
;; FIXME: Improve performance
@@ -722,48 +699,60 @@
(update-group [group objects]
(let [lookup (d/getf objects)
children (get group :shapes)]
(cond
;; If the group is empty we don't make any changes. Will be removed by a later process
(empty? children)
group
children (get group :shapes)
group (cond
;; If the group is empty we don't make any changes. Will be removed by a later process
(empty? children)
group
(= :bool (:type group))
(path/update-bool-shape group objects)
(= :bool (:type group))
(path/update-bool-shape group objects)
(:masked-group group)
(->> (map lookup children)
(set-mask-selrect group))
(:masked-group group)
(->> (map lookup children)
(set-mask-selrect group))
:else
(->> (map lookup children)
(gsh/update-group-selrect group)))))]
:else
(->> (map lookup children)
(gsh/update-group-selrect group)))]
#?(:clj (validate-shape group page-id))
group))]
(if page-id
(d/update-in-when data [:pages-index page-id :objects] reg-objects)
(d/update-in-when data [:components component-id :objects] reg-objects))))
(defmethod process-change :mov-objects
[data {:keys [parent-id shapes index page-id component-id ignore-touched after-shape allow-altering-copies syncing]}]
;; FIXME: ignore-touched is no longer used, so we can consider it deprecated
[data {:keys [parent-id shapes index page-id component-id #_ignore-touched after-shape allow-altering-copies syncing]}]
(letfn [(calculate-invalid-targets [objects shape-id]
(let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))]
(->> (get-in objects [shape-id :shapes])
(reduce reduce-fn #{shape-id}))))
;; Avoid placing a shape as a direct or indirect child of itself,
;; or inside its main component if it's in a copy,
;; or inside a copy, or from a copy
;; Avoid placing a shape as a direct or indirect child of itself, or
;; inside its main component if it's in a copy, or inside a copy, or
;; from a copy
(is-valid-move? [objects shape-id]
(let [invalid-targets (calculate-invalid-targets objects shape-id)
shape (get objects shape-id)]
(and shape
(not (invalid-targets parent-id))
(not (cfh/components-nesting-loop? objects shape-id parent-id))
(or allow-altering-copies ;; In some cases (like a component swap) it's allowed to change the structure of a copy
syncing ;; If we are syncing the changes of a main component, it's allowed to change the structure of a copy
(and
(not (ctk/in-component-copy? (get objects (:parent-id shape)))) ;; We don't want to change the structure of component copies
(not (ctk/in-component-copy? (get objects parent-id)))))))) ;; We need to check the origin and target frames
(or
;; In some cases (like a component
;; swap) it's allowed to change the
;; structure of a copy
allow-altering-copies
;; DEPRECATED, remove once v2.12 released
syncing
(and
;; We don't want to change the structure of component copies
(not (ctk/in-component-copy? (get objects (:parent-id shape))))
;; We need to check the origin and target frames
(not (ctk/in-component-copy? (get objects parent-id))))))))
(insert-items [prev-shapes index shapes]
(let [prev-shapes (or prev-shapes [])]
@@ -772,17 +761,13 @@
(cfh/append-at-the-end prev-shapes shapes))))
(add-to-parent [parent index shapes]
(let [parent (-> parent
(update :shapes insert-items index shapes)
;; We need to ensure that no `nil` in the
;; shapes list after adding all the
;; incoming shapes to the parent.
(update :shapes d/vec-without-nils))]
(cond-> parent
(and (:shape-ref parent)
(#{:group :frame} (:type parent))
(not ignore-touched))
(dissoc :remote-synced))))
(update parent :shapes
(fn [parent-shapes]
(-> parent-shapes
(insert-items index shapes)
;; We need to ensure that no `nil` in the shapes list
;; after adding all the incoming shapes to the parent.
(d/vec-without-nils)))))
(remove-from-old-parent [old-objects objects shape-id]
(let [prev-parent-id (dm/get-in old-objects [shape-id :parent-id])]
@@ -790,58 +775,63 @@
;; the new destination target parent id.
(if (= prev-parent-id parent-id)
objects
(let [sid shape-id
pid prev-parent-id
obj (get objects pid)
component? (and (:shape-ref obj)
(= (:type obj) :group)
(not ignore-touched))]
(-> objects
(d/update-in-when [pid :shapes] d/without-obj sid)
(d/update-in-when [pid :shapes] d/vec-without-nils)
(cond-> component? (d/update-when pid #(dissoc % :remote-synced))))))))
(d/update-in-when objects [prev-parent-id :shapes]
(fn [shapes]
(-> shapes
(d/without-obj shape-id)
(d/vec-without-nils)))))))
(update-parent-id [objects id]
(-> objects
(d/update-when id assoc :parent-id parent-id)))
(d/update-when objects id assoc :parent-id parent-id))
;; Updates the frame-id references that might be outdated
(assign-frame-id [frame-id objects id]
(let [objects (d/update-when objects id assoc :frame-id frame-id)
obj (get objects id)]
(update-frame-id [frame-id objects id]
(let [obj (some-> (get objects id)
(assoc :frame-id frame-id))]
(cond-> objects
;; If we moving frame, the parent frame is the root
;; and we DO NOT NEED update children because the
;; children will point correctly to the frame what we
;; are currently moving
(not= :frame (:type obj))
(as-> $$ (reduce (partial assign-frame-id frame-id) $$ (:shapes obj))))))
(some? obj)
(assoc id obj)
;; If we moving a frame, we DO NOT NEED update
;; children because the children will point correctly
;; to the frame what we are currently moving
(not (cfh/frame-shape? obj))
(as-> $$ (reduce (partial update-frame-id frame-id) $$ (:shapes obj))))))
(validate-shape [objects #_:clj-kondo/ignore shape-id]
#?(:clj (when-let [shape (get objects shape-id)]
(validate-shape shape page-id)))
objects)
(move-objects [objects]
(let [valid? (every? (partial is-valid-move? objects) shapes)
parent (get objects parent-id)
after-shape-index (d/index-of (:shapes parent) after-shape)
index (if (nil? after-shape-index) index (inc after-shape-index))
frame-id (if (= :frame (:type parent))
(:id parent)
(:frame-id parent))]
(let [parent (get objects parent-id)]
;; Do not proceed with the move if parent does not
;; exists; this can happen on a race condition when an
;; inflight move operations lands when parent is deleted
(if (and (seq shapes) (every? (partial is-valid-move? objects) shapes) parent)
(let [index (or (some-> (d/index-of (:shapes parent) after-shape) inc) index)
frame-id (if (cfh/frame-shape? parent)
(:id parent)
(:frame-id parent))]
(as-> objects $
;; Add the new shapes to the parent object.
(d/update-when $ parent-id #(add-to-parent % index shapes))
(if (and valid? (seq shapes))
(as-> objects $
;; Add the new shapes to the parent object.
(d/update-when $ parent-id #(add-to-parent % index shapes))
;; Update each individual shape link to the new parent
(reduce update-parent-id $ shapes)
;; Update each individual shape link to the new parent
(reduce update-parent-id $ shapes)
;; Analyze the old parents and clear the old links
;; only if the new parent is different form old
;; parent.
(reduce (partial remove-from-old-parent objects) $ shapes)
;; Analyze the old parents and clear the old links
;; only if the new parent is different form old
;; parent.
(reduce (partial remove-from-old-parent objects) $ shapes)
;; Ensure that all shapes of the new parent has a
;; correct link to the topside frame.
(reduce (partial update-frame-id frame-id) $ shapes)
;; Perform validation of the affected shapes
(reduce validate-shape $ shapes)))
;; Ensure that all shapes of the new parent has a
;; correct link to the topside frame.
(reduce (partial assign-frame-id frame-id) $ shapes))
objects)))]
(if page-id

View File

@@ -120,11 +120,8 @@
:terms-and-privacy-checkbox
;; Only for developtment.
:tiered-file-data-storage
:token-units
:token-base-font-size
:token-color
:token-typography-types
:token-typography-composite
:token-shadow
:transit-readable-response
:user-feedback
@@ -132,7 +129,6 @@
:v2-migration
:webhooks
;; TODO: deprecate this flag and consolidate the code
:export-file-v3
:render-wasm-dpr
:hide-release-modal
:subscriptions
@@ -172,9 +168,8 @@
:enable-google-fonts-provider
:enable-component-thumbnails
:enable-render-wasm-dpr
:enable-token-units
:enable-token-typography-types
:enable-token-typography-composite
:enable-token-color
:enable-inspect-styles
:enable-feature-fdata-objects-map])
(defn parse

View File

@@ -12,8 +12,11 @@
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.common :as gco]
[app.common.logging :as log]
[app.common.logic.shapes :as cls]
[app.common.logic.variant-properties :as clvp]
@@ -26,6 +29,7 @@
[app.common.types.library :as ctl]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.path.segment :as segment]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.interactions :as ctsi]
@@ -1512,7 +1516,7 @@
:shapes [(:id shape)]
:index index-after
:ignore-touched true
:syncing true}))
:allow-altering-copies true}))
(update :undo-changes conj (make-change
container
{:type :mov-objects
@@ -1520,7 +1524,7 @@
:shapes [(:id shape)]
:index index-before
:ignore-touched true
:syncing true})))]
:allow-altering-copies true})))]
(if (and (ctk/touched-group? parent :shapes-group) omit-touched?)
changes
@@ -1876,6 +1880,44 @@
roperations'
uoperations')))))))
(defn- set-path-new-values
[current-shape prev-shape transform]
(let [new-content (segment/transform-content
(:content current-shape)
(gmt/transform-in (gpt/point 0 0) transform))
new-points (-> (segment/content->selrect new-content)
(grc/rect->points))
points-center (gco/points->center new-points)
new-selrect (gsh/calculate-selrect new-points points-center)
shape (assoc current-shape
:content new-content
:points new-points
:selrect new-selrect)
prev-center (segment/content-center (:content prev-shape))
delta (gpt/subtract points-center (first new-points))
new-pos (gpt/subtract prev-center delta)]
(gsh/absolute-move shape new-pos)))
(defn- switch-path-change-value
[prev-shape ;; The shape before the switch
current-shape ;; The shape after the switch (a clean copy)
ref-shape ;; The referenced shape on the main component
;; before the switch
attr]
(let [old-width (-> ref-shape :selrect :width)
new-width (-> prev-shape :selrect :width)
old-height (-> ref-shape :selrect :height)
new-height (-> prev-shape :selrect :height)
transform (-> (gpt/point (/ new-width old-width)
(/ new-height old-height))
(gmt/scale-matrix))
shape (set-path-new-values current-shape prev-shape transform)]
(get shape attr)))
(defn- switch-text-change-value
[prev-content ;; The :content of the text before the switch
@@ -2027,6 +2069,10 @@
(= :content attr)
(touched attr-group))
path-change?
(and (= :path (:type current-shape))
(contains? #{:points :selrect :content} attr))
;; position-data is a special case because can be affected by :geometry-group and :content-group
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
@@ -2055,6 +2101,12 @@
(:content origin-ref-shape)
touched)
path-change?
(switch-path-change-value previous-shape
current-shape
origin-ref-shape
attr)
:else
(get previous-shape attr)))

View File

@@ -281,7 +281,20 @@
(defn check-fn
"Create a predefined check function"
[s & {:keys [hint type code]}]
(let [s (schema s)
(let [s #?(:clj
(schema s)
:cljs
(try
(schema s)
(catch :default cause
(let [data (ex-data cause)]
(if (= :malli.core/invalid-schema (:type data))
(throw (ex-info
(str "Invalid schema\n"
(pp/pprint-str (:data data)))
{}))
(throw cause))))))
validator* (delay (m/validator s))
explainer* (delay (m/explainer s))
hint (or ^boolean hint "check error")
@@ -842,38 +855,6 @@
choices))]
{:pred pred}))})
;; (register!
;; {:type ::inst
;; :pred tm/instant?
;; :type-properties
;; {:title "inst"
;; :description "Satisfies Inst protocol"
;; :error/message "should be an instant"
;; :gen/gen (->> (sg/small-int :min 0 :max 100000)
;; (sg/fmap (fn [v] (tm/parse-inst v))))
;; :decode/string tm/parse-inst
;; :encode/string tm/format-inst
;; :decode/json tm/parse-inst
;; :encode/json tm/format-inst
;; ::oapi/type "string"
;; ::oapi/format "iso"}})
;; (register!
;; {:type ::timestamp
;; :pred tm/instant?
;; :type-properties
;; {:title "inst"
;; :description "Satisfies Inst protocol, the same as ::inst but encodes to epoch"
;; :error/message "should be an instant"
;; :gen/gen (->> (sg/small-int)
;; (sg/fmap (fn [v] (tm/parse-inst v))))
;; :decode/string tm/parse-inst
;; :encode/string inst-ms
;; :decode/json tm/parse-inst
;; :encode/json inst-ms
;; ::oapi/type "string"
;; ::oapi/format "number"}})
#?(:clj
(register!
@@ -951,7 +932,7 @@
:pred #(and (string? %) (not (str/blank? %)))
:property-pred
(fn [{:keys [min max] :as props}]
(if (seq props)
(if (or min max)
(fn [value]
(let [size (count value)]
(cond

View File

@@ -362,24 +362,24 @@
component (ctkl/get-component component-file (:component-id top-instance) true)
remote-shape (get-ref-shape component-file component shape)
component-container (get-component-container component-file component)
[remote-shape component-container]
[remote-shape component-container component-file]
(if (some? remote-shape)
[remote-shape component-container]
[remote-shape component-container component-file]
;; If not found, try the case of this being a fostered or swapped children
(let [head-instance (ctn/get-head-shape (:objects container) shape)
component-file (get-in libraries [(:component-file head-instance) :data])
head-component (ctkl/get-component component-file (:component-id head-instance) true)
remote-shape' (get-ref-shape component-file head-component shape)
component-container (get-component-container component-file component)]
[remote-shape' component-container]))]
(let [head-instance (ctn/get-head-shape (:objects container) shape)
component-file (get-in libraries [(:component-file head-instance) :data])
head-component (ctkl/get-component component-file (:component-id head-instance) true)
remote-shape' (get-ref-shape component-file head-component shape)
component-container' (get-component-container component-file head-component)]
[remote-shape' component-container' component-file]))]
(if (nil? remote-shape)
nil
(if (nil? (:shape-ref remote-shape))
(cond-> remote-shape
(and remote-shape with-context?)
(with-meta {:file {:id (:id file-data)
:data file-data}
(with-meta {:file {:id (:id component-file)
:data component-file}
:container component-container}))
(find-remote-shape component-container libraries remote-shape :with-context? with-context?)))))
@@ -1082,33 +1082,35 @@
detach-shape
(fn [objects shape]
(l/debug :hint "detach-shape"
:file-id file-id
:component-ref-file (get-component-ref-file objects shape)
::l/sync? true)
(cond-> shape
(not= file-id (:fill-color-ref-file shape))
(dissoc :fill-color-ref-id :fill-color-ref-file)
(let [shape' (cond-> shape
(not= file-id (:fill-color-ref-file shape))
(dissoc :fill-color-ref-id :fill-color-ref-file)
(not= file-id (:stroke-color-ref-file shape))
(dissoc :stroke-color-ref-id :stroke-color-ref-file)
(not= file-id (:stroke-color-ref-file shape))
(dissoc :stroke-color-ref-id :stroke-color-ref-file)
(not= file-id (get-component-ref-file objects shape))
(dissoc :component-id :component-file :shape-ref :component-root)
(not= file-id (get-component-ref-file objects shape))
(dissoc :component-id :component-file :shape-ref :component-root)
(= :text (:type shape))
(update :content detach-text)))
(= :text (:type shape))
(update :content detach-text))]
(when (not= shape shape')
(l/dbg :hint "detach shape"
:file-id (str file-id)
:shape-id (str (:id shape))))
shape'))
detach-objects
(fn [objects]
(update-vals objects #(detach-shape objects %)))
(d/update-vals objects #(detach-shape objects %)))
detach-pages
(fn [pages-index]
(update-vals pages-index #(update % :objects detach-objects)))]
(d/update-vals pages-index #(update % :objects detach-objects)))]
(-> file
(update-in [:data :pages-index] detach-pages))))
(update-in file [:data :pages-index] detach-pages)))
;; Base font size

View File

@@ -11,6 +11,7 @@
[app.common.exceptions :as ex]
[app.common.flags :as flags]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.types.color :as types.color]
[app.common.types.fills.impl :as impl]
[clojure.core :as c]
@@ -49,12 +50,19 @@
(= 1 (count result))))
(def schema:fill
[:and schema:fill-attrs
[:fn has-valid-fill-attrs?]])
[:and schema:fill-attrs [:fn has-valid-fill-attrs?]])
(def check-fill
(sm/check-fn schema:fill))
(def ^:private schema:fills-as-vector
[:vector {:gen/max 2} schema:fill])
(def schema:fills
[:or {:gen/gen (sg/generator schema:fills-as-vector)}
schema:fills-as-vector
[:fn impl/fills?]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CONSTRUCTORS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -267,3 +267,4 @@
(-> (stp/convert-to-path shape objects)
(update :content impl/path-data))))
(dm/export impl/decode-segments)

View File

@@ -565,6 +565,9 @@
(def check-content
(sm/check-fn schema:content))
(def decode-segments
(sm/lazy-decoder schema:segments sm/json-transformer))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CONSTRUCTORS & PREDICATES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -19,7 +19,7 @@
[app.common.schema.generators :as sg]
[app.common.transit :as t]
[app.common.types.color :as clr]
[app.common.types.fills :refer [schema:fill fill->color]]
[app.common.types.fills :refer [schema:fills fill->color]]
[app.common.types.grid :as ctg]
[app.common.types.path :as path]
[app.common.types.plugins :as ctpg]
@@ -192,8 +192,7 @@
[:locked {:optional true} :boolean]
[:hidden {:optional true} :boolean]
[:masked-group {:optional true} :boolean]
[:fills {:optional true}
[:vector {:gen/max 2} schema:fill]]
[:fills {:optional true} schema:fills]
[:proportion {:optional true} ::sm/safe-number]
[:proportion-lock {:optional true} :boolean]
[:constraints-h {:optional true}

View File

@@ -7,7 +7,7 @@
(ns app.common.types.shape.text
(:require
[app.common.schema :as sm]
[app.common.types.fills :refer [schema:fill]]))
[app.common.types.fills :refer [schema:fills]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
@@ -32,8 +32,7 @@
[:type [:= "paragraph"]]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} schema:fill]]]
[:maybe schema:fills]]
[:font-family {:optional true} ::sm/text]
[:font-size {:optional true} ::sm/text]
[:font-style {:optional true} ::sm/text]
@@ -49,8 +48,7 @@
[:text :string]
[:key {:optional true} :string]
[:fills {:optional true}
[:maybe
[:vector {:gen/max 2} schema:fill]]]
[:maybe schema:fills]]
[:font-family {:optional true} ::sm/text]
[:font-size {:optional true} ::sm/text]
[:font-style {:optional true} ::sm/text]
@@ -71,7 +69,7 @@
[:y ::sm/safe-number]
[:width ::sm/safe-number]
[:height ::sm/safe-number]
[:fills [:vector {:gen/max 2} schema:fill]]
[:fills schema:fills]
[:font-family {:optional true} ::sm/text]
[:font-size {:optional true} ::sm/text]
[:font-style {:optional true} ::sm/text]

View File

@@ -90,6 +90,10 @@
[{:fill-color clr/black
:fill-opacity 1}])
(def default-paragraph-attrs
{:text-align "left"
:text-direction "ltr"})
(def default-text-attrs
{:font-id "sourcesanspro"
:font-family "sourcesanspro"

View File

@@ -266,10 +266,6 @@
typography-token-keys
#{:line-height}))
;; TODO: Created to extract the font-size feature from the typography feature flag.
;; Delete this once the typography feature flag is removed.
(def ff-typography-keys (set/difference typography-keys font-size-keys))
(def ^:private schema:number
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
schema:rotation])

View File

@@ -1410,8 +1410,8 @@ Will return a value that matches this schema:
;; NOTE: we can't assign statically at eval time the value of a
;; function that is declared but not defined; so we need to pass
;; an anonymous function and delegate the resolution to runtime
{:encode/json #(export-dtcg-json %)
:decode/json #(read-multi-set-dtcg %)
{:encode/json #(some-> % export-dtcg-json)
:decode/json #(some-> % read-multi-set-dtcg)
;; FIXME: add better, more reallistic generator
:gen/gen (->> (sg/small-int)
(sg/fmap (fn [_]
@@ -1679,7 +1679,7 @@ Will return a value that matches this schema:
["id" {:optional true} :string]
["name" :string]
["description" :string]
["isSource" :boolean]
["isSource" {:optional true} :boolean]
["selectedTokenSets"
[:map-of :string [:enum "enabled" "disabled"]]]]]]
["$metadata" {:optional true}
@@ -1794,17 +1794,19 @@ Will return a value that matches this schema:
data (without any case transformation). Used as schema decoder and
in the SDK."
[data]
(let [data (if (string? data)
(json/decode data :key-fn identity)
data)
data #?(:cljs (if (object? data)
(json/->clj data :key-fn identity)
data)
:clj data)
(if (instance? TokensLib data)
data
(let [data (if (string? data)
(json/decode data :key-fn identity)
data)
data #?(:cljs (if (object? data)
(json/->clj data :key-fn identity)
data)
:clj data)
data (decode-multi-set-dtcg-data data)]
(-> (check-multi-set-dtcg-data data)
(parse-multi-set-dtcg-json))))
data (decode-multi-set-dtcg-data data)]
(-> (check-multi-set-dtcg-data data)
(parse-multi-set-dtcg-json)))))
(defn- parse-multi-set-legacy-json
"Parse a decoded json file with multi sets in legacy format into a TokensLib."

View File

@@ -311,16 +311,22 @@
[variant]
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
(def ^:private boolean-pairs
[["on" "off"]
["yes" "no"]
["true" "false"]])
(defn find-boolean-pair
"Given a vector, return the map from 'bool-values' that contains both as keys.
Returns nil if none match."
[v]
(let [bool-values [{"on" true "off" false}
{"yes" true "no" false}
{"true" true "false" false}]]
"Given a vector, return a map that contains the boolean equivalency if the values match
with any of the boolean pairs. Returns nil if none match."
[[a b :as v]]
(let [a' (-> a str/trim str/lower)
b' (-> b str/trim str/lower)]
(when (= (count v) 2)
(some (fn [b]
(when (and (contains? b (first v))
(contains? b (last v)))
b))
bool-values))))
(some (fn [[t f]]
(cond (and (= a' t)
(= b' f)) {a true b false}
(and (= b' t)
(= a' f)) {b true a false}
:else nil))
boolean-pairs))))

View File

@@ -163,9 +163,11 @@
(t/deftest find-boolean-pair
(t/testing "find-boolean-pair"
(t/is (= (ctv/find-boolean-pair ["off" "on"]) {"on" true "off" false}))
(t/is (= (ctv/find-boolean-pair ["on" "off"]) {"on" true "off" false}))
(t/is (= (ctv/find-boolean-pair ["off" "on"]) {"on" true "off" false}))
(t/is (= (ctv/find-boolean-pair ["OfF" "oN"]) {"oN" true "OfF" false}))
(t/is (= (ctv/find-boolean-pair [" ofF" "oN "]) {"oN " true " ofF" false}))
(t/is (= (ctv/find-boolean-pair ["on" "off"]) {"on" true "off" false}))
(t/is (= (ctv/find-boolean-pair ["yes" "no"]) {"yes" true "no" false}))
(t/is (= (ctv/find-boolean-pair ["false" "true"]) {"true" true "false" false}))
(t/is (= (ctv/find-boolean-pair ["off" "on" "other"]) nil))
(t/is (= (ctv/find-boolean-pair ["yes" "no"]) {"yes" true "no" false}))
(t/is (= (ctv/find-boolean-pair ["false" "true"]) {"true" true "false" false}))
(t/is (= (ctv/find-boolean-pair ["hello" "bye"]) nil))))
(t/is (= (ctv/find-boolean-pair ["hello" "bye"]) nil))))

View File

@@ -101,13 +101,45 @@ RUN set -eux; \
corepack enable; \
rm -rf /tmp/nodejs.tar.gz;
################################################################################
## CADDYSERVER SETUP
################################################################################
FROM base AS setup-caddy
ENV CADDY_VERSION=2.10.2
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
BINARY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_arm64.tar.gz"; \
;; \
amd64|x86_64) \
BINARY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_amd64.tar.gz"; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
curl -LfsSo /tmp/caddy.tar.gz ${BINARY_URL}; \
mkdir -p /tmp/caddy; \
cd /tmp/caddy; \
tar -xf /tmp/caddy.tar.gz; \
chown -R root /tmp/caddy; \
mv /tmp/caddy/caddy /usr/bin/; \
rm -rf /tmp/caddy.tar.gz; \
rm -rf /tmp/caddy;
################################################################################
## JVM SETUP
################################################################################
FROM base AS setup-jvm
ENV CLOJURE_VERSION=1.12.2.1565
ENV CLOJURE_VERSION=1.12.3.1577
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
@@ -393,6 +425,7 @@ COPY --from=setup-utils /opt/utils /opt/utils
COPY --from=setup-rust /opt/cargo /opt/cargo
COPY --from=setup-rust /opt/rustup /opt/rustup
COPY --from=setup-rust /opt/emsdk /opt/emsdk
COPY --from=setup-caddy /usr/bin/caddy /usr/bin/caddy
COPY files/nginx.conf /etc/nginx/nginx.conf
COPY files/nginx-mime.types /etc/nginx/mime.types
@@ -403,6 +436,9 @@ COPY files/vimrc /root/.vimrc
COPY files/tmux.conf /root/.tmux.conf
COPY files/sudoers /etc/sudoers
COPY files/Caddyfile /home/
COPY files/selfsigned.crt /home/
COPY files/selfsigned.key /home/
COPY files/start-tmux.sh /home/start-tmux.sh
COPY files/start-tmux-back.sh /home/start-tmux-back.sh
COPY files/entrypoint.sh /home/entrypoint.sh

View File

@@ -33,6 +33,8 @@ services:
- 3447:3447
- 3448:3448
- 3449:3449
- 3449:3449/udp
- 3450:3450
- 6006:6006
- 6060:6060
- 6061:6061
@@ -82,6 +84,11 @@ services:
- 9000:9000
- 9001:9001
networks:
default:
aliases:
- minio
postgres:
image: postgres:16.8
command: postgres -c config_file=/etc/postgresql.conf
@@ -110,6 +117,11 @@ services:
volumes:
- "valkey_data:/data"
networks:
default:
aliases:
- redis
mailer:
image: sj26/mailcatcher:latest
restart: always
@@ -118,6 +130,12 @@ services:
ports:
- "1080:1080"
networks:
default:
aliases:
- mailer
# https://github.com/rroemhild/docker-test-openldap
ldap:
image: rroemhild/test-openldap:2.1
@@ -131,3 +149,9 @@ services:
nofile:
soft: 1024
hard: 1024
networks:
default:
aliases:
- ldap

View File

@@ -0,0 +1,12 @@
{
auto_https off
}
localhost:3449 {
reverse_proxy localhost:4449
tls /home/selfsigned.crt /home/selfsigned.key
}
http://localhost:3450 {
reverse_proxy localhost:4449
}

View File

@@ -2,4 +2,5 @@
set -e
nginx
tail -f /dev/null
caddy start -c /home/Caddyfile
tail -f /dev/null;

View File

@@ -12,7 +12,7 @@ http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_timeout 100;
types_hash_max_size 2048;
server_tokens off;
@@ -55,7 +55,7 @@ http {
proxy_cache_key "$host$request_uri";
server {
listen 3449 default_server;
listen 4449 default_server;
server_name _;
client_max_body_size 300M;
@@ -141,6 +141,10 @@ http {
proxy_pass http://127.0.0.1:5000;
}
location /nitrate/ {
proxy_pass http://127.0.0.1:3000/;
}
location /playground {
alias /home/penpot/penpot/experiments/;
add_header Cache-Control "no-cache, max-age=0";
@@ -219,26 +223,19 @@ http {
add_header X-Cache-Status $upstream_cache_status;
}
location ~ ^/js/config.js$ {
add_header Cache-Control "no-store, no-cache, max-age=0" always;
location ~* \.(jpg|png|svg|ttf|woff|woff2)$ {
add_header Cache-Control "public, max-age=604800" always; # 7 days
}
location ~* \.(js|css|jpg|svg|png|mjs|map)$ {
# We set no cache only on devenv
add_header Cache-Control "no-store, no-cache, max-age=0" always;
# add_header Cache-Control "max-age=604800" always; # 7 days
}
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
location ~* \.(js|css|wasm)$ {
add_header Cache-Control "no-store" always;
}
location ~ ^/[^/]+/(.*)$ {
return 301 " /404";
}
add_header Last-Modified $date_gmt;
add_header Cache-Control "no-store, no-cache, max-age=0" always;
if_modified_since off;
add_header Cache-Control "no-store" always;
try_files $uri /index.html$is_args$args /index.html =404;
}
}

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDuzCCAqOgAwIBAgIUa3THJQSn1+ErK65g1jDL0tjUkBYwDQYJKoZIhvcNAQEL
BQAwXzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2Nh
bDEOMAwGA1UECgwFTG9jYWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxo
b3N0MB4XDTI1MTIwMjA4MjUyM1oXDTI2MTIwMjA4MjUyM1owXzELMAkGA1UEBhMC
VVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2NhbDEOMAwGA1UECgwFTG9j
YWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyVIlfpIPE+QyL/q7IQOilEA7wEOZ6wbsh2Fr
59H1gSLFvgoCxI6RVUkQ/MFRnw/r1ZbAqRpc2xAl5a9Ml14q20Zlj6dAHsWX6O2J
EwNsD18dQmX3BncnjV3yCZM2iQcMFKuXG4KQNdIQNNvdIgtlrHYp0ohS9s3XC7cj
KxNrm/pW9EAXfn9AYDd/qER090L2E4ipP9m/5l3MjinNc4l2kpH9rLOgb79H0RLt
PK3/KP8ErZhAvzdmDBAdM5Z5K37b+TfB/kSVNUKL6qyw5CCjlShERLhBNprlnRfz
tHNIQ1RHq3qJJN19ZnJrLqICuQ5ztvj7hBDiOSV0LnmyKgXr6wIDAQABo28wbTAd
BgNVHQ4EFgQUPL8WGf6z/wB8TimJBx1zybsIeikwHwYDVR0jBBgwFoAUPL8WGf6z
/wB8TimJBx1zybsIeikwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2Nh
bGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBACMMVyR3kbNxnzuUc2lahKH4
cPXVWOsvCvnDtjzm41XmKjUJTbtjn3p5d/ZmLbZ4zzIQULfWXO3XG/HevkvVo0g6
6pJXTXc6C6ZhFG0rIYMcPPzmGmalDV5n+lUaCVx5XbFFxvRQ7893auwhRATdwGs+
xiMyYbE2w9otKqyDItmJZJ5nW6vmXJ42YHxlXF18u9U88xqtOSMd5xZahbsmw7Gg
A4/o4TPoAX5QfA306sL443WaczsF7bmsTf9qcYa/3xxQkP5Seyqx8ePWpS22qysE
jG6XPpymxb6sb2mVaFBAzhEMb/eBvE9nRAopxmB7uV4TbqC51K/U3uo6jFX4Jbw=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJUiV+kg8T5DIv
+rshA6KUQDvAQ5nrBuyHYWvn0fWBIsW+CgLEjpFVSRD8wVGfD+vVlsCpGlzbECXl
r0yXXirbRmWPp0AexZfo7YkTA2wPXx1CZfcGdyeNXfIJkzaJBwwUq5cbgpA10hA0
290iC2WsdinSiFL2zdcLtyMrE2ub+lb0QBd+f0BgN3+oRHT3QvYTiKk/2b/mXcyO
Kc1ziXaSkf2ss6Bvv0fREu08rf8o/wStmEC/N2YMEB0zlnkrftv5N8H+RJU1Qovq
rLDkIKOVKEREuEE2muWdF/O0c0hDVEereokk3X1mcmsuogK5DnO2+PuEEOI5JXQu
ebIqBevrAgMBAAECggEABqtE+LNn8nW9v98jcc2IBjc2g4D5yVJaZYWxqGVJJ7T6
Lfhw7Qf4AoZAHM9en9FMM7Ahw7hO2SboynoLJHyHGOp1FNQqiJptFNdBkjKr0rqI
4pk0HK+3zLQO/4gz50gne0vP3qZtlorV5Jpf8e/Et3jWm9XOQcTB2e6AKL4k827B
dv4Tld+/7PoZVXjahfrUWuIZr5mzyF1eUkD8sPOpdr3HJxSueqsOMjbG8XMRqCQ+
5eCWWSW5yPQlMr7M7cXM+a0k73Xn1sKl7fP3/9byji25zxGUaMu5RA1kw0Oqseid
RXuRxGphGZgnx1aFxDAPg3FtmGch7/Cc6WfqboOL0QKBgQD4GZO1gGaE8cg4lvuo
ZUX2YJu6UJuNOmuhfvG3ui4WO9PHy3btc2q+3kutSuBcyIjhi+qbXasBcX/QOOJF
udyTZc5PopNkJojS4JdXAZCiu5sKI3lp4DIt9qNISlXGgrJgdxGUO+DzarBctXdn
BSwXFw5hcjJjl7wsPGQl1tBTQwKBgQDPuz5MEM5ZeUe9CT5sQDq/ld0u4aL5AHmx
aaA2gzDgd9l2R5wHX6wLzjoVWXOmeqaYzJopt2JN4iXrtbjWkyePgZeZMyWoyJ/v
clW9bi8HM9f9EpPr7czSj9sLUnsjd9cuTD+JuXK//jRGbRpw7r7nWtLHImjj6d2v
APZRq0v2OQKBgBcESG/OObSbubeGSlKVEqiIzem7ELNJeDLDVCl3XE8zvbILbj0Z
OA39EYhCKg5xjEFgeaNwTS0VGoZ2wIc3dv81sq4wpvvjl035CBFKU+DFBt0p7Vml
MwKQnxVV0B9agLHyWe8mnvf2LeZr72ffUvfRa8QelA4pRYvVDnV0OF+BAoGAW6rM
+tQPuvwB5DFIEozlX9XKHP4E5MyI5vktceDCmMtKcx92gup9CVif2Pv4ROaqzZK8
FNyPzL6W7UTrpASb2H/fXgNsAudFbGyP2V/d8Ne34D1qeRoe4GwKxRxIqoYftpZ/
E096i66pcsqCeINiSsWRbb6JesmgwbEzAScOBkECgYEA6O/Dibc9PaqRpaiE6Qut
S3W/Rr1Pd1jbN4rOVI2TFCgMJQmc6jOdq2fCntR9acsa8HPx+djOlXTUBPKBZ/Ae
p8umRdXVWcNMnwWVWHt7tsEuR/gYkxQ5xjXeS1VDPnEre9+EaevMBuVs8HdRsKQO
uzvNGeAFEfqwIqn7CFQ+ndU=
-----END PRIVATE KEY-----

View File

@@ -19,7 +19,7 @@
##
## You can read more about all available flags and other
## environment variables here:
## https://help.penpot.app/technical-guide/configuration/#advanced-configuration
## https://help.penpot.app/technical-guide/configuration/#penpot-configuration
#
# WARNING: if you're exposing Penpot to the internet, you should remove the flags
# 'disable-secure-session-cookies' and 'disable-email-verification'
@@ -37,6 +37,15 @@ x-body-size: &penpot-http-body-size
# Max multipart body size (350MiB)
PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600
## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems
## (eg http sessions, or invitations) are derived.
##
## We recommend to use a trully randomly generated
## 512 bits base64 encoded string here. You can generate one with:
##
## python3 -c "import secrets; print(secrets.token_urlsafe(64))"
x-secret-key: &penpot-secret-key
PENPOT_SECRET_KEY: change-this-insecure-key
networks:
penpot:
@@ -45,7 +54,6 @@ volumes:
penpot_postgres_v15:
penpot_assets:
# penpot_traefik:
# penpot_minio:
services:
## Traefik service declaration example. Consider using it if you are going to expose
@@ -120,20 +128,7 @@ services:
## Configuration envronment variables for the backend container.
environment:
<< : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size]
## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems
## (eg http sessions, or invitations) are derived.
##
## If you leave it commented, all created sessions and invitations will
## become invalid on container restart.
##
## If you going to uncomment this, we recommend to use a trully randomly generated
## 512 bits base64 encoded string here. You can generate one with:
##
## python3 -c "import secrets; print(secrets.token_urlsafe(64))"
# PENPOT_SECRET_KEY: my-insecure-key
<< : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key]
## The PREPL host. Mainly used for external programatic access to penpot backend
## (example: admin). By default it will listen on `localhost` but if you are going to use
@@ -159,13 +154,12 @@ services:
PENPOT_ASSETS_STORAGE_BACKEND: assets-fs
PENPOT_STORAGE_ASSETS_FS_DIRECTORY: /opt/data/assets
## Also can be configured to to use a S3 compatible storage
## service like MiniIO. Look below for minio service setup.
## Also can be configured to to use a S3 compatible storage.
# AWS_ACCESS_KEY_ID: <KEY_ID>
# AWS_SECRET_ACCESS_KEY: <ACCESS_KEY>
# PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
# PENPOT_STORAGE_ASSETS_S3_ENDPOINT: http://penpot-minio:9000
# PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <ENDPOINT>
# PENPOT_STORAGE_ASSETS_S3_BUCKET: <BUKET_NAME>
## Telemetry. When enabled, a periodical process will send anonymous data about this
@@ -202,6 +196,7 @@ services:
- penpot
environment:
<< : [*penpot-secret-key]
# Don't touch it; this uses an internal docker network to
# communicate with the frontend.
PENPOT_PUBLIC_URI: http://penpot-frontend:8080
@@ -265,22 +260,3 @@ services:
- "1080:1080"
networks:
- penpot
## Example configuration of MiniIO (S3 compatible object storage service); If you don't
## have preference, then just use filesystem, this is here just for the completeness.
# minio:
# image: "minio/minio:latest"
# command: minio server /mnt/data --console-address ":9001"
# restart: always
#
# volumes:
# - "penpot_minio:/mnt/data"
#
# environment:
# - MINIO_ROOT_USER=minioadmin
# - MINIO_ROOT_PASSWORD=minioadmin
#
# ports:
# - 9000:9000
# - 9001:9001

View File

@@ -120,7 +120,7 @@
</ul>
</div>
<div class="footer-bottom">
<div class="footer-text"><span>Kaleidos © 2024 | Made with LOVE and Open Source</span></div>
<div class="footer-text"><span>Kaleidos © 2025 | Made with LOVE and Open Source</span></div>
<div class="github-widget">
<a class="github-link" href="https://github.com/penpot/penpot" rel="noopener" target="_blank" aria-label="Star penpot/penpot on GitHub">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58.208 58.208" version="1.1">

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -314,7 +314,7 @@ If you're using the official <code class="language-bash">docker-compose.yml</cod
## Email configuration
By default, <code class="language-bash">smpt</code> flag is disabled, the email will be
By default, <code class="language-bash">smtp</code> flag is disabled, the email will be
printed to the console, which means that the emails will be shown in the stdout.
Note that if you plan to invite members to a team, it is recommended that you enable SMTP

View File

@@ -1,5 +1,6 @@
---
title: 3.07. Abstraction levels
desc: "Penpot Technical Guide: organize data and logic in clear abstraction layers—ADTs, file ops, event-sourced changes, business rules, and data events."
---
# Code organization in abstraction levels

View File

@@ -1,5 +1,6 @@
---
title: 3.06. Backend Guide
desc: "Penpot Technical Guide: Backend basics - REPL setup, loading fixtures, database migrations, and clj-kondo linting to speed development workflows."
---
# Backend guide #

View File

@@ -1,5 +1,6 @@
---
title: 1.2 Install with Elestio
desc: "Step-by-step guide to deploy a self-hosted Penpot on Elestio: 3-minute setup, managed DNS/SMTP/SSL/backups, Docker Compose config, updates & support."
---
# Install with Elestio

View File

@@ -10,19 +10,19 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
<li>
<a href="/user-guide/account-teams/your-account">
<h2>Your account →</h2>
<p>Ways to start with Penpot</p>
<p>Access your account settings and manage personal access tokens</p>
</a>
</li>
<li>
<a href="/user-guide/account-teams/teams">
<h2>Teams →</h2>
<p>Info of interest about Penpot</p>
<p>Create and manage your teams</p>
</a>
</li>
<li>
<a href="/user-guide/account-teams/comments/">
<h2>Comments →</h2>
<p>Info of interest about Penpot</p>
<p>Give and receive feedback right over your designs</p>
</a>
</li>
</ul>

View File

@@ -1,6 +1,7 @@
---
title: Design Tokens
order: 5
desc: Learn how to create, manage and apply Penpot Design Tokens using W3C DTCG format, with sets, themes, aliases, equations and JSON import/export.
---
<h1 id="design-tokens">Design Tokens</h1>

View File

@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
<li>
<a href="/user-guide/design-systems/assets">
<h2>Assets →</h2>
<p>Ways to start with Penpot</p>
<p>Store elements and styles to easily reuse them</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/libraries">
<h2>Libraries →</h2>
<p>Info of interest about Penpot</p>
<p>Organize and manage your stored elements with Libraries</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/components">
<h2>Components →</h2>
<p>Speed your design workflow</p>
<p>Speed your design workflow with reusable components</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/variants">
<h2>Variants →</h2>
<p>Info of interest about Penpot</p>
<p>Group components into a single, customizable one</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/design-tokens">
<h2>Design Tokens →</h2>
<p>Info of interest about Penpot</p>
<p>Synchronize visual elements across your designs</p>
</a>
</li>
</ul>

View File

@@ -5,7 +5,7 @@ desc: Use Penpot's libraries for reusable design elements! Learn to create, mana
---
<h1 id="libraries">Libraries</h1>
<p class="main-paragraph">Libraries may include components, graphics, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
<p class="main-paragraph">Libraries may include components, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
<h3 id="file-libraries">File libraries</h3>
<p>Each file has its own file library which is where the assets that belong to this file are stored.</p>

View File

@@ -107,6 +107,25 @@ desc: Streamline your design workflow with Penpot's Components guide! Learn to c
<li>Select the variant copy, press right-click, and select the menu option <strong>Restore variant</strong> (will show if the main component still exists).</li>
</ul>
<h3 id="component-variants-toggle">Toggle for boolean variants</h3>
<p>When a variant property represents a boolean state, Penpot can display it as a toggle instead of a dropdown. This offers a quicker and more visual way to switch between two opposite values when working with copies.</p>
<p>The toggle appears in place of the property values dropdown, <strong>only when a copy is selected</strong>.</p>
<figure>
<img src="/img/variants/07-variants-boolean.webp" alt="Boolean variant option" />
</figure>
<h4>Accepted value pairs</h4>
<p>For Penpot to recognize the property as a boolean and display the toggle, the property must be defined with exactly two opposing values. These can be any of the following pairs:</p>
<ul>
<li><code>true</code> / <code>false</code></li>
<li><code>on</code> / <code>off</code></li>
<li><code>yes</code> / <code>no</code></li>
</ul>
<p>The order of the values does not matter. Penpot automatically maps them to ON and OFF states:</p>
<ul>
<li><strong>ON state:</strong> <code>true</code>, <code>yes</code>, <code>on</code></li>
<li><strong>OFF state:</strong> <code>false</code>, <code>no</code>, <code>off</code></li>
</ul>
<h3 id="component-use-variants">Use variants</h3>
<p>Once you have created your variants, you can place a copy of a component with variants into your design and then switch between its different versions.</p>

View File

@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
<li>
<a href="/user-guide/designing/workspace-basics">
<h2>Workspace basics →</h2>
<p>Workspace basics</p>
<p>Get to know the Workspace, where designs are created</p>
</a>
</li>
<li>
<a href="/user-guide/designing/layers">
<h2>Layers →</h2>
<p>Info of interest about Penpot</p>
<p>Objects available in Penpot and how to get the most of them</p>
</a>
</li>
<li>
<a href="/user-guide/designing/color-stroke/">
<h2>Color & Strokes→</h2>
<p>Info of interest about Penpot</p>
<p>Styling options available for each layer</p>
</a>
</li>
<li>
<a href="/user-guide/designing/text-typo">
<h2>Text & Typography→</h2>
<p>Info of interest about Penpot</p>
<p>Styling text content & using custom fonts</p>
</a>
</li>
<li>
<a href="/user-guide/designing/flexible-layouts">
<h2>Flexible layouts →</h2>
<p>Info of interest about Penpot</p>
<p>Create designs that adapt automatically</p>
</a>
</li>
</ul>

View File

@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
<li>
<a href="/user-guide/export-import/export-import-files/">
<h2>Export/Import Penpot files →</h2>
<p>Ways to start with Penpot</p>
<p>How to export and import your Penpot files</p>
</a>
</li>
<li>
<a href="/user-guide/export-import/exporting-layers/">
<h2>Exporting layers →</h2>
<p>Exporting layers</p>
<p>How to export elements from your design into different file formats</p>
</a>
</li>
</ul>

View File

@@ -16,7 +16,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
<li>
<a href="/user-guide/first-steps/the-interface">
<h2>Interface tour →</h2>
<p>Info of interest about Penpot</p>
<p>Take a tour of Penpot's main areas</p>
</a>
</li>
<li>
@@ -28,7 +28,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
<li>
<a href="/user-guide/first-steps/info">
<h2>Tutorials & info →</h2>
<p>Info of interest about Penpot</p>
<p>Useful resources to better understand Penpot</p>
</a>
</li>
</ul>

View File

@@ -22,49 +22,49 @@ eleventyNavigation:
<li>
<a href="/user-guide/designing/layers/">
<h2>Layers</h2>
<p>Ways to start with Penpot</p>
<p>Objects available in Penpot and how to get the most of them</p>
</a>
</li>
<li>
<a href="/user-guide/designing/flexible-layouts/">
<h2>Flexible layouts</h2>
<p>Create designs that adapt automatically.</p>
<p>Create designs that adapt automatically</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/components/">
<h2>Components</h2>
<p>Ways to start with Penpot</p>
<p>Speed your design workflow with reusable components</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/variants/">
<h2>Variants</h2>
<p>Penpot's main areas and features</p>
<p>Group components into a single, customizable one</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/design-tokens/">
<h2>Design Tokens</h2>
<p>Penpot's main areas and features</p>
<p>Synchronize visual elements across your designs</p>
</a>
</li>
<li>
<a href="/user-guide/dev-tools/#inspect-design">
<h2>Inspect design</h2>
<p>Ways to start with Penpot</p>
<p>Get production-ready code</p>
</a>
</li>
<li>
<a href="/user-guide/prototyping-testing/prototyping/">
<h2>Prototyping</h2>
<p>Ways to start with Penpot</p>
<p>Build interactive prototypes to mimic your product behaviour</p>
</a>
</li>
<li>
<a href="/user-guide/design-systems/libraries/">
<h2>Libraries</h2>
<p>Ways to start with Penpot</p>
<p>Organize and manage your stored elements with Libraries</p>
</a>
</li>
</ul>

View File

@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
<li>
<a href="/user-guide/prototyping-testing/prototyping">
<h2>Prototyping →</h2>
<p>Ways to start with Penpot</p>
<p>Build interactive prototypes to mimic your product behaviour</p>
</a>
</li>
<li>
<a href="/user-guide/prototyping-testing/testing-view-mode">
<h2>Testing: View mode →</h2>
<p>Info of interest about Penpot</p>
<p>Test your designs and play the interactions</p>
</a>
</li>
</ul>

View File

@@ -21,6 +21,7 @@
"raw-body": "^3.0.1",
"source-map-support": "^0.5.21",
"svgo": "penpot/svgo#v3.1",
"undici": "^7.16.0",
"xml-js": "^1.6.11",
"xregexp": "^5.1.2"
},

View File

@@ -18,4 +18,15 @@ cp ../.yarnrc.yml target/;
cp yarn.lock target/;
cp package.json target/;
cat <<EOF | tee target/setup
#/usr/bin/env bash
set -e;
corepack enable;
corepack install;
yarn install
yarn run playwright install chromium;
EOF
chmod +x target/setup;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/app.js;

6
exporter/scripts/run Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/../../backend/scripts/_env;
exec node target/app.js

View File

@@ -1,6 +1,10 @@
#!/usr/bin/env bash
SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/../../backend/scripts/_env;
bb -i '(babashka.wait/wait-for-port "localhost" 9630)';
bb -i '(babashka.wait/wait-for-path "target/app.js")';
sleep 2;
node target/app.js
exec node target/app.js

View File

@@ -100,7 +100,7 @@
(def browser-pool-factory
(letfn [(create []
(p/let [opts #js {:args #js ["--font-render-hinting=none"]}
(p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
browser (.launch pw/chromium opts)
id (swap! pool-browser-id inc)]
(l/info :origin "factory" :action "create" :browser-id id)

View File

@@ -7,7 +7,9 @@
(ns app.config
(:refer-clojure :exclude [get])
(:require
["process" :as process]
["node:buffer" :as buffer]
["node:crypto" :as crypto]
["node:process" :as process]
[app.common.data :as d]
[app.common.flags :as flags]
[app.common.schema :as sm]
@@ -21,13 +23,14 @@
:host "localhost"
:http-server-port 6061
:http-server-host "0.0.0.0"
:tempdir "/tmp/penpot-exporter"
:tempdir "/tmp/penpot"
:redis-uri "redis://redis/0"})
(def ^:private
schema:config
(def ^:private schema:config
[:map {:title "config"}
[:secret-key :string]
[:public-uri {:optional true} ::sm/uri]
[:management-api-key {:optional true} :string]
[:host {:optional true} :string]
[:tenant {:optional true} :string]
[:flags {:optional true} [::sm/set :keyword]]
@@ -93,3 +96,10 @@
(c/get config key))
([key default]
(c/get config key default)))
(def management-key
(or (c/get config :management-api-key)
(let [secret-key (c/get config :secret-key)
derived-key (crypto/hkdfSync "blake2b512" secret-key, "management" "" 32)]
(-> (.from buffer/Buffer derived-key)
(.toString "base64url")))))

Some files were not shown because too many files have changed in this diff Show More