Compare commits

..

1311 Commits

Author SHA1 Message Date
Alejandro Alonso
85f2804af8 📎 Tag new minor release 2022-06-10 08:24:13 +02:00
Alejandro Alonso
97f5f54d1c 📎 Tag new minor release 2022-06-09 13:44:22 +02:00
Alejandro
e6b80bf73e Merge pull request #1988 from penpot/superalex-fix-exporter-to-frontend-communication
🐛 fix exporter to frontend communication
2022-06-09 13:43:49 +02:00
Alejandro Alonso
bc3914e7e0 🐛 Fix exporter to frontend communication 2022-06-09 13:39:59 +02:00
Alejandro Alonso
28b6175943 📎 Tag new minor release 2022-06-09 08:47:55 +02:00
Alejandro Alonso
ba85dcf1a3 🐛 Fix orientation artboard preset does not work with differently sized artboards 2022-06-08 13:11:59 +02:00
Alejandro Alonso
c3486c566a 🐛 Fix exporter to frontend communication 2022-06-08 13:09:16 +02:00
Andrés Moya
71501d966c 🐛 Fix resize parents when there are nested groups 2022-06-07 14:36:26 +02:00
alonso.torres
1764d965c1 📚 Upgrade to version 1.13.4-beta 2022-06-06 15:22:23 +02:00
Alejandro Alonso
a120630a7f 🐛 Fix environment import for exporter at docker 2022-06-06 13:23:40 +02:00
Alejandro
f33ad5e8fa Merge pull request #1972 from penpot/hiru-fix-orphaned-shapes
 Add script to fix broken objects
2022-06-06 13:18:07 +02:00
Andrés Moya
f04859f8a6 Add script to fix broken objects 2022-06-06 12:56:37 +02:00
Alejandro Alonso
31aed2aaa4 🐛 Fix base background not visible for imported svg 2022-06-06 12:34:19 +02:00
Alejandro
18109b2387 Merge pull request #1976 from penpot/hiru-fix-scrollintoview
🐛 Fix auto scroll layers panel in firefox
2022-06-06 11:05:35 +02:00
Andrés Moya
a0cc8a06b6 🐛 Fix auto scroll layers panel in firefox 2022-06-06 10:21:32 +02:00
Pablo Alba
462ec0c12a Merge pull request #1973 from penpot/alotor-more-hotfixes
Hotfixes
2022-06-03 10:59:26 +02:00
Alejandro Alonso
2b61b1768f 🐛 Fix exporter white list domains configuration 2022-06-03 07:43:19 +02:00
alonso.torres
424630a67f 📚 Update changelog 2022-06-02 22:53:50 +02:00
alonso.torres
14b1970a8a 🐛 Fix concurrent thumbnail modification 2022-06-02 22:37:33 +02:00
alonso.torres
541168aee4 🐛 Fix problem with some data and text input 2022-06-02 22:35:59 +02:00
alonso.torres
6e9a77edcd 🐛 Fix undo for drawing curves 2022-06-02 22:31:27 +02:00
Pablo Alba
8d1cd2f56d 🐛 Empty groups were not deleted 2022-06-02 16:53:01 +02:00
alonso.torres
0b149dd302 ⬆️ Update to 1.13.3-beta 2022-06-01 10:41:08 +02:00
Pablo Alba
662fc073df Merge pull request #1966 from penpot/alotor-fix-font-loading
🐛 Fix problem with font loading
2022-06-01 10:37:30 +02:00
alonso.torres
46be4ca6d1 🐛 Fix problem with font loading 2022-06-01 09:38:27 +02:00
Alejandro Alonso
c356ae6de8 🐛 Fix github auth without name 2022-05-31 10:26:13 +02:00
Pablo Alba
c5e872b81d 🐛 Remove default font on team change 2022-05-30 14:19:34 +02:00
Alejandro Alonso
0307e58fbe 🐛 Fix old texts with opacity and no fill 2022-05-30 12:40:07 +02:00
Alejandro
5c14c3fafc Merge pull request #1960 from penpot/alotor-fixes
🐛 Fix thumbnails. Add safety text position
2022-05-30 12:21:40 +02:00
alonso.torres
321c3fb34b 🐛 Fix problem with missplaced texts 2022-05-30 12:09:04 +02:00
alonso.torres
4764674374 🐛 Fix thumbnails. Add safety text position 2022-05-30 12:09:04 +02:00
Pablo Alba
0416988913 Set invitations expiration to 7 days 2022-05-30 10:41:23 +02:00
Andrés Moya
ec884787f1 🔧 Fix docker dependencies 2022-05-27 13:57:33 +02:00
alonso.torres
c08ad5c8c0 ⬆️ Update version 1.13.2-beta 2022-05-27 10:29:39 +02:00
alonso.torres
2ce766c49e 🐛 Fix performance issue with focus mode 2022-05-26 17:55:19 +02:00
Alejandro
bb18a69394 Merge pull request #1958 from penpot/alotor-improved-thumbnail-generation
 Improved frame generation performance
2022-05-26 16:51:13 +02:00
alonso.torres
96ed66d86e Improved frame generation performance 2022-05-26 16:33:16 +02:00
Eva
57ccb18517 💄 remove console 2022-05-26 13:58:00 +02:00
Andrés Moya
d5df465992 🌐 Add Norwegian, Persian and Chinese (Traditional) 2022-05-26 12:48:36 +02:00
Alejandro Alonso
ea6c34f6b2 🐛 Fix github auth without public email 2022-05-26 11:16:09 +02:00
Andrés Moya
36390be72a 🌐 Add new Polish language 2022-05-26 11:10:16 +02:00
alonso.torres
3c41693787 :docs: Update changelog 2022-05-25 21:45:21 +02:00
alonso.torres
b25806b172 🐛 Fix problem with auto-width initial text 2022-05-25 21:43:50 +02:00
Alejandro
0828d43f8f Merge pull request #1954 from penpot/alotor-fix-cache-embeds
🐛 Fix problems with embed data cache
2022-05-25 18:16:15 +02:00
alonso.torres
402212c808 🐛 Fix problems with embed data cache 2022-05-25 18:00:23 +02:00
Andrés Moya
11b2144274 🌐 Add translations for: Spanish.
Currently translated at 100.0% (1110 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2022-05-25 17:20:51 +02:00
Eva Marco
216dbc8e0d Merge pull request #1950 from penpot/palba-invitations-validation
Palba invitations validation
2022-05-25 17:01:21 +02:00
Eva Marco
67b81fbe67 Merge pull request #1949 from penpot/palba-fix-importing-old-penpot-files-frames
Palba fix importing old penpot files frames
2022-05-25 16:50:29 +02:00
Hosted Weblate
fcafe66bd8 🌐 Cherry-pick texts from Weblate for main branch 2022-05-25 16:07:05 +02:00
Pablo Alba
931759f468 🎉 Activate invitations validation 2022-05-25 12:03:05 +02:00
Pablo Alba
f33360a22b 🐛 Importing old penpot files generates frames with 0.01 as width and height
https://tree.taiga.io/project/penpot/issue/3455
2022-05-25 11:58:46 +02:00
Alejandro
910fb55b69 Merge pull request #1948 from penpot/fix-issue-hang-file
🐛 Fix problem with hanging file
2022-05-25 11:33:34 +02:00
alonso.torres
18849307e9 🐛 Fix linting issue 2022-05-25 11:29:49 +02:00
alonso.torres
0f2b2d4590 🐛 Fix problem with hanging file 2022-05-25 11:24:07 +02:00
Andrés Moya
ef37abcbbd 🐛 Allow debug load file with random uuid 2022-05-25 09:52:32 +02:00
alonso.torres
02427285ef 📚 Update changelog 2022-05-25 09:25:51 +02:00
Alejandro
38bc3b061a Merge pull request #1940 from penpot/repair-idless-components
🔧 Add a function to manually repair components without :id
2022-05-25 09:04:19 +02:00
Alejandro
047b3f0987 Merge pull request #1944 from penpot/hiru-dbg-update-file
Hiru dbg update file
2022-05-25 09:04:05 +02:00
Alejandro
6a8f3c7283 Merge pull request #1947 from penpot/alotor-hotfix-1.13
Alotor hotfix 1.13
2022-05-25 08:18:00 +02:00
alonso.torres
525da266b8 🐛 Creates a migration to invalidate texts-position-data 2022-05-24 23:34:23 +02:00
alonso.torres
97c9035cfd 🐛 Fix problems with font initialization 2022-05-24 23:34:23 +02:00
alonso.torres
35681c3af8 🐛 Fix problem with multiple users and texts positions 2022-05-24 23:34:23 +02:00
alonso.torres
8a6f01404c 🐛 Fix hide artboard 2022-05-24 23:34:23 +02:00
alonso.torres
6901431f8a Add debugging tool 2022-05-24 23:34:23 +02:00
Alejandro Alonso
2261bde6f1 🐛 Fix fills priority over imported svg attributes 2022-05-24 14:17:23 +02:00
elhombretecla
40e1d5a2a1 tada: Remove discussions and add twitter to form 2022-05-24 13:58:08 +02:00
Andrés Moya
d52c4541ae 🔧 Preserve id when downloading file with dbg 2022-05-24 13:34:42 +02:00
Andrés Moya
b0c3b38cc5 🔧 Add a function to manually repair components without :id 2022-05-24 12:26:21 +02:00
Eva Marco
494e2df49f Merge pull request #1937 from penpot/superalex-fix-add-stroke-for-text-with-shortcut
🐛 Fix adding string for texts with shortcut
2022-05-24 11:01:32 +02:00
Alejandro Alonso
dcac6d9ea4 🐛 Fix adding string for texts with shortcut 2022-05-24 07:17:15 +02:00
Alejandro
f5128d8d43 Merge pull request #1932 from penpot/fix-div-by-zero
🐛 Fix problem with division by zero
2022-05-23 13:52:17 +02:00
alonso.torres
4c2182dd0b 🐛 Fix problem with division by zero 2022-05-23 13:46:36 +02:00
Alejandro
c83affe351 Merge pull request #1931 from penpot/alotor-bugfix-initialize-texts
🐛 Fix problem when initializing texts
2022-05-23 12:15:21 +02:00
alonso.torres
51a9b10d51 🐛 Fix problem when initializing texts 2022-05-23 12:00:46 +02:00
alonso.torres
0fc2c312d5 🐛 Fix problem with fonts and thumbnails 2022-05-23 10:26:07 +02:00
Pablo Alba
ba139d7d2c 🐛 Fix unathorized redirect 2022-05-20 12:37:57 +02:00
Alejandro
426758d9b2 Merge pull request #1924 from penpot/fix-sync
🐛 Fix some component shapes not synced
2022-05-20 10:39:22 +02:00
alonso.torres
542fb9c754 🐛 Fix problem with nested constraints and text 2022-05-20 10:26:41 +02:00
Andrés Moya
13492f5ac7 🐛 Fix orphaned component instances 2022-05-20 09:17:55 +02:00
Andrés Moya
43d3b06c30 🐛 Fix some component shapes not synced 2022-05-19 17:52:31 +02:00
alonso.torres
d8a7402046 🐛 Fix problems with text position data 2022-05-19 16:33:43 +02:00
alonso.torres
93b582c385 🐛 Fix problem with small with texts 2022-05-19 15:02:50 +02:00
alonso.torres
d45bb0ace1 🐛 Fix dirty text modifiers when changing pages 2022-05-19 15:02:50 +02:00
alonso.torres
25ff15c62e 🐛 Fix rendering thumbnail with pending images/fonts 2022-05-19 15:02:50 +02:00
Andrés Moya
30bcdda90e 🐛 Add a protection for some corner cases 2022-05-19 09:49:42 +02:00
alonso.torres
e22ef536ed 🐛 Fix problem with Safari and text resize 2022-05-18 22:27:21 +02:00
Eva
b5e696c6b4 🐛 Fix ungroup typographies on edit 2022-05-18 17:23:26 +02:00
alonso.torres
2b1e126ff8 🐛 Fix problem with thumbnails 2022-05-18 17:04:59 +02:00
Alejandro
1690f1ee23 Merge pull request #1919 from penpot/alotor-buf-export
🐛 Fix problem when exporting penpot files
2022-05-18 15:59:21 +02:00
alonso.torres
6a74f29f96 🐛 Fix problem when exporting penpot files 2022-05-18 15:52:45 +02:00
Andrés Moya
d666755159 🐛 Synchronize text positions in components 2022-05-18 13:45:03 +02:00
Alejandro
fa00d674eb Merge pull request #1914 from penpot/alotor-performance-improvements
Performance improvements
2022-05-18 11:15:40 +02:00
Pablo Alba
7c23b7ea79 Merge pull request #1916 from penpot/superalex-fix-undo-drawing-curve
🐛 Fix undo when drawing curve
2022-05-18 10:57:07 +02:00
Alejandro Alonso
919ca68a77 🐛 Fix undo when drawing curve 2022-05-18 10:49:55 +02:00
Pablo Alba
29010453e6 Merge pull request #1913 from penpot/eva-fix-scroll-into-view
🐛 Fix scroll into view with big groups
2022-05-17 19:44:40 +02:00
alonso.torres
a8cc9ace72 Improved text move performance 2022-05-17 17:02:45 +02:00
alonso.torres
9ab922a0fa Improved snap-pixel performance 2022-05-17 17:02:28 +02:00
alonso.torres
c9dadce12a Skip calculate children modifiers on move 2022-05-17 17:02:11 +02:00
Eva
eabfa7a541 🐛 Fix scroll into view with big groups 2022-05-17 16:38:24 +02:00
Andrés Moya
95a2da5ebc Rework multi edit of measures values 2022-05-17 14:42:16 +02:00
Pablo Alba
180c355340 Merge pull request #1911 from penpot/alotor-fix-texts
Fix problems with texts and thumbnails
2022-05-17 14:26:24 +02:00
alonso.torres
01664a04fc 🐛 Problem recalculating thumbnails 2022-05-17 14:09:03 +02:00
alonso.torres
edce45095e 🐛 Remove xlinkHref to resolve Safari problem 2022-05-17 14:09:03 +02:00
alonso.torres
5a07599fc7 🐛 Fix problem with thumbnail re-rendering 2022-05-17 14:09:03 +02:00
alonso.torres
d684970bfb 🐛 Fix problem with single line texts 2022-05-17 14:09:03 +02:00
Alejandro Alonso
216b510900 🐛 Fix security concern 2022-05-17 13:03:04 +02:00
Alejandro
5c2b5f7cda Merge pull request #1909 from penpot/eva-fix-typo
🐛 Fix typo
2022-05-17 12:57:25 +02:00
Eva
712c68fc77 🐛 Fix typo 2022-05-17 12:43:44 +02:00
Alejandro
f290465edd Merge pull request #1908 from penpot/eva-no-rotation-in-paths
🐛 Fix rotation value when path is not rotated
2022-05-17 12:09:53 +02:00
Eva
141bcdd25e 🐛 Fix rotation value when path is not rotated 2022-05-17 11:59:48 +02:00
Pablo Alba
f68a4eb84a Merge pull request #1907 from penpot/eva-fix-layers-when-group
🐛 Fix change layer position on group or component creation
2022-05-17 10:48:08 +02:00
Eva
a240fbdf5b 🐛 Fix change layer position on group or component creation 2022-05-17 10:29:19 +02:00
Alejandro Alonso
799bb87398 🐛 Fix security concern 2022-05-17 10:25:13 +02:00
Alejandro
2b5282025c Merge pull request #1904 from penpot/alotor-fix-text-problems
Fix text issues
2022-05-17 06:41:39 +02:00
alonso.torres
a2de5f8fb4 🐛 Fix center alignment with new lines 2022-05-13 16:17:05 +02:00
alonso.torres
080139cd56 🐛 Improved performance for text resize 2022-05-13 16:17:05 +02:00
alonso.torres
570f038062 🐛 Disable stroke style for texts 2022-05-13 16:17:05 +02:00
alonso.torres
ae84f3cbe8 🐛 Fix typo in debug option 2022-05-13 16:17:05 +02:00
alonso.torres
abdc9b2cbd 🐛 Fix problem with center vertical align and auto-height 2022-05-13 16:17:05 +02:00
Pablo Alba
92d7521ec7 Merge pull request #1898 from penpot/superalex-fix-paste-svg-with-empty-space
🐛 Fix paste svg with empty space
2022-05-13 16:16:16 +02:00
alonso.torres
4730273ad3 🐛 Rollback thumbnail problem 2022-05-13 13:32:22 +02:00
Alejandro
a3935953f7 Merge pull request #1902 from penpot/palba-fix-artboards-thumbnail-another-page
🐛 Fix artboards thumbnail in another page
2022-05-13 13:17:46 +02:00
alonso.torres
ea50622bf7 🐛 Fine tune thumbnails 2022-05-13 13:16:58 +02:00
Alejandro
4b0b7463c7 Merge pull request #1903 from penpot/eva-bugfix-handoff
🐛 Show strokes and fills for texts when in handoff
2022-05-13 13:11:23 +02:00
Alejandro Alonso
95d4018074 🐛 Fix paste svg with empty space 2022-05-13 13:05:49 +02:00
Eva
3f413e4920 🐛 Show strokes and fills in text when in handoff 2022-05-13 12:44:11 +02:00
Alejandro Alonso
db8e829339 🐛 Fix remove time debug info 2022-05-13 12:00:18 +02:00
Pablo Alba
448e0dd415 🐛 Fix artboards thumbnail in another page 2022-05-13 11:29:46 +02:00
Alejandro
15418a252e Merge pull request #1893 from penpot/superalex-fix-thumbnail-blur
🐛 Fix Thumbnail blur on mouse movements
2022-05-13 09:18:30 +02:00
Alejandro
21d845d254 Merge pull request #1896 from penpot/superalex-multiple-fills-with-texts-are-not-working-properly
🐛 Fix multiple fills with texts are not working properly
2022-05-13 09:17:47 +02:00
Alejandro Alonso
c84017eb72 🐛 Fix multiple fills with texts are not working properly 2022-05-13 07:58:02 +02:00
Alejandro
431e42c80a Merge pull request #1895 from penpot/release-1.13
💄 Release 1.13 onboarding texts
2022-05-13 06:46:49 +02:00
elhombretecla
ca2eb1ac12 💄 Add new onboarding texts 2022-05-13 06:42:22 +02:00
alonso.torres
d2983c1110 🐛 Improve active frame behaviour for thumbnails 2022-05-13 06:20:31 +02:00
Alejandro Alonso
74612178d7 🐛 Fix Thumbnail blur on mouse movements 2022-05-13 06:20:31 +02:00
Eva Marco
af519b3f89 Merge pull request #1892 from penpot/alotor-bugfixing-2
Change text disposition on resize
2022-05-12 16:52:27 +02:00
alonso.torres
d8d4ce7a46 🐛 Fix linter 2022-05-12 16:32:25 +02:00
alonso.torres
3930be5d9e 🐛 Remove warnings from external library 2022-05-12 16:23:45 +02:00
alonso.torres
d85a4d6539 🐛 Minor improvements on refs 2022-05-12 16:23:45 +02:00
alonso.torres
7446fe77b3 🐛 Change text disposition on resize 2022-05-12 16:23:45 +02:00
alonso.torres
8b1f8d1418 🐛 Fix error in view mode 2022-05-12 15:18:23 +02:00
Pablo Alba
d387ca81d8 Merge pull request #1894 from penpot/superalex-fix-scrollbars-not-shown
Fix Scrollbars not shown
2022-05-12 14:25:51 +02:00
Alejandro Alonso
b7b5f3b4c2 Fix Scrollbars not shown 2022-05-12 14:18:26 +02:00
Eva Marco
698dd872e4 Merge pull request #1886 from penpot/superalex-multiple-fills-with-texts-are-not-working-properly
🐛 Fix multiple fills with texts are not working properly
2022-05-12 09:43:21 +02:00
Alejandro Alonso
767f0fe16b 🐛 Fix multiple fills with texts are not working properly 2022-05-12 09:30:37 +02:00
Alejandro
a19c56c0ce Merge pull request #1885 from penpot/eva-bugfix
🐛 Avoid scroll behind fixed element in layers
2022-05-12 09:05:04 +02:00
Eva
b9e984300c 🐛 Avoid scroll behind fixed element in layers 2022-05-12 08:43:53 +02:00
Alejandro
0727757eb1 Merge pull request #1884 from penpot/superalex-fix-import-svg-shapes-without-fill
🐛 Fix import svg shapes without fill
2022-05-12 06:57:05 +02:00
Eva Marco
50037a6a88 Merge pull request #1890 from penpot/alotor-bugfixing-2
🐛 Fix problem with RTL texts
2022-05-11 17:08:51 +02:00
Eva Marco
5bdea086e9 Merge pull request #1889 from penpot/palba-canceled-invitation-page
🎉 Show an error page when the user uses a cancelled/invalid/expired invitation
2022-05-11 16:39:39 +02:00
alonso.torres
fef69cb707 🐛 Fix problem with RTL texts 2022-05-11 15:53:50 +02:00
Eva Marco
20211101b7 Merge pull request #1888 from penpot/alotor-bugfixing-2
Fix problem with frame resize
2022-05-11 14:23:58 +02:00
Pablo Alba
ce41a38098 🎉 Show an error page when the user uses a cancelled/invalid/expired invitation 2022-05-11 13:46:43 +02:00
alonso.torres
c14ece9f8d 🐛 Fix problems with thumbnails 2022-05-11 13:44:47 +02:00
Alejandro Alonso
f2bb59fd77 🐛 Fix Paths have a black fill while being drawn 2022-05-11 13:11:55 +02:00
alonso.torres
af6a687187 🐛 Fix performance problem with import SVG 2022-05-11 11:29:32 +02:00
alonso.torres
40de8781ef 🐛 Improved zoom responsiveness 2022-05-11 11:29:32 +02:00
alonso.torres
33e776fefe 🐛 Fix path handler radius 2022-05-11 11:29:32 +02:00
Alejandro Alonso
efcabe7ffb 🐛 Fix import svg shapes without fill 2022-05-11 11:04:04 +02:00
Pablo Alba
77e9b8aa70 Merge pull request #1873 from penpot/superalex-import-svg-with-exterior-strokes
🐛  Import svg with exterior strokes
2022-05-11 09:23:40 +02:00
Alejandro
238cd14eb8 Merge pull request #1881 from penpot/hirunatan-fix-pdf-page-size
🐛 Fix page size at pdf export
2022-05-10 17:38:27 +02:00
Eva Marco
22193635d6 Merge pull request #1880 from penpot/palba-no-copy-use-for-thumbnail-on-duplicate
🐛 Do not copy the atribute use-for-thumbnail on frame duplicate
2022-05-10 16:04:40 +02:00
Andrés Moya
8432e970cb 🐛 Fix page size at pdf export
https://tree.taiga.io/project/penpot/issue/3371
2022-05-10 15:54:01 +02:00
Alejandro Alonso
55df28d5dc 🐛 Fix change username if not subscribed to newsletter 2022-05-10 15:12:17 +02:00
Eva Marco
33882f44ef Merge pull request #1875 from penpot/alotor-bugfixing-2
Bugfixes
2022-05-10 14:23:39 +02:00
Pablo Alba
c06042c91b 🐛 Do not copy the atribute use-for-thumbnail on frame duplicate
https://tree.taiga.io/project/penpot/issue/3362
2022-05-10 13:26:19 +02:00
alonso.torres
2976c5c572 🐛 Fix problem with flipped texts 2022-05-10 11:58:44 +02:00
alonso.torres
8df93c2707 🐛 Fix problem when exporting single text 2022-05-10 11:58:21 +02:00
Eva
0c26dad3b2 🐛 Show selrect in paths 2022-05-10 10:51:47 +02:00
Alejandro Alonso
8d399cb562 🐛 Fix import svg shapes without fill 2022-05-10 10:49:50 +02:00
alonso.torres
82d744b94a 🐛 Fix problem with scrolling on already visible layers 2022-05-09 17:50:34 +02:00
alonso.torres
94d3f66ef1 🐛 Fix problem with rotated shapes and auto-width/auto-height 2022-05-09 17:37:37 +02:00
alonso.torres
40a38cbd38 🐛 Fix problem when pasting frame and selected shape 2022-05-09 17:01:42 +02:00
alonso.torres
644c796772 🐛 Fix problem with path edition 2022-05-09 16:46:52 +02:00
alonso.torres
81dac233a7 🐛 Fix problem with text edition selection area 2022-05-09 16:46:52 +02:00
alonso.torres
6bbd76f350 🐛 Fix problem with text shapes in components 2022-05-09 16:46:52 +02:00
alonso.torres
3a6072bc8f 🐛 Fix problem with RTL 2022-05-09 16:46:52 +02:00
Alejandro
0bcf3d99a0 Merge pull request #1872 from penpot/alotor-fix-thumbnail-problem
Fix thumbnails problem
2022-05-09 15:44:05 +02:00
alonso.torres
8cd7f61150 🐛 Fix problem with duplicated ids for thumbnails 2022-05-09 15:37:47 +02:00
Alejandro Alonso
96aa756eb6 🐛 Fix import svg with exterior strokes 2022-05-09 12:46:52 +02:00
Eva Marco
4cdf8cec4e Merge pull request #1866 from penpot/palba-add-icon-to-artboard-thumbnail
Palba add icon to artboard thumbnail
2022-05-09 09:21:27 +02:00
Pablo Alba
d9a9eb3729 Add icon to artboard thumbnail 2022-05-06 19:12:05 +02:00
Eva Marco
8298d460e6 Merge pull request #1865 from penpot/alotor-bugfixing
Alotor bugfixing
2022-05-06 14:10:12 +02:00
Eva
462eabd8a1 🐛 Show '--' when multiple rotations 2022-05-06 13:31:24 +02:00
Eva
afa1af6dc2 🐛 Fix comments in viewer mode 2022-05-06 13:31:24 +02:00
Eva
37fdf51eaf 🐛 Fix copying layout values with only multiple decimals 2022-05-06 13:31:24 +02:00
Eva
1102bc9cba 🐛 Activate button when input change in account 2022-05-06 13:31:24 +02:00
Eva
18afb701fb 🐛 Fix apply color to groups from assets panel 2022-05-06 13:31:24 +02:00
Eva Marco
15a26d10f0 Merge pull request #1867 from penpot/hirunatan-bugfixing
Hirunatan bugfixing
2022-05-06 13:09:44 +02:00
Andrés Moya
9b8b6134c5 🐛 Allow images to adjust to the shape size
https://tree.taiga.io/project/penpot/issue/3329
2022-05-06 12:07:19 +02:00
Andrés Moya
7e05b7e6d9 🐛 Fix group typographies
https://tree.taiga.io/project/penpot/issue/3338
2022-05-06 10:56:20 +02:00
Andrés Moya
b86ea5b5e2 🐛 Fix notifications of external library changes
https://tree.taiga.io/project/penpot/issue/3348
2022-05-06 10:56:20 +02:00
alonso.torres
66f7d35510 🐛 Fix problem with multi-line text and strokes 2022-05-05 17:21:28 +02:00
Andrés Moya
8fb22b8eee 🐛 Add a protection for some possible race condition 2022-05-05 17:16:27 +02:00
alonso.torres
5b37c11221 🐛 Fix letter spacing for svg texts 2022-05-05 17:16:05 +02:00
alonso.torres
1723ff1da5 🐛 Numeric input for font size 2022-05-05 17:04:03 +02:00
alonso.torres
9099403421 🐛 Improved resilience for thumbnail generation 2022-05-05 16:46:21 +02:00
alonso.torres
baf3f7ea15 🐛 Fix problem with outerstrokes for frames 2022-05-05 14:24:14 +02:00
Pablo Alba
1d39bbaa3c 🐛 Do not show team-up modal for users already on a team 2022-05-05 14:08:51 +02:00
alonso.torres
0db2f87e3e 🐛 Fix problems with thumbnails generation 2022-05-05 13:11:03 +02:00
alonso.torres
430ccda02c 🐛 Fix problem with black frame background 2022-05-05 13:03:36 +02:00
Pablo Alba
fe6e62482a 🐛 Fix bad texts in layers filter pills 2022-05-05 09:25:51 +02:00
Pablo Alba
82185794a8 🐛 Fix shapes filter 2022-05-05 09:25:19 +02:00
Pablo Alba
053975ef82 Fix members menu popup is not correctly aligned 2022-05-05 09:24:34 +02:00
Pablo Alba
7185199d05 🐛 Fix feedback crash 2022-05-05 09:24:21 +02:00
Pablo Alba
9dcad7ebef 🐛 Round the size values on handoff to two decimals 2022-05-03 10:42:37 +02:00
alonso.torres
39e4651374 📚 Update changelog 2022-05-03 09:49:37 +02:00
Alejandro Alonso
fe1ae7dbb4 🐛 Fix import svg shapes without fill 2022-05-03 09:30:36 +02:00
alonso.torres
39b0de1ced 🐛 Fix thumbnails problem 2022-04-29 14:56:14 +02:00
Alejandro Alonso
2f0e85f619 🐛 Fix scroll bars 2022-04-29 14:55:05 +02:00
Alejandro
4d106d9e15 Merge pull request #1849 from penpot/alotor-bugfixing
Bugfixes
2022-04-29 10:46:02 +02:00
elhombretecla
e5ccf36c07 add new release info and images 2022-04-29 10:30:47 +02:00
alonso.torres
d92df31b3e 🐛 Fix problem with horizontal scroll 2022-04-28 16:51:27 +02:00
alonso.torres
8b3062be0b 🐛 Fix problem when resizing a group with texts with auto-width/height 2022-04-28 15:32:41 +02:00
alonso.torres
c7e23c1b58 🐛 Fix problem when export/importing guides attached to frame 2022-04-28 14:43:44 +02:00
alonso.torres
9923268589 🐛 Fix issue with paste ordering sometimes not being respected 2022-04-28 14:43:44 +02:00
alonso.torres
a8103cbc3e ⬆️ Update potok 2022-04-28 14:43:44 +02:00
alonso.torres
26a074768f 🐛 Fix path editing 2022-04-28 14:43:44 +02:00
alonso.torres
1c87195fa6 🐛 Fix error when drawing curves with only one point 2022-04-28 14:43:44 +02:00
alonso.torres
2a1ca07554 🐛 Fix problem when changing group size with decimal values 2022-04-28 14:43:44 +02:00
alonso.torres
c3be87ed30 🐛 Fix problem with thumbnail refresh 2022-04-28 14:27:23 +02:00
alonso.torres
609ce1c106 🐛 Fix poblems with SVG transformations 2022-04-27 14:37:53 +02:00
Andrey Antukh
5b2d1b310a Merge pull request #1845 from penpot/alotor-performance
Loading time improvement
2022-04-27 12:15:05 +02:00
Andrey Antukh
a7ded66eab Merge pull request #1846 from penpot/alotor-bugfixes
Fix focus mode problem
2022-04-27 11:59:28 +02:00
alonso.torres
74d195c745 🐛 Fix style issue with focus mode 2022-04-27 11:08:18 +02:00
alonso.torres
1705954b07 🐛 Fix problem with transforms 2022-04-27 09:17:35 +02:00
alonso.torres
71bb34efc5 Improved first load time 2022-04-27 09:17:35 +02:00
Alejandro
32d61eaf70 Merge pull request #1844 from penpot/superalex-fix-duplicate-artobard-without-guides
:bug Fix duplicate artboard without whithout guides
2022-04-27 06:42:02 +02:00
Alejandro Alonso
20badb7676 :bug Fix duplicate artboard without whithout guides 2022-04-26 17:37:10 +02:00
Andrey Antukh
dbfa0e7a4b 🐛 Fix unexpected exception on workspace libraries modal 2022-04-26 17:08:02 +02:00
Andrey Antukh
95c73585d2 Merge pull request #1843 from penpot/remove-backend-only-devenv
🔥 Remove backend-only devenv container
2022-04-26 17:01:06 +02:00
Andrés Moya
c4939c152d 🔥 Remove backend-only devenv container
(disable requirement of using cors and secure cookies in devenv)
2022-04-26 16:47:14 +02:00
Pablo Alba
7560e32911 Merge pull request #1840 from penpot/alotor-improved-filter-layers
 Improved filter layers
2022-04-26 16:16:00 +02:00
alonso.torres
d50299bdbb Improved performance for layers filtering 2022-04-26 16:15:34 +02:00
Andrey Antukh
c34c1c4375 📎 Update docker files 2022-04-26 13:28:05 +02:00
Alejandro Alonso
b62f387ff4 :bug Fix blend modes are ignored in component updates 2022-04-26 09:57:28 +02:00
Alejandro Alonso
d28b4092d9 🐛 Fix guides are not duplicated with the artboard 2022-04-25 17:43:39 +02:00
Pablo Alba
658e3b7aee 🐛 Fix mouse leave in handoff close overlay animation breaks 2022-04-25 17:20:24 +02:00
Eva Marco
d18c96360f Merge pull request #1836 from penpot/alotor-more-performance-changes
Alotor more performance changes
2022-04-25 15:32:14 +02:00
Alejandro
c83bb70074 Merge pull request #1834 from penpot/hirunatan-update-color-library
Synchronize library colors in all parts of a shape
2022-04-25 14:00:05 +02:00
Andrés Moya
02157cbeb9 🎉 Synchronize library colors in all parts of a shape 2022-04-25 12:18:51 +02:00
Andrés Moya
7581230b6e 🔧 Small refactor of sync helper 2022-04-25 12:18:51 +02:00
Andrey Antukh
049f4ce784 ♻️ Refactor persistence flow 2022-04-25 12:07:26 +02:00
Andrey Antukh
c01e4e52f8 ♻️ Reorganize workspace persistence related namespace 2022-04-25 12:07:26 +02:00
Andrey Antukh
3ab3ea68b4 📎 Change namespace alias naming on persistence ns 2022-04-25 12:07:26 +02:00
alonso.torres
41948ff86b 🐛 Changes after review 2022-04-25 11:41:05 +02:00
alonso.torres
01ca538c72 Debounce update indices event 2022-04-25 10:47:47 +02:00
alonso.torres
2b9badfd4e Debounce update position-data event 2022-04-25 10:47:47 +02:00
alonso.torres
6ad591eb23 🐛 Fix problem with export texts and fonts 2022-04-25 10:47:47 +02:00
alonso.torres
581c50b5ff Improved copy objects performance 2022-04-25 10:47:47 +02:00
Andrey Antukh
9492dd7856 Merge branch 'main' into staging 2022-04-22 14:40:41 +02:00
Andrey Antukh
b239a9b09e Merge pull request #1819 from penpot/alotor-performance-improvements
Frames performance improvements
2022-04-22 14:20:27 +02:00
Andrey Antukh
e0aeb3b5ac 📎 Reduce default chunk size of the audit log archive task 2022-04-22 12:08:29 +02:00
Andrey Antukh
58cfd61997 🐛 Don't send url on file-media-upload 2022-04-22 12:08:29 +02:00
alonso.torres
a82bcd0ab2 🐛 Fixes after review 2022-04-22 11:33:40 +02:00
alonso.torres
dfc9d0709d 🐛 Fix problems with masks 2022-04-22 11:09:59 +02:00
alonso.torres
b7d33041e8 Improved performand for text editing 2022-04-22 11:09:59 +02:00
alonso.torres
f945a6e649 Changed thumbnails to webp format 2022-04-22 11:09:59 +02:00
alonso.torres
6a3a460203 Advanced frame thumbnail handling 2022-04-22 11:09:59 +02:00
alonso.torres
b576ef02af Performance improvements 2022-04-22 11:09:58 +02:00
Alejandro Alonso
814042909a 🐛 Import svg with exterior stroke 2022-04-22 11:06:59 +02:00
Alejandro Alonso
9856da4a1f 🐛 Fix black background while drawing a path 2022-04-22 11:05:01 +02:00
Andrey Antukh
202e7eb3f2 Merge pull request #1823 from penpot/superalex-drop-shadow-not-working-on-fill-less-strokes
🐛 Fix drop shadow not working on fill-less strokes
2022-04-21 15:52:12 +02:00
Eva Marco
38deacdf31 Merge pull request #1826 from penpot/superalex-internal-error-when-hoverin-over-shape
🐛 Internal error when hoverin over shape
2022-04-21 13:31:37 +02:00
Alejandro Alonso
c809890cfd 🐛 Fix black background while drawing a path 2022-04-21 13:31:19 +02:00
Alejandro Alonso
224d466122 Fix internal error when hoverin over shape 2022-04-21 13:27:40 +02:00
Alejandro Alonso
08c6e9b702 🐛 Fix different behaviour during image drag 2022-04-21 12:13:12 +02:00
Andrey Antukh
9e940dc042 Improve dm/get-in macro to be fully compliant with core/get-in 2022-04-21 09:43:54 +02:00
Alejandro Alonso
6fda156164 🐛 Fix drop shadow not working on fill-less strokes 2022-04-21 07:16:48 +02:00
Andrey Antukh
5eb53da374 Merge pull request #1824 from penpot/alotor-fix-problem-with-texts
Fix problem with texts
2022-04-20 15:46:55 +02:00
alonso.torres
68e0b3e756 🐛 Fix problem with text and blank spaces 2022-04-20 14:16:51 +02:00
Alejandro Alonso
cfe374b08c 📎 Tag new minor release 2022-04-20 11:26:01 +02:00
alonso.torres
cc046555a3 🐛 Fix problem with zoom with wheel in Firefox 2022-04-20 10:40:07 +02:00
Andrey Antukh
31ec4092ed Improve logging performance
Delay the message building until it really needed to be
printed.
2022-04-20 10:03:04 +02:00
Andrey Antukh
d9d47b2c65 🐛 Fix missing key properties and react warnings 2022-04-20 10:03:04 +02:00
Andrey Antukh
506f63317a Merge pull request #1805 from penpot/hirunatan-set-html-theme
Hirunatan set html theme
2022-04-20 09:20:46 +02:00
Andrey Antukh
d658145450 Merge pull request #1813 from penpot/superalex-prototype-connection-handler-is-extremely-hard-to-use
🐛 Prototype connection handler is extremely hard to use
2022-04-20 09:19:35 +02:00
Andrey Antukh
b2d13f277a Merge pull request #1815 from penpot/superalex-bullet-colors-from-pasted-shapes-with-library-colors
🐛 Fix bullet colors from pasted shapes with library colors
2022-04-20 09:18:31 +02:00
Andrey Antukh
59310cdd71 Merge pull request #1822 from penpot/superalex-multiselected-elements-drag-problem-on-empty-areas
🐛 Multiselected elements drag problem on empty areas
2022-04-20 09:16:13 +02:00
Alejandro Alonso
c8d3975680 🐛 Fix multiselected elements drag problem on empty areas 2022-04-19 14:20:42 +02:00
alonso.torres
b6f2800aa3 🐛 Fix pinch to zoom on mac 2022-04-19 13:22:50 +02:00
alonso.torres
a579ea3c25 🐛 Fix pinch to zoom on mac 2022-04-19 13:21:45 +02:00
Andrey Antukh
7b3ab2287a 🎉 Backport pprint module to common 2022-04-19 12:08:47 +02:00
Andrey Antukh
b78d9dcc52 Merge pull request #1814 from penpot/alotor-backports
Backport 1.13.4
2022-04-19 08:52:29 +02:00
Andrey Antukh
caa81b4fe2 Merge pull request #1812 from penpot/release-1.12.4
Release 1.12.4
2022-04-19 08:52:15 +02:00
Alejandro Alonso
b9ab00c549 🐛 Fix bullet colors from pasted shapes with library colors 2022-04-19 07:33:55 +02:00
alonso.torres
2707903f8a 🐛 Fix start script in local environment 2022-04-18 19:04:24 +02:00
alonso.torres
28031a247a 🐛 Fix problem with ctrl+click context menu in mac 2022-04-18 19:03:25 +02:00
alonso.torres
175f4b57f5 🐛 Fix problem with ctrl+click context menu in mac 2022-04-18 16:41:35 +02:00
Andrey Antukh
2ae2877f45 Improve email console logging
And invitation console logging
2022-04-18 14:10:52 +02:00
Alejandro Alonso
5e7a609b3d 🐛 Fix prototype connection handler is extremely hard to use 2022-04-18 14:07:08 +02:00
alonso.torres
9ffe406d0d 🐛 Fix shift+2 shortcut in MacOS with non-english keyboards 2022-04-18 11:36:03 +02:00
alonso.torres
adfc0902a2 🐛 Fix problems with CTRL in MacOS 2022-04-18 11:36:03 +02:00
alonso.torres
620efcb5cb 🐛 Fix problem with copy/paste in Safari 2022-04-18 11:36:03 +02:00
alonso.torres
0ed23f94c7 🐛 Fix problems with trackpad zoom and scroll in MacOS 2022-04-18 11:36:03 +02:00
alonso.torres
1cac7d55d0 🐛 Fix crash on iOS when displaying viewer 2022-04-18 11:36:03 +02:00
alonso.torres
875fd78f73 🐛 Fix rounding problem with texts 2022-04-18 10:49:50 +02:00
Alejandro Alonso
82ae4e60f8 🐛 Texts with center align and fixed width are not shown 2022-04-11 15:28:09 +02:00
Alejandro Alonso
5fc27a7594 🐛 Blur not working 2022-04-11 14:03:55 +02:00
Andrés Moya
6ad06d9665 🎉 Show Penpot color in Safari tab bar 2022-04-11 12:51:24 +02:00
Alejandro Alonso
c766e08027 🐛 [LIBRARIES & TEMPLATES] Missing fills and texts 2022-04-11 12:45:37 +02:00
Andrey Antukh
62f55a47c5 ⬆️ Update okulary dependency 2022-04-11 01:05:06 +02:00
Eva Marco
b1edcba0c2 Merge pull request #1798 from penpot/palba-dashboard-import-file-name-hidden
Palba dashboard import file name hidden
2022-04-08 09:20:41 +02:00
Pablo Alba
f7d2f6ec51 🐛 Fix hidden file name on import 2022-04-08 09:13:43 +02:00
Andrey Antukh
3a95a1cea1 Merge pull request #1797 from penpot/palba-unnecessary-scrollbars-color-list
Palba unnecessary scrollbars color list
2022-04-08 00:12:35 +02:00
Andrey Antukh
4143573868 🐛 Fix okulary and tab component 2022-04-07 23:52:27 +02:00
Pablo Alba
26daf507b3 🐛 Fix unneccessary scrollbars at the color list 2022-04-07 22:15:28 +02:00
Eva
f2c0683803 Revert "🐛 Fix gap between contiguous shapes"
This reverts commit 39fa939f58.
2022-04-07 16:21:01 +02:00
Pablo Alba
aa2bb75f95 Merge pull request #1792 from penpot/niwinz-minor-enhancements
Enhancements
2022-04-07 10:10:40 +02:00
Pablo Alba
004fddfcf4 Merge pull request #1789 from penpot/superalex-show-in-exports-is-showing-in-multiselections
🐛 'Show in exports' is showing in multiselections
2022-04-06 13:58:21 +02:00
Andrés Moya
a61301c698 🐛 Fix call to exporter and exporter setup in devenv 2022-04-06 12:54:05 +02:00
Andrey Antukh
b2607b28ff 🎉 Add build date and changelog to the bundle 2022-04-06 11:20:48 +02:00
Andrey Antukh
c2c01831fb Merge pull request #1791 from penpot/alotor-bug-fixing
Bug fixes
2022-04-06 10:49:21 +02:00
alonso.torres
ea38d12a73 🐛 Fix problem with exported text 2022-04-06 10:08:35 +02:00
alonso.torres
76abd6796e 🐛 Fix import problems 2022-04-06 10:08:35 +02:00
alonso.torres
0bb20197f1 Improved performance of refs 2022-04-06 10:08:35 +02:00
Andrey Antukh
2af057a79f ⬆️ Update backend and docker dependencies 2022-04-06 09:54:40 +02:00
Andrey Antukh
fd9b442075 Improve email console logging
And invitation console logging
2022-04-06 09:40:20 +02:00
Alejandro Alonso
5edbebcfec 🐛 'Show in exports' is showing in multiselections 2022-04-06 09:37:12 +02:00
Andrey Antukh
e62f0603b5 Merge pull request #1788 from penpot/hirunatan-fix-multi-user
Hirunatan fix multi user
2022-04-06 09:20:27 +02:00
Andrés Moya
654e12a2c3 🐛 Fix multi user not working 2022-04-06 09:16:22 +02:00
Alejandro Alonso
5299465864 🐛 Setting in-progress to false when export fails 2022-04-06 08:28:57 +02:00
Eva
39fa939f58 🐛 Fix gap between contiguous shapes 2022-04-05 13:53:03 +02:00
Andrey Antukh
4adc5d25a7 📎 Fix review issues 2022-04-05 13:23:39 +02:00
Andrey Antukh
7a38b08506 🐛 Fix default configuration 2022-04-05 13:23:39 +02:00
Andrey Antukh
df4b92fb6b Improve logging ordering of message parts 2022-04-05 13:23:39 +02:00
Andrey Antukh
ca02999ae9 Improve error reporting 2022-04-05 13:23:39 +02:00
Andrey Antukh
701a98fab6 Improve backend and worker error handling 2022-04-05 13:23:39 +02:00
Andrey Antukh
c026d05bc3 Set consistent max body size
And make it configurable
2022-04-05 13:23:39 +02:00
Andrey Antukh
602b736163 📎 Update default scripts 2022-04-05 13:23:39 +02:00
Andrey Antukh
c5b1b67c50 📎 Add TODO comment on changes ns 2022-04-05 13:23:39 +02:00
Andrey Antukh
8eae892983 🔥 Remove old and already deprecated utils.data ns 2022-04-05 13:23:39 +02:00
Andrey Antukh
7d32d03156 💄 Add cosmetic changes on workspace/changes ns 2022-04-05 13:23:39 +02:00
Andrey Antukh
f9e83f2cc7 Improve implementation of without-keys helper 2022-04-05 13:23:39 +02:00
Andrey Antukh
20d3251a93 🎉 Add generic file object thumbnail abstraction
As replacement to the file frame thumbnail mechanism
2022-04-05 13:23:39 +02:00
Andrey Antukh
147f56749e ⬆️ Update some dependencies 2022-04-05 13:23:39 +02:00
Andrey Antukh
9140fc71b9 ♻️ Refactor exportation process, make it considerably faster 2022-04-05 13:23:39 +02:00
alonso.torres
d6abd2202c 🐛 Revert pixel grid color change 2022-04-05 13:04:44 +02:00
Alejandro Alonso
911d4edb9f 🐛 Import a file with image background won't show the background 2022-04-05 12:09:06 +02:00
Andrey Antukh
e9e5b07bdb Merge pull request #1782 from penpot/superalex-fix-edit-file-name-navigates-to-the-file-workspace
🐛 Fix edit file name navigates to the file workspace
2022-04-05 11:16:18 +02:00
Alejandro Alonso
cef1c0d1d1 🐛 Edit file name navigates to the file workspace 2022-04-05 11:15:51 +02:00
Andrey Antukh
0fb54a5edd Merge pull request #1777 from penpot/eva-fix_scroll_into_view
🐛 fix scroll into view behind fixed Element
2022-04-05 11:13:39 +02:00
Eva
abd7a88ba0 🐛 Fix scroll into view behing fixed element 2022-04-05 11:03:04 +02:00
Andrey Antukh
d37457dc10 Merge pull request #1783 from penpot/eva-fix-sidebar-icon-in-viewer
🐛 Fix sidebar icon in viewer mode
2022-04-05 10:56:46 +02:00
Eva
fc7707ad3e 🐛 Fix sidebar icon in viewer mode 2022-04-05 10:35:26 +02:00
Andrés Moya
f43c6ab3c5 🐛 Fix resize for rotated shapes with top&down constraints 2022-04-05 09:58:04 +02:00
Andrey Antukh
11c3b6cfe2 🐛 Fix issue with password persistence 2022-04-04 23:54:54 +02:00
Andrey Antukh
b4a997cde9 🐛 Fix issue with password persistence 2022-04-04 23:46:42 +02:00
Andrey Antukh
7105255212 Merge branch 'us/newsletter_subscription' into staging 2022-04-04 23:12:03 +02:00
Andrey Antukh
1338491616 Make the subscription modal configurable 2022-04-04 23:10:41 +02:00
Andrey Antukh
0afb47ade0 Update telemetry task for handle user subscriptions 2022-04-04 22:57:27 +02:00
Andrey Antukh
88292f2f3b Properly initialize options and profile forms 2022-04-04 22:57:27 +02:00
Andrey Antukh
d389dab8d2 Mark form as touched on changing the checkbox or radio buttons 2022-04-04 22:57:27 +02:00
Andrey Antukh
1205bdcaae Make the update-profile operation atomic with prop update 2022-04-04 22:57:27 +02:00
Eva
5e7e055539 🎉 Add newsletter subscription modal 2022-04-04 22:57:27 +02:00
Eva
3822be76a8 🐛 Fix send to back several shapes at a time 2022-04-04 17:44:50 +02:00
Eva Marco
b904237c5a Merge pull request #1773 from penpot/eva-fix_artboard_fills
🐛 Fix add fill to artboard modify children
2022-04-04 16:58:12 +02:00
Eva
df930cb879 🐛 Fix add fill to artboard modify children 2022-04-04 16:54:35 +02:00
Alejandro Alonso
327331475e 🐛 Hide the drop shadow also hides the shape 2022-04-04 16:39:17 +02:00
Eva
91a8386ba4 🐛 Fix duplicate multiselected elements 2022-04-04 16:24:50 +02:00
Andrés Moya
b7e0619e9a 🐛 Fix order of undo operations 2022-04-04 14:05:01 +02:00
Andrey Antukh
0b984a44d7 🐛 Fix default configuration 2022-04-04 10:54:40 +02:00
Alejandro
b2b221516c Merge pull request #1768 from penpot/alotor/bugfixes
Bugfixing
2022-04-01 11:06:59 +02:00
Andrés Moya
1bcb0128f0 🐛 Fix paste shapes while editing text 2022-03-31 14:35:33 +02:00
alonso.torres
5633291ab0 🐛 Fix problem when alt+drag duplicate frames 2022-03-31 12:44:56 +02:00
alonso.torres
785ae01a51 🐛 Fix problem rendering some SVG filters 2022-03-31 11:21:15 +02:00
alonso.torres
34fd9d0d88 🐛 Fix problem with fonts in viewer 2022-03-31 11:18:28 +02:00
alonso.torres
9f19676dc2 🐛 Fix problem with wheel-zoom on an editing text 2022-03-31 11:18:28 +02:00
alonso.torres
4a3fb55b30 🐛 Fix issue with drag-select shapes 2022-03-31 11:11:44 +02:00
alonso.torres
eaa6327663 🐛 Fix issue with drag-select shapes 2022-03-31 11:06:19 +02:00
Andrey Antukh
13ca506015 Improve migrate-data function (file data migrations)
This will enable the ability to apply some migration to a specific
file from the Server REPL.
2022-03-31 10:40:15 +02:00
Andrey Antukh
59d0bafdc9 📎 Add analyze-file helper to srepl.main namespace 2022-03-31 10:40:15 +02:00
Andrey Antukh
cee85942e6 📎 Set explicit clojure version on frontend and backend 2022-03-31 10:40:15 +02:00
Andrey Antukh
f303d3c45d 🐛 Fix wrong type hints 2022-03-31 10:40:15 +02:00
Andrey Antukh
6f7f74f7c6 🐛 Add migrations to fix wrongly migrated data
Also port the migration introduced in main branch
for the recent hotfix
2022-03-31 10:40:15 +02:00
Alejandro Alonso
ba398569c1 🐛 Fix shapes with no fill 2022-03-31 08:13:46 +02:00
Eva Marco
a8a47dca8f Merge pull request #1760 from penpot/fix-name-component
Fix name component
2022-03-30 16:51:42 +02:00
Andrés Moya
f782a7027a 🐛 Fix error when deleting all children of a nested group 2022-03-30 16:46:29 +02:00
Andrés Moya
a434318535 🐛 Fix show component name in sidebar 2022-03-30 16:39:47 +02:00
Eva
134265094c 🐛 Avoid numeric inputs to allow big numbers 2022-03-30 16:35:36 +02:00
Eva
4909e7861f 🐛 FIx the context menu of component widget 2022-03-30 16:35:36 +02:00
Andrey Antukh
ad9a7fdce8 📎 Set explicit clojure version on frontend and backend 2022-03-30 15:10:28 +02:00
Andrés Moya
97e97d0984 🐛 Fix undo after rotating a group 2022-03-30 15:07:56 +02:00
Andrey Antukh
4c6433b0f1 Improve migration 14
Remove frame thumbnail if the migration modifies a shape.
2022-03-30 14:38:36 +02:00
Andrey Antukh
f0d956f71c 📎 Update version.txt file 2022-03-30 13:43:46 +02:00
Alejandro Alonso
3a9d348cab 🐛 Add shadow to artboard make it lose the fill 2022-03-30 13:35:52 +02:00
alonso.torres
586bd13cc2 🐛 Fix issue with shift+select to deselect shapes 2022-03-30 13:28:25 +02:00
alonso.torres
e601e2acca 🐛 Fix linter problem 2022-03-30 13:23:59 +02:00
Alejandro Alonso
2a3c0e11da 🐛 Fixing export styles prettier 2022-03-30 13:13:29 +02:00
alonso.torres
bee40ae35c 🐛 Fix issue with shift+select to deselect shapes 2022-03-30 13:06:54 +02:00
Andrey Antukh
0392a1649f 🐛 Remove default fill-color and fill-opacity on image shapes 2022-03-30 12:27:30 +02:00
Alejandro Alonso
d4b52ad4f1 🐛 Fixing export styles 2022-03-29 18:25:11 +02:00
Alejandro Alonso
91249bc892 🐛 Weird stroke behaviour on duplicate 2022-03-29 16:27:33 +02:00
Eva
369eab3b5f 🐛 Avoid rotating shape when scrolling 2022-03-29 10:56:17 +02:00
alonso.torres
6780d17d2e 🐛 Fix drag guides to delete target area 2022-03-29 09:55:38 +02:00
alonso.torres
af22fee0c1 🐛 Fix problem with boolean and children objects 2022-03-29 09:55:38 +02:00
alonso.torres
61c111d5ae 🐛 Some fixes to SVG imports 2022-03-29 09:55:38 +02:00
Eva
3301148da6 🐛 Fix comments modal remains open on page change 2022-03-28 17:31:53 +02:00
Andrey Antukh
9ce0497f00 Add proper error handlings on http middleware 2022-03-28 17:24:52 +02:00
Andrey Antukh
36027583cd 📎 Minor change on create team instrumentation 2022-03-28 17:24:52 +02:00
Andrey Antukh
9abf4b126c Improve error handling 2022-03-28 17:24:52 +02:00
Andrey Antukh
ec5a4d09b8 🐛 Fix possible issue that causes exception on node tests 2022-03-28 17:24:52 +02:00
Andrey Antukh
2832736826 🎉 Add garbage collection task for file thumbnails
And additionally, rename the current task to file-gc
to match the real purpose of the task.
2022-03-28 17:24:52 +02:00
Andrey Antukh
b87e3c22b3 Improve worker error handling
Use the global error handlers for handle
also the worker errors.
2022-03-28 17:24:52 +02:00
Andrey Antukh
9582cc0211 🔥 Remove unused code 2022-03-28 17:24:52 +02:00
Andrey Antukh
1943877b21 Simplify d/group-by impl 2022-03-28 17:24:52 +02:00
Andrey Antukh
c876534c85 Move the dashboard grid thumbnails to backend cache 2022-03-28 17:24:52 +02:00
Andrey Antukh
b91c42e186 Add performance improvements to file thumbnails
Mainly addresing unnecesary object transmission. The new code strips
unnecesary data to be transferred from back to front.

Additionally it removes some legacy code and simplifies other
parts of code.
2022-03-28 17:24:52 +02:00
Alejandro Alonso
27c8f883ff 🐛 Fix ctrl-click on assets 2022-03-28 09:16:38 +02:00
Alejandro Alonso
5817b5fe19 🐛 Fix completed export text not shown 2022-03-25 14:50:13 +01:00
Alejandro Alonso
1db9b04bfd 🐛 Fix error when adding gradient stroke to shape 2022-03-25 14:49:42 +01:00
Andrey Antukh
00d851998b Merge pull request #1744 from penpot/multiexport-checkbox-fixes
🐛 Fix export multiple styles
2022-03-25 14:48:41 +01:00
Alejandro Alonso
927dbbfe82 🐛 Fix precission on export modal 2022-03-25 13:37:38 +01:00
Alejandro Alonso
d73ed95719 🐛 Fix export multiple styles 2022-03-25 13:20:46 +01:00
alonso.torres
01194d5e25 Add dashboard to shortcuts 2022-03-25 12:18:33 +01:00
alonso.torres
32d31da0da Show shortcuts debugging command 2022-03-25 12:00:58 +01:00
Alejandro Alonso
655afa088d 🐛 Fix copy paste inside a text layer leaves pasted text transparent 2022-03-25 10:08:41 +01:00
Andrey Antukh
0355e1bfc7 Merge branch 'alotor/bugfixes' into staging 2022-03-25 09:33:03 +01:00
alonso.torres
5aa68c7052 🐛 Fix problem with text displacement in Safari 2022-03-24 18:03:14 +01:00
alonso.torres
6e36f66dde 🐛 Fix shift+2 shortcut in MacOS with non-english keyboards 2022-03-24 18:03:14 +01:00
alonso.torres
32e4569495 🐛 Fix problems with CTRL in MacOS 2022-03-24 18:03:14 +01:00
alonso.torres
5a591d2acd 🐛 Fix paste ordering for frames not being respected 2022-03-24 17:25:43 +01:00
alonso.torres
e8980fbbfe 🐛 Fix problem with copy/paste in Safari 2022-03-24 17:25:43 +01:00
alonso.torres
8e68781a1b 🐛 Fix problems with trackpad zoom and scroll in MacOS 2022-03-24 17:25:43 +01:00
alonso.torres
ad19d64ce8 🐛 Fix problem with localhost register in Safari 2022-03-24 17:25:43 +01:00
Andrey Antukh
5ed84e3ae5 🐛 Set proper extension on download exported asset 2022-03-24 17:02:38 +01:00
Pablo Alba
5264863863 🐛 Fix enter on empty search page 2022-03-24 16:36:49 +01:00
Andrey Antukh
9c5c2ac8bf Merge pull request #1725 from penpot/multiexport-fixes
🐛 Multiexport fixes
2022-03-24 16:36:01 +01:00
Alejandro Alonso
1bbcf67396 🐛 Fix paths with no fill 2022-03-24 16:35:18 +01:00
Andrey Antukh
8b44b4d8f1 🐛 Fix unexpected decoding of fresian data 2022-03-24 15:15:42 +01:00
alonso.torres
ea7266dc3b 🐛 Fix performance problem with new texts 2022-03-24 13:50:08 +01:00
Alejandro Alonso
effb76c8db 🐛 Fix export multiple styles 2022-03-24 12:38:31 +01:00
Alejandro Alonso
2d52c4f4f5 🐛 Fix export translation 2022-03-24 12:19:06 +01:00
Alejandro Alonso
a753037178 🐛 Fix migration of fills and strokes for components 2022-03-24 11:39:01 +01:00
Alejandro Alonso
0d449f1292 🐛 Fix constraints assignation on multi-selection 2022-03-23 16:21:54 +01:00
Andrey Antukh
a0762aca45 🐛 Fix pdf print on exporter 2022-03-23 14:46:04 +01:00
Andrey Antukh
88ad68069c 📚 Update contributing file 2022-03-23 14:16:03 +01:00
Alejandro Alonso
80ef69c710 🐛 Fix sorting on multiple export 2022-03-23 14:08:33 +01:00
Andrey Antukh
6b164e10f2 📎 Update version.txt file 2022-03-23 13:23:16 +01:00
Andrey Antukh
b3d70f2556 🐛 Fix many issues related to exportation process 2022-03-23 13:21:52 +01:00
Pablo Alba
8fa708d573 Merge pull request #1715 from penpot/add-translations-terms-privacy
🐛 Translations missing on login/register for 'Terms of service an…
2022-03-23 13:20:18 +01:00
Pablo Alba
a68612ca2b 🐛 Translations missing on login/register for 'Terms of service and Privacy policy' 2022-03-23 13:10:53 +01:00
Alejandro
7d483b36d0 Merge pull request #1713 from penpot/keep-pencil-cursor
🐛 Pencil cursor changes when activated
2022-03-23 11:47:18 +01:00
Pablo Alba
61e409a09e 🐛 Pencil cursor changes when activated 2022-03-23 11:40:29 +01:00
Alejandro
5564d93d59 Merge pull request #1712 from penpot/revert-not-allow-edits-on-prototype-mode
🐛 Revert d2590c7: 🐛 [Prototype] Prototype mode should not all…
2022-03-23 11:22:49 +01:00
Pablo Alba
6674135c74 🐛 Revert d2590c7: 🐛 [Prototype] Prototype mode should not allow edits 2022-03-22 19:21:04 +01:00
Andrey Antukh
a4fbc050cc Merge remote-tracking branch 'origin/staging' into develop 2022-03-22 15:01:43 +01:00
Andrey Antukh
205b6d9881 Merge pull request #1708 from penpot/alotor/bugfixes
Alotor/bugfixes
2022-03-22 15:01:30 +01:00
alonso.torres
f2d1a4190a Don't stop SVG import when an image cannot be imported 2022-03-22 15:01:16 +01:00
alonso.torres
6008dc12d3 🐛 Fix clickable area in layers 2022-03-22 15:01:16 +01:00
alonso.torres
118b4367e7 🐛 Parametrized render to embed objects. Fix problem with fonts when exporting to SVG 2022-03-22 15:01:16 +01:00
alonso.torres
e6f8269c0b 🐛 Fix problem with inconsistency with border-radius 2022-03-22 15:01:16 +01:00
alonso.torres
928128ba2d 🐛 Fix problem when changing page while editing text 2022-03-22 15:01:16 +01:00
alonso.torres
444567faac 🐛 Fix problem when importing SVG's with uses with overriding properties 2022-03-22 15:01:16 +01:00
alonso.torres
eaa6ea80e6 🐛 Fix problem when adding shadows to imported text 2022-03-22 15:01:16 +01:00
alonso.torres
a4d362d43d 🐛 Fix problem when importing a SVG with text 2022-03-22 15:01:16 +01:00
alonso.torres
89e2f4a481 🐛 Fix crash on iOS when displaying viewer 2022-03-22 15:01:16 +01:00
Andrey Antukh
8acc9af1f5 📎 Add more events instrumentation 2022-03-22 14:48:10 +01:00
Andrey Antukh
0ebc1a766e Merge remote-tracking branch 'origin/staging' into develop 2022-03-22 14:34:25 +01:00
Andrey Antukh
bf6211903c 🐛 Fix issue on logging (backend) 2022-03-22 14:34:00 +01:00
Andrey Antukh
ad262f6fb3 Merge remote-tracking branch 'origin/library-changes-builder' into staging 2022-03-22 13:14:53 +01:00
Andrey Antukh
0a7d1831d2 Merge pull request #1701 from penpot/library-changes-builder
Library changes builder
2022-03-22 13:13:25 +01:00
Andrés Moya
ca56e08459 🎉 Add more test cases, and some fixes 2022-03-22 13:12:19 +01:00
Andrés Moya
31bfe3930d Prepare debug functions to be used in unit tests 2022-03-22 13:12:19 +01:00
Andrés Moya
48624b1db6 🔧 Refactor frontend unit tests and some fixes 2022-03-22 13:12:19 +01:00
Andrés Moya
5a33a002e4 🔧 Use changes-builder in library synchronization module 2022-03-22 13:12:19 +01:00
Andrey Antukh
43d3cc36e9 📎 Start new development cycle 2022-03-22 12:59:34 +01:00
Andrey Antukh
ee813abdc1 📎 Update changelog file 2022-03-22 12:58:33 +01:00
Andrey Antukh
411acc0a2f 📎 Sort translation files 2022-03-22 12:54:11 +01:00
Andrey Antukh
28cd649db3 Merge remote-tracking branch 'weblate/develop' into translations 2022-03-22 12:53:31 +01:00
bingling_sama
94f2269ff2 🌐 Add translations for: Chinese (Simplified).
Currently translated at 79.0% (714 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2022-03-22 12:53:14 +01:00
Andrey Antukh
c106b74239 Merge remote-tracking branch 'weblate/develop' into translations 2022-03-22 12:52:43 +01:00
Alejandro Alonso
3ae7c42afa Exporting big files flow 2022-03-22 12:31:34 +01:00
Andrey Antukh
0d4de50f13 📎 Minor fix on docker image files 2022-03-22 11:47:18 +01:00
Andrey Antukh
d4c1e2fc36 📎 Minor cosmetic fixes 2022-03-22 11:34:32 +01:00
Andrey Antukh
903a9356a9 🐛 Fix many issues after PR review 2022-03-22 11:34:32 +01:00
Alejandro Alonso
2f6018c35c 📎 Update changelog 2022-03-22 11:34:32 +01:00
Alejandro Alonso
0e0fb68c38 🎉 Add assets exportation in bulk (multiple)
And adapt to the websocket changes on backend and
exporter.
2022-03-22 11:34:32 +01:00
Andrey Antukh
f60d8c6c96 ♻️ Refactor websockets subsystem (on backend)
- Refactor msgbus subsystem, simplifying many parts.
- Enable persistent websocket connection for the all session duration.
2022-03-22 11:34:32 +01:00
Andrey Antukh
4a9e38a221 ♻️ Refactor exporter
- Migrate from puppeteer to playwright
- Fix many lifecycle and resource usage issues
- Add redis integration
- Enable multiple exportation
- Enable asynchronos exportation (with progress reporting)
2022-03-22 11:34:32 +01:00
Pablo Alba
f0a9889f33 🐛 Remove a decimal sets value to 0 (refactor) 2022-03-22 10:07:32 +01:00
Alejandro
aa386e12bc Merge pull request #1705 from penpot/fix/minus_placement
🐛 fix alignement of icon
2022-03-22 10:02:41 +01:00
Eva
ba46ab7361 🐛 fix alignement of icon 2022-03-22 09:51:52 +01:00
Alejandro
5ce3ce06c6 Merge pull request #1704 from penpot/fix/scroll_comments
🐛 Fix scroll in comment section
2022-03-22 09:49:48 +01:00
Eva
e95d940b5d 🐛 Fix scroll in comment section 2022-03-22 09:36:19 +01:00
Pablo Alba
14ed83fb31 🐛 Remove a decimal sets value to 0 2022-03-21 21:41:32 +01:00
Ahmad HosseinBor
497d42b822 🌐 Add translations for: Persian.
Currently translated at 22.7% (205 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-03-21 14:56:12 +01:00
Pablo Alba
3bae4839bd Search and filter layers 2022-03-21 11:21:12 +01:00
Andrey Antukh
81adcd03fb Minor fixes on devenv dockerfile 2022-03-20 13:37:37 +01:00
Andrey Antukh
7f3c67724e 🐛 Fix svg media asset upload internal server error 2022-03-20 13:04:12 +01:00
Andrey Antukh
741ad29d82 🎉 Add missing rlimit metadata and configuration 2022-03-18 17:12:12 +01:00
Ahmad HosseinBor
374de57e15 🌐 Add translations for: Persian.
Currently translated at 21.4% (194 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-03-18 16:58:06 +01:00
Andrey Antukh
ff30d505af ⬆️ Update CircleCI config 2022-03-18 15:16:08 +01:00
Alejandro Alonso
d4dc32a5e5 🐛 Go to style library file to edit in a new tab 2022-03-18 13:20:30 +01:00
Alejandro Alonso
c073a66e7e 🐛 Inner shadow with border not working properly 2022-03-18 10:55:55 +01:00
Andrés Moya
4d2de63374 Merge pull request #1690 from penpot/feat/pixel-precision
Pixel precision
2022-03-18 10:49:01 +01:00
Andrey Antukh
fa33c5852c Add missing rlimits on team and profile rpc mutations 2022-03-18 09:59:10 +01:00
Eva
510d9ab4d8 🐛 Fix overflow in color picker 2022-03-18 09:09:34 +01:00
alonso.torres
4f07613154 After review changes 2022-03-17 14:53:21 +01:00
alonso.torres
d2b5283489 🐛 Revert debugging text utilities 2022-03-16 17:52:38 +01:00
alonso.torres
aec68c52ab Improved snap to grids 2022-03-16 17:46:38 +01:00
alonso.torres
b5e965cf1a Improved behaviour for horizontal/vertical lines 2022-03-16 17:46:38 +01:00
alonso.torres
640723a4e7 Improved options input 2022-03-16 17:46:38 +01:00
alonso.torres
ccca3a38f0 🐛 Fix problem with multiple values in inputs 2022-03-16 17:46:38 +01:00
alonso.torres
9b862b672f Show pixel grid 2022-03-16 17:46:38 +01:00
alonso.torres
ad4c1aae45 🐛 Fix problem with flip rotations 2022-03-16 17:46:38 +01:00
alonso.torres
099d1259b2 Pixel/half-pixel on path drawing 2022-03-16 17:46:38 +01:00
alonso.torres
e5206e65e7 Pixel precision on modifiers 2022-03-16 17:46:38 +01:00
alonso.torres
9332d6f36c Improved resize/rotation handlers for shapes with tiny height/width 2022-03-16 17:46:38 +01:00
alonso.torres
f4be3aa9de Improvements over selrect generation 2022-03-16 17:46:38 +01:00
alonso.torres
0f54e85b36 ♻️ Refactor selrec generation 2022-03-16 17:46:38 +01:00
alonso.torres
ed9400912c Fix problems with extreme values 2022-03-16 17:46:38 +01:00
Alejandro Alonso
999af63118 🐛 Fixing dbg file upload with new http implementation 2022-03-16 13:07:01 +01:00
Alejandro
b0e2200166 Merge pull request #1686 from penpot/artboard-fixed
 Set the artboard layer fixed at the top side of the layers
2022-03-15 11:37:20 +01:00
alonso.torres
43d4acc94b 🐛 Fix linter issue 2022-03-15 11:27:12 +01:00
alonso.torres
7a253dc9e4 🐛 Fix problem with thumbnails not working 2022-03-15 11:17:06 +01:00
andy
b587f88968 🌐 Added translation for: Persian. 2022-03-15 10:29:41 +01:00
alonso.torres
491748af9f 🐛 Fix problem with import old files 2022-03-15 09:46:17 +01:00
alonso.torres
10e981d034 🐛 Fix problem with strokes and texts 2022-03-14 17:21:26 +01:00
Andrey Antukh
e188ae732a Merge remote-tracking branch 'origin/main' into develop 2022-03-14 14:34:58 +01:00
Andrey Antukh
7e8d8eef5a 🐛 Fix minor issues on event instumentation module 2022-03-14 13:56:32 +01:00
Andrey Antukh
e6d6b60b63 🐛 Properly filter complex data on events payload 2022-03-14 12:39:37 +01:00
Eva
70beb6c60c 🐛 Add ellipsis in long page names 2022-03-14 12:39:27 +01:00
alonso.torres
1990722f18 Merge remote-tracking branch 'origin/main' into develop 2022-03-14 12:17:06 +01:00
alonso.torres
aa416a782d 🐛 Fix problem with handlers over rules 2022-03-14 10:23:13 +01:00
Pablo Alba
7f2d5f4d69 Set the artboard layer fixed at the top side of the layers 2022-03-14 09:54:08 +01:00
Rodion Borisov
4fa6d37d6f 🌐 Add translations for: Russian.
Currently translated at 61.7% (558 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2022-03-13 00:56:28 +01:00
Rubén
b061844530 🌐 Add translations for: Catalan.
Currently translated at 99.4% (898 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2022-03-13 00:56:26 +01:00
Andrey Antukh
5add196d88 🐛 Don't instrument events with complex data 2022-03-11 18:11:59 +01:00
Andrey Antukh
1e580638d2 Merge pull request #1656 from penpot/social-logins-redesign
Authentication page and OIDC flows improvements
2022-03-11 17:22:03 +01:00
Andrey Antukh
f33d6610e7 📎 Properly log error on audit archive task fail 2022-03-11 16:21:11 +01:00
alonso.torres
a592f37593 Merge remote-tracking branch 'origin/main' into develop 2022-03-11 16:18:15 +01:00
Andrey Antukh
51dd869874 Merge pull request #1682 from penpot/alotor/hotfixes
Hotfixes
2022-03-11 15:56:45 +01:00
alonso.torres
5347409804 🐛 Fix problem with shift+ctrl+click to select 2022-03-11 15:38:48 +01:00
alonso.torres
aa6f82c31f 🐛 Fix issue with guides over shape handlers 2022-03-11 15:38:48 +01:00
Andrey Antukh
d9bd63d34f 📎 Reduce audit log archive task chunk size 2022-03-11 15:14:40 +01:00
Andrey Antukh
a8f5604718 📎 Improve http server configuration 2022-03-11 15:01:49 +01:00
Andrey Antukh
cf4f999b6a 📎 Improve api ergonomy of http server module 2022-03-11 09:50:49 +01:00
Andrey Antukh
52029f83ef 📎 Disable by default terms and privacy links
And make them configurable
2022-03-10 18:26:00 +01:00
Andrey Antukh
0c9a06789a 📎 Add correct copys and icons to login page 2022-03-10 17:45:20 +01:00
Alejandro
5709d2e757 Merge pull request #1677 from penpot/fix-select-color-for-stroke-from-palette
🐛 Fixing select color for stroke from palette
2022-03-10 17:13:02 +01:00
Andrey Antukh
11a0e01f08 Merge pull request #1670 from penpot/more-changes-builder
More changes builder
2022-03-10 16:50:55 +01:00
Alejandro Alonso
553c0e6d6a 🐛 Fixing select color for stroke from palette 2022-03-10 16:34:55 +01:00
Andrés Moya
7b81bb3fc2 💄 Change some code styles 2022-03-10 16:12:22 +01:00
Andrés Moya
e609670a41 🔧 Use changes-builder in many places 2022-03-10 15:37:10 +01:00
Andrés Moya
a7b455fb9a 🔧 Use changes-builder in workspace common operations 2022-03-10 15:21:58 +01:00
Andrés Moya
8ed857b4b9 🔧 Move :reg-objects operation to frontend 2022-03-10 15:21:58 +01:00
Eva
2bb8c535bd 🐛 Fix palette selection in color picker 2022-03-10 14:40:37 +01:00
Eva
e09884af60 🐛 Add ellipsis in long page names 2022-03-10 14:02:47 +01:00
Andrey Antukh
57399aeab2 🎉 Add the ability to specify email attr on oidc integration 2022-03-10 13:35:23 +01:00
Andrey Antukh
33c3e86e66 Add tests and improve impl of registration with invitation 2022-03-10 13:32:06 +01:00
Andrey Antukh
a7e77c3ea6 Minor fixes on login and register page structure 2022-03-10 13:32:06 +01:00
Andrey Antukh
2d76364b09 Enable login flag and disable demo-users by default 2022-03-10 13:32:06 +01:00
Andrey Antukh
36eaa18749 Enable register by invitation when register is disabled 2022-03-10 13:32:06 +01:00
Andrey Antukh
f7bb08382c Fix issues from previous refactor peer review 2022-03-10 13:32:06 +01:00
Andrey Antukh
9841a39d04 🐛 Fix issues on github oauth integration 2022-03-10 13:32:06 +01:00
Andrey Antukh
edf53840de 🐛 Fix issues with gitlab oidc provider 2022-03-10 13:32:06 +01:00
Andrey Antukh
6bd2dcff2a Minor improvements on error reporting 2022-03-10 13:32:06 +01:00
Andrey Antukh
73117f6f27 🐛 Set correct scopes for gitlab auth integration 2022-03-10 13:32:06 +01:00
Pablo Alba
3d588a88e2 💄 Social login redesign 2022-03-10 13:32:04 +01:00
Andrey Antukh
636dbd4e57 Merge pull request #1672 from penpot/set-artboard-as-thumbnail
 Set an artboard as the file thumbnail
2022-03-10 09:27:20 +01:00
Pablo Alba
0a04a856da Set an artboard as the file thumbnail 2022-03-10 09:05:41 +01:00
Andrey Antukh
e139284a98 Merge remote-tracking branch 'origin/main' into develop 2022-03-09 17:51:48 +01:00
Andrés Moya
a04980b251 Merge pull request #1660 from penpot/niwinz-async-refactor-2
Refactor backend (part3)
2022-03-09 17:20:12 +01:00
Andrey Antukh
8120a0cb9c 📎 Change backend repl script default env options 2022-03-09 17:18:06 +01:00
Andrey Antukh
c84f8808cb ♻️ Refactor loki integration
Make it implemented as worker thread instead of async
process just for simplify it.
2022-03-09 17:18:06 +01:00
Andrey Antukh
1b444a42f2 ♻️ Refactor http server layer
Make it fully asynchronous.
2022-03-09 17:18:06 +01:00
Andrey Antukh
a7e79b13f9 🐛 Fix library selection on color palette 2022-03-09 15:12:07 +01:00
Andrey Antukh
3e6be7e04c Merge pull request #1658 from penpot/fix-get-attrs-multi
🐛 Fix multiple edition
2022-03-08 15:25:15 +01:00
Andrés Moya
aa1e3f59ed 🔧 Small refactors 2022-03-08 15:17:02 +01:00
Andrés Moya
a13fb1f94f 🐛 Fix multiple edition 2022-03-08 15:10:23 +01:00
Andrey Antukh
19f4faa03f ♻️ Refactor workspace layout initialization and persistence 2022-03-08 12:59:56 +01:00
Andrey Antukh
965148f3a6 📎 Port fixes from main branch 2022-03-08 12:59:56 +01:00
alonso.torres
a0c0ab1871 🐛 Fix problem with handoff css 2022-03-08 11:53:56 +01:00
Alejandro
43cbe2dd39 Merge pull request #1665 from penpot/fix/bool-with-multiple-shapes
🐛 Fix problem with booleans and new fills/strokes
2022-03-08 10:02:20 +01:00
alonso.torres
9c00de047a 🐛 Fix problem with booleans and new fills/strokes 2022-03-08 09:52:20 +01:00
Andrey Antukh
49649a8814 Merge pull request #1662 from penpot/niwinz-hotfix-event-tracing-improvements
Minor improvements (hotfix)
2022-03-07 15:52:26 +01:00
Andrey Antukh
18a67a80bc 🔥 Remove unused code 2022-03-07 15:50:31 +01:00
Andrey Antukh
867669cc98 Add missing origin meta on left-toolbar events 2022-03-07 15:19:51 +01:00
Andrey Antukh
0158a93391 📎 Fix linter issues on staging branch 2022-03-07 15:10:03 +01:00
Andrey Antukh
fdb6533149 Minor improvement on workspace flags and modal event tracing 2022-03-07 15:10:03 +01:00
Andrey Antukh
6f32d721c2 📎 Minor changes on default values on devenv docker compose 2022-03-07 15:10:03 +01:00
Andrey Antukh
5f49656e30 Add proper event tracing on nudge modal
And ♻️ refactor data event handling, moving
some logic from component to the event.
2022-03-07 15:10:03 +01:00
Andrey Antukh
8114b165d9 📎 Update version.txt file 2022-03-07 13:13:41 +01:00
Andrey Antukh
dd39cb5a1c Merge pull request #1661 from penpot/fix/viewer-performance
🐛 Fix problems with viewer performance
2022-03-07 13:13:11 +01:00
Andrey Antukh
7f8c217e7c Merge remote-tracking branch 'origin/main' into staging 2022-03-07 13:11:38 +01:00
Andrey Antukh
d731a095c6 Merge branch 'main' into staging 2022-03-07 13:08:20 +01:00
alonso.torres
6630899d6e 🐛 Fix problems with viewer performance 2022-03-07 12:40:27 +01:00
Andrey Antukh
0cfd5095a7 🐛 Fix stack trace reporting on loki 2022-03-07 11:31:36 +01:00
Andrey Antukh
a588267fc2 Merge remote-tracking branch 'origin/main' into develop 2022-03-07 11:22:02 +01:00
Andrey Antukh
4f379821b5 🐛 Fix labels on loki logger 2022-03-07 11:09:06 +01:00
Eva
9eea7dabc2 🐛 Fix length of names in sidebar 2022-03-07 11:07:15 +01:00
Joseph V M
ca85a9a2a5 🌐 Add translations for: Malayalam.
Currently translated at 7.5% (68 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ml/
2022-03-05 21:57:58 +01:00
Pablo Alba
e34885de9b 🐛 Fix error on frame with border 2022-03-04 15:38:46 +01:00
Andrey Antukh
192b9213ac Merge pull request #1655 from penpot/multiple-members-invitations
 Allow send multiple team invitations at once
2022-03-04 15:20:51 +01:00
Pablo Alba
7e26e2bc21 Small changes on multi-input behaviour and styles 2022-03-04 15:06:58 +01:00
Eva
f9c0482949 Show actual coordinates while modifying and creating a shape 2022-03-04 13:16:57 +01:00
Eva
7e0d7ef727 🐛 avoid show rotation options with frames 2022-03-04 09:43:45 +01:00
Alejandro Alonso
d6820a69d4 🐛 Fixing texts with multiple strokes and fills 2022-03-04 07:56:47 +01:00
Pablo Alba
cf09ff8dc3 📎 Change spanish translation of pin-unpin 2022-03-03 22:02:36 +01:00
Pablo Alba
bda941746b Add '_' as zoom out shortcut 2022-03-03 21:54:30 +01:00
Andrey Antukh
f638a2ff49 Add revision fixes 2022-03-03 16:05:52 +01:00
Andrey Antukh
b348a882f4 🎉 Add minio client to devenv
And minor fix the nginx config.
2022-03-03 16:05:52 +01:00
Andrey Antukh
9e4a50fb15 ♻️ Refactor backend to be more async friendly 2022-03-03 16:05:52 +01:00
Andrey Antukh
cfe657d853 Make the multi-input more generic 2022-03-03 14:49:10 +01:00
Andrey Antukh
a1c3789ec2 🎉 Add parse email helper function 2022-03-03 14:49:10 +01:00
Pablo Alba
1cf9ad55c6 Allow send multiple team invitations at once 2022-03-03 14:49:09 +01:00
Andrés Moya
087d896569 🔧 Fix multiple edition 2022-03-03 11:36:25 +01:00
alonso.torres
17fc15138a Add suport to export/import frames with radius 2022-03-03 11:36:25 +01:00
Eva
d4af28c52b Add border radius to artboards 2022-03-03 11:36:25 +01:00
nautilusx
767a162077 🌐 Add translations for: German.
Currently translated at 97.8% (884 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-03-02 19:54:23 +01:00
alonso.torres
78d7fe3e10 New focus mode in workspace 2022-03-02 10:41:13 +01:00
Andrey Antukh
dc18a6c3bc 📎 Fix linter issues 2022-03-01 15:30:58 +01:00
Andrey Antukh
03cb738e55 Merge remote-tracking branch 'origin/main' into develop 2022-03-01 15:10:33 +01:00
Andrey Antukh
d1c834e647 🐛 Fix minor issue on executors monitor 2022-03-01 14:34:13 +01:00
Andrey Antukh
03a082fe40 🐛 Fix metrics on websocket connections 2022-03-01 14:19:26 +01:00
Pablo Alba
7691377c1b Persist color palette and color picker across refresh 2022-03-01 14:06:13 +01:00
alonso.torres
0534570784 🐛 Fix typo in text palette 2022-03-01 13:00:48 +01:00
Andrey Antukh
f2e389593a 🐛 Fix graphic asset rename 2022-03-01 12:50:10 +01:00
Alejandro
2037c3b202 Merge pull request #1649 from penpot/fixing-default-path-for-strokes
🐛 Fixing default path for strokes
2022-03-01 11:53:22 +01:00
Alejandro Alonso
1dc7db4456 🐛 Fixing default path for strokes 2022-03-01 11:23:20 +01:00
Andrey Antukh
fae79d67e6 Merge branch 'staging' 2022-03-01 11:10:27 +01:00
Andrey Antukh
271f69d59d Merge branch 'release-1.12' into staging 2022-03-01 11:08:21 +01:00
elhombretecla
6563cd9c8b 🎉 Add new release info dialog 2022-03-01 11:07:50 +01:00
alonso.torres
8d700491da 🐛 Fix 404 error on fills 2022-03-01 09:52:17 +01:00
Alejandro Alonso
7962c104b6 Adding specs for fills and strokes 2022-03-01 09:14:23 +01:00
Andrey Antukh
505d0f4768 📎 Update clj-kondo config 2022-02-28 22:11:42 +01:00
Andrey Antukh
e60b8a7aef 🐛 Minor fix on worker executors monitor 2022-02-28 17:21:36 +01:00
Andrey Antukh
cb65eca062 🐛 Fix double deref 2022-02-28 17:17:54 +01:00
alonso.torres
d6a5913086 Merge remote-tracking branch 'origin/staging' into develop 2022-02-28 16:10:30 +01:00
alonso.torres
a644599b16 🐛 Fix problem when disabling grid snap 2022-02-28 16:07:43 +01:00
alonso.torres
52def43f5a 🐛 Fix issue with react hooks 2022-02-28 15:46:11 +01:00
Andrey Antukh
5d2715dd32 Improve monitors monitor 2022-02-28 15:29:30 +01:00
Alejandro Alonso
13af98e5ad 📎 Removing unncesary TODO 2022-02-28 15:13:59 +01:00
Andrey Antukh
d14e907954 Merge remote-tracking branch 'origin/staging' into develop 2022-02-28 12:54:02 +01:00
alonso.torres
3f804339b9 🐛 Fix linter issues 2022-02-28 12:38:57 +01:00
Alejandro Alonso
a73a393e26 Ability to add multiple strokes to a shape 2022-02-28 12:38:57 +01:00
Andrey Antukh
1bad233e2f 📎 Fix linter issues on staging branch 2022-02-28 12:09:59 +01:00
Andrey Antukh
f64b1d3651 🐛 Properly handle invitations on login 2022-02-28 12:08:31 +01:00
Andrey Antukh
eb57c2f980 💄 Cosmetic changes on mutation profile ns 2022-02-28 12:08:05 +01:00
Andrey Antukh
ecd491cd09 🐛 Don't mark as touched temporal file 2022-02-28 12:07:44 +01:00
Andrey Antukh
dead3138b3 Reduce the size of the default thread pool 2022-02-28 12:07:21 +01:00
Andrey Antukh
0416082d4d 🐛 Fix awsns handler, convert it ot async 2022-02-28 12:06:47 +01:00
Joseph V M
98d1fd85fb 🌐 Add translations for: Malayalam.
Currently translated at 6.5% (59 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ml/
2022-02-25 21:56:12 +01:00
Andrey Antukh
719aacd6f8 🎉 Add new fmt macro 2022-02-25 14:57:37 +01:00
Andrey Antukh
4ee2ca2a33 🐛 Backport some fixes from staging 2022-02-25 13:18:51 +01:00
Andrey Antukh
45f9d5bb81 Merge remote-tracking branch 'origin/staging' into develop 2022-02-25 12:56:30 +01:00
Andrey Antukh
9f2d87d7d7 📎 Fix linter issues related to clj-kondo update 2022-02-25 12:54:29 +01:00
Andrey Antukh
d5b163f04d 🐛 Fix naming consistency and page background forwarding 2022-02-25 12:54:29 +01:00
alonso.torres
05c77d0248 🐛 Fix problem with collapsing pages 2022-02-25 12:53:22 +01:00
Alejandro Alonso
2fc4c30bed 🐛 [Prototype] Prototype mode should not allow edits 2022-02-25 12:41:19 +01:00
Alejandro Alonso
d2590c7651 🐛 [Prototype] Prototype mode should not allow edits 2022-02-25 12:24:09 +01:00
alonso.torres
237af505f9 🐛 Fix problem when editing texts 2022-02-25 11:41:55 +01:00
Andrey Antukh
7b4f522a33 📎 Minor fixes on frontend test code. 2022-02-25 11:07:40 +01:00
Andrey Antukh
0e7ce55f9a 📎 Fix linter issues and linter config 2022-02-25 11:07:40 +01:00
Andrey Antukh
fe43b3494c 🐛 Fix minor issues on es6 imports 2022-02-25 11:07:40 +01:00
Andrey Antukh
4c00c8f3ec Minor performance enhancement on str concat opetations
And proper stringify of :key prop of react components
2022-02-25 11:07:40 +01:00
Andrey Antukh
f05518e357 ♻️ Refactor workspace state organization
Move many local to a specific global prop.
2022-02-25 11:07:40 +01:00
Andrey Antukh
6e667e078c 🎉 Add cljs benchmark code under dev directory 2022-02-25 11:07:40 +01:00
Andrey Antukh
84a36624a6 🎉 Add specific namespace for data macros
And additionally add optimized macros for get-in,
select-keys and str.
2022-02-25 11:07:40 +01:00
Andrey Antukh
165c551e39 ⬆️ Update dependencies 2022-02-25 11:07:40 +01:00
Andrey Antukh
fe6ed2ceae Merge pull request #1631 from penpot/fix/color_palette_animation
🐛 Fix color palette animation
2022-02-25 09:15:39 +01:00
Andrey Antukh
92bcd549ef ⬆️ Update dependencies on devenv docker 2022-02-25 08:46:38 +01:00
Andrés Maldonado
5216471226 🐳 Fix run-devenv on systems with SELinux
This sets the selinux label on bind mounts (https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label), which is necessary so that containers can read the files.

Signed-off-by: Andrés Maldonado <maldonado@codelutin.com>
2022-02-24 22:31:33 +01:00
Joseph V M
6497ee02fb 🌐 Add translations for: Malayalam.
Currently translated at 5.3% (48 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ml/
2022-02-24 20:53:58 +01:00
Yaron Shahrabani
859e26cf8f 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (903 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2022-02-24 20:53:58 +01:00
alonso.torres
9964360656 📚 Updated changelog 2022-02-24 18:11:12 +01:00
Andrey Antukh
73f5e7c2ef Merge pull request #1623 from penpot/feat/svg-texts
Render Text as native SVG elements
2022-02-24 14:34:11 +01:00
alonso.torres
64ffa9bb3f 🐛 Fix problems with old texts 2022-02-24 14:05:01 +01:00
alonso.torres
ec63d23666 Multiple fills in text shapes 2022-02-24 14:05:01 +01:00
alonso.torres
a3063eb46d Add support for multiple shapes 2022-02-24 14:05:00 +01:00
alonso.torres
40b7cafacc Fix problems with strokes 2022-02-24 14:05:00 +01:00
alonso.torres
82c6b8daae Fix problems with export/import 2022-02-24 14:05:00 +01:00
alonso.torres
3228582cbe Fix problems when migrating old texts 2022-02-24 14:05:00 +01:00
alonso.torres
d0e008665f Fix masks for Firefox 2022-02-24 14:05:00 +01:00
alonso.torres
96eacb6efe Changed update text flow 2022-02-24 14:05:00 +01:00
alonso.torres
e183d67e2a Add spec for new text data 2022-02-24 14:05:00 +01:00
alonso.torres
bbf91a8957 Improved text selection 2022-02-24 14:05:00 +01:00
alonso.torres
618d22d214 Changes to text editor 2022-02-24 14:05:00 +01:00
alonso.torres
d83459f674 ❇️ Change mutation listener 2022-02-24 14:05:00 +01:00
alonso.torres
6cb6adc134 Allows svg text on test edit and creation 2022-02-24 14:05:00 +01:00
alonso.torres
18dded1a00 Fix editor and bounds for new texts 2022-02-24 14:05:00 +01:00
alonso.torres
1c2785f34e Adds borders to SVG texts 2022-02-24 14:05:00 +01:00
alonso.torres
a411cbc640 Initial SVG text support 2022-02-24 14:05:00 +01:00
alonso.torres
b4c87ad0b9 🐛 Fix font for guides and rulese 2022-02-24 11:45:56 +01:00
Andrey Antukh
37a35b1827 Minor improvements on telemetry task 2022-02-24 11:02:05 +01:00
Eva
ddae26b48b 🐛 Fix color palette animation 2022-02-24 09:46:19 +01:00
Andrey Antukh
c3f57cf900 Merge pull request #1619 from penpot/use-changes-builder
🔧 Refactor to use changes-builder
2022-02-24 09:19:51 +01:00
Andrés Moya
56b74c6ff2 🔧 Refactor shape ordering to use changes-builder 2022-02-23 14:16:45 +01:00
Andrés Moya
8682c07148 🔧 Small refactor changes-builder 2022-02-23 14:16:45 +01:00
Andrés Moya
96870c3fee 🔧 Refactor page actions to use changes-builder 2022-02-23 14:16:45 +01:00
Eva
24a0b4445e Open feedback page in a new tab 2022-02-23 12:51:02 +01:00
Eva
e139cba621 Scroll to selected font size or closest in font size selector 2022-02-23 12:50:23 +01:00
Andrey Antukh
07e8d110a2 🐛 Fix incorrect error id reporting on mattermost webhook 2022-02-23 12:41:33 +01:00
Andrey Antukh
87c1bc4bdb 🐛 Fix incorrect error id reporting on mattermost webhook 2022-02-23 12:40:28 +01:00
Andrey Antukh
31b13f3551 🐛 Fix issues with not authenticated requests
Related to concurrency model refactor.
2022-02-23 12:34:59 +01:00
Andrey Antukh
e15f5bb432 🐛 Fix issues with not authenticated requests
Related to concurrency model refactor.
2022-02-23 12:34:08 +01:00
Andrey Antukh
340ee859f9 📎 Fix linter issues 2022-02-23 12:17:18 +01:00
Andrey Antukh
496ba433e9 📎 Fix linter issues 2022-02-23 12:16:51 +01:00
Andrey Antukh
b183dc3e62 Merge remote-tracking branch 'origin/staging' into develop 2022-02-23 12:00:50 +01:00
Andrey Antukh
0b0ae756a3 🐛 Minor fix on audit http handler 2022-02-23 11:59:17 +01:00
Andrey Antukh
0ade0405f5 🐛 Fix feedback and audit-log http handlers 2022-02-23 11:49:25 +01:00
Eva
fcf8ad0611 ♻️ Rearrange changelog 2022-02-23 09:34:01 +01:00
Andrey Antukh
e0cb6d32ea Merge remote-tracking branch 'origin/staging' into develop 2022-02-23 09:14:51 +01:00
Andrey Antukh
aeed535f1b Minor improvement on reference handling on touched-gc task 2022-02-23 09:13:48 +01:00
Andrey Antukh
974084a9ca 🐛 Add missing executor dependency to auth handlers 2022-02-23 09:13:48 +01:00
Alejandro Alonso
88706534c2 🐛 Fixing fil typo 2022-02-23 08:33:03 +01:00
Eva
70def21153 ♻️ Improve file menu usage 2022-02-22 13:36:01 +01:00
Eva
941174a9fa 🐛 Show code icon on preview hover 2022-02-22 13:11:59 +01:00
Andrés Moya
46bfb2aacd 🐛 Fixed alignment of layers with children 2022-02-22 13:10:59 +01:00
Andrey Antukh
a4ef3f770c Merge remote-tracking branch 'origin/staging' into develop 2022-02-22 13:06:09 +01:00
Andrey Antukh
7cf27ac86d ♻️ Refactor general resource and concurrency model on backend 2022-02-22 13:05:41 +01:00
Joseph V M
823e5ca058 🌐 Add translations for: Malayalam.
Currently translated at 2.4% (22 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ml/
2022-02-22 12:57:50 +01:00
John Terroa
b7a182129d 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 56.0% (506 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2022-02-22 12:57:50 +01:00
Alejandro Alonso
10b147a25d 🐛 Importing shapes without fills 2022-02-22 10:53:47 +01:00
alonso.torres
6550631003 📚 Updated changelog 2022-02-22 10:52:58 +01:00
Migara
9d04dc7d9a 🎉 Add invitation section to dashboard 2022-02-22 09:20:31 +01:00
Andrey Antukh
486d89c5d0 Merge pull request #1607 from penpot/duplicate-flow
Duplicate flow
2022-02-22 08:48:20 +01:00
alonso.torres
d24f16563f Use remove to delete guides 2022-02-21 17:30:08 +01:00
Eva Marco
bb68838fa4 Merge pull request #1620 from penpot/fix_double_click
🐛 Fix problem with double click
2022-02-21 17:27:46 +01:00
alonso.torres
aed6a8a5ff 🐛 Fix problem with double click 2022-02-21 16:57:35 +01:00
Andrey Antukh
e13bceeb59 Merge remote-tracking branch 'origin/staging' into develop 2022-02-21 16:29:45 +01:00
Alejandro Alonso
1dab89f7ae 🌐 Added translation for: Malayalam. 2022-02-21 12:18:44 +01:00
Andrey Antukh
96facc5100 ♻️ Refactor invitation flow
Enfoces security and make the flow more deterministic.
2022-02-21 11:38:28 +01:00
Rubén
43d94d208f 🌐 Add translations for: Catalan.
Currently translated at 97.5% (881 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2022-02-19 22:58:21 +01:00
Yaron Shahrabani
741ee99e6b 🌐 Add translations for: Hebrew.
Currently translated at 99.6% (900 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2022-02-18 18:56:07 +01:00
Oğuz Ersen
6f2cff2f33 🌐 Add translations for: Turkish.
Currently translated at 99.7% (901 of 903 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2022-02-18 18:56:06 +01:00
Andrés Moya
0035827209 🎉 Duplicate shapes must create new flows if needed 2022-02-18 17:46:26 +01:00
Andrés Moya
c626b1d106 ♻️ Refactor duplicate objects 2022-02-18 13:14:20 +01:00
Andrés Moya
9c895cb8bb ♻️ Reorder some functions 2022-02-18 13:14:20 +01:00
Alejandro Alonso
23a9c74297 Ability to add multiple fills to a shape 2022-02-17 11:19:21 +01:00
Andrés Moya
aecb8a1464 🐛 Fix some broken tests 2022-02-17 11:19:21 +01:00
Andrés Moya
b9e3426532 🔧 Refactor calculation of multi selection attributes 2022-02-17 11:19:21 +01:00
Andrey Antukh
809d7ab7f4 Merge remote-tracking branch 'origin/staging' into develop 2022-02-17 11:16:00 +01:00
Andrey Antukh
6486b24c8b ⬆️ Update shadow-cljs version 2022-02-17 11:15:19 +01:00
Alejandro
e11d78d37a Merge pull request #1589 from penpot/us/team_members_redesing
Redesign Team members
2022-02-17 09:22:56 +01:00
Eva
3a34b3ae5f Team member redesign 2022-02-17 09:04:29 +01:00
Pablo Alba
75a8f85ebb Do not show the templates modal on onboarding 2022-02-16 21:34:47 +01:00
Andrés Moya
3d8f757712 🐛 Fixed cannot align objects inside a group but not inside a frame 2022-02-16 17:07:28 +01:00
Andrey Antukh
b37d6ec500 Merge remote-tracking branch 'origin/staging' into develop 2022-02-16 16:30:45 +01:00
Alejandro Alonso
4efd8b7d3f 🐛 Select All (CTRL+A) fails 2022-02-16 16:24:00 +01:00
Andrés Moya
5d17933593 🐛 Fix touched component marker appearing when it's not needed 2022-02-16 16:23:31 +01:00
Andrey Antukh
277d8f8b93 📎 Increase version on develop branch. 2022-02-16 14:01:30 +01:00
Andrey Antukh
f2c5add752 📎 Add new ongoing release to CHANGES.md file 2022-02-16 14:01:00 +01:00
Andrey Antukh
60d37b6de0 Merge branch 'staging' into develop 2022-02-16 14:00:46 +01:00
Andrey Antukh
206778021f 📎 Update changes.md file 2022-02-16 14:00:25 +01:00
Andrey Antukh
4a262de550 Merge branch 'niwinz-storage-transactionality-refactor' into staging 2022-02-16 13:58:36 +01:00
Andrey Antukh
350663b7ce 🎉 Add support for alternative S3 compatible services
And also add support for all AWS regions (prevoiosly onlu eu-central-1)
was supported.
2022-02-16 13:58:19 +01:00
Andrey Antukh
f1db0fea03 ♻️ Refactor storage transaction management 2022-02-16 13:58:15 +01:00
Pablo Alba
1990232adc 🎉 Add team invitations API 2022-02-16 13:52:31 +01:00
Andrey Antukh
256ed7410f Add unique id (uuid) on each log entry 2022-02-16 11:58:43 +01:00
Andrey Antukh
09a4cb30ec 🐛 Fix unresolved symbol error introduced in prev merge 2022-02-16 11:29:30 +01:00
Andrey Antukh
aa3826c389 📎 Sort translations 2022-02-16 11:26:13 +01:00
Andrey Antukh
b91042c1e5 Merge remote-tracking branch 'weblate/develop' into translations 2022-02-16 11:24:56 +01:00
Andrey Antukh
7eed8c5ee5 Merge remote-tracking branch 'origin/main' into develop 2022-02-16 11:23:26 +01:00
Andrey Antukh
3207860374 🐛 Fix compatibility issues of some requires and shadow-cljs 2022-02-15 16:01:46 +01:00
Keunes
b3bb8b6692 📎 Update bug_report.md file
Make clearer what information should be provided when filing a bug report.
2022-02-15 15:54:59 +01:00
Andrey Antukh
5b8b13c94c ⬆️ Update shadow-cljs to 2.17.2 2022-02-15 15:07:29 +01:00
Andrey Antukh
e8426006e3 Update version.txt file 2022-02-15 13:27:08 +01:00
Andrey Antukh
116fafd0e1 📎 Minor log param naming change 2022-02-15 13:25:46 +01:00
Andrey Antukh
e9fe1800e0 Fix minor issues on session expiration handling 2022-02-15 13:25:06 +01:00
Andrey Antukh
82796822d1 🐛 Fix possible race condition on component rename and deletion 2022-02-15 12:26:36 +01:00
Andrey Antukh
ce61b783fb Minor improvements on telemetry task 2022-02-15 12:26:36 +01:00
Andrey Antukh
9b78b2a432 Improve error reporting on background tasks 2022-02-15 12:26:36 +01:00
Andrey Antukh
321b2c7c23 🐛 Fix error handling on s3 delete-in-bulk operation 2022-02-15 12:26:36 +01:00
Andrey Antukh
dee397615c 📎 Update changelog file 2022-02-15 12:26:36 +01:00
Andrey Antukh
ef9339f6f1 🐛 Fix unexpected exception on handling empty state on boolean calc 2022-02-15 12:26:36 +01:00
Alejandro
f7f32408fc Merge pull request #1577 from penpot/fix/radial-gradients
 Changed radial gradients to use objectBoundingBox
2022-02-14 12:26:43 +01:00
Andrey Antukh
d4e6992442 Merge remote-tracking branch 'origin/main' into develop 2022-02-12 17:36:19 +01:00
Andrey Antukh
420ece7005 📎 Increase *print-level* on error reporting. 2022-02-12 17:35:29 +01:00
Andrey Antukh
741d2b3f3c Merge remote-tracking branch 'origin/main' into develop 2022-02-12 17:33:28 +01:00
Andrey Antukh
c8bf319b39 Merge pull request #1567 from penpot/frame-snapshot-api
 Frame snapshot api
2022-02-12 16:09:03 +01:00
Pablo Alba
34df52be5f 🎉 Add frame thumbnail API 2022-02-12 16:08:46 +01:00
Pablo Alba
fc2399a885 Rotation to snap to 15º intervals with shift 2022-02-11 12:42:43 +01:00
alonso.torres
699ec93ca4 Changed radial gradients to use objectBoundingBox 2022-02-11 12:33:13 +01:00
Andrés Moya
10598063d1 🔧 Provisional change menu to staging landing page 2022-02-11 12:32:57 +01:00
Eva Marco
db1e9574cd Merge pull request #1568 from penpot/fix/gradient-problem
🐛 Fix problem with gradient handlers
2022-02-11 11:27:01 +01:00
Andrés Moya
af74a1575b 🐛 Clear authentication cookies when logged out 2022-02-11 10:07:03 +01:00
Eva
03242e1a9c 🐛 Fix ungroup typography when editing 2022-02-10 16:20:13 +01:00
Andrey Antukh
dcbd89ff7c Increase default max connection pool size to 60. 2022-02-10 15:12:35 +01:00
Andrey Antukh
2312561041 Temporaly disable parallel uploading of files on import 2022-02-10 15:12:35 +01:00
Andrey Antukh
b591fbecf0 🎉 Add health check api endpoint 2022-02-10 15:12:35 +01:00
Andrey Antukh
3fbb440436 Handle EOF on websocket write/ping operations 2022-02-10 15:12:35 +01:00
Andrey Antukh
d358185a04 💄 Minor cosmetic change on database logger processor 2022-02-10 15:12:35 +01:00
Andrey Antukh
8babb59f75 Process audit log events only if profile-id is known 2022-02-10 15:12:35 +01:00
Andrey Antukh
3461ec2281 Ignore EOF errors on writting streamed response 2022-02-10 15:12:35 +01:00
Andrey Antukh
3dd94bd362 🐛 Log correct deleted number value on recheck task 2022-02-10 15:12:35 +01:00
Andrey Antukh
827c2140b7 ♻️ Refactor error reporting and logging context formatting
The prev approach uses clojure.pprint without any limit extensivelly
for format error context data and all this is done on the calling
thread. The clojure.pprint seems very inneficient in cpu and memory
usage on pprinting large data structures.

This is improved in the following way:

- All formatting and pretty printing is moved to logging thread,
  reducing unnecesary blocking and load on jetty http threads.
- Replace the clojure.pprint with fipp.edn that looks considerably
  faster than the first one.
- Add some safe limits on pretty printer for strip printing some
  data when the data structure is very large, very deep or both.
2022-02-10 15:12:35 +01:00
Andrés Moya
5a5222a97a 🐛 Fix error getting file library 2022-02-10 13:17:57 +01:00
Andrés Moya
bea3699451 🐛 Fix error instantiating a component 2022-02-10 12:27:44 +01:00
alonso.torres
93174f54a3 Change menu to add show/hide ui 2022-02-10 09:41:50 +01:00
Eva
e1348725c1 🐛 fix error when posting an empty comment 2022-02-10 09:28:05 +01:00
Andrey Antukh
528839cde2 Merge pull request #1569 from penpot/dashboard-user-menu
Dashboard user menu and session cookie
2022-02-09 23:51:14 +01:00
Andrés Moya
c5c331ee30 Refactor user menu in dashboard 2022-02-09 15:52:04 +01:00
Eva Marco
69effa37a3 Merge pull request #1570 from penpot/fix/problem-with-typographies
🐛 Fix problem with typographies in assets
2022-02-09 15:48:34 +01:00
alonso.torres
4c7a781228 🐛 Fix problem with typographies in assets 2022-02-09 15:26:45 +01:00
Andrés Moya
62a67bdb94 🎉 Set a domain cookie to check for logged from landing page 2022-02-09 15:25:40 +01:00
alonso.torres
c5c0b36f28 Improved mouse collision detection for groups and text shapes 2022-02-09 15:17:59 +01:00
Andrés Moya
0d48c758df 📚 Add new contributor change 2022-02-09 15:16:19 +01:00
Andrés Moya
4856413b24 Merge branch 'rhcarvalho-zopflipng' into develop 2022-02-09 15:13:53 +01:00
Rodolfo Carvalho
a1586280a9 Compress PNG images using zopflipng
Add a helper script and compress existing PNG images with zopflipng.

Before
552K    total

After
428K    total

Signed-off-by: Rodolfo Carvalho
2022-02-09 15:11:46 +01:00
Andrés Moya
00950b2c97 📚 Add new contributor change 2022-02-09 15:07:05 +01:00
Andrés Moya
79666bd51a Merge branch 'rhcarvalho-remove-dangling-png' into develop 2022-02-09 14:48:07 +01:00
Rodolfo Carvalho
ca284a86a3 Remove dangling images
Clean up images that are no longer in use.

Removed in 50eb744c3b:
- frontend/resources/images/color-bar-library.png
- frontend/resources/images/color-bar-options.png

Removed in 0de4f9074d:
- frontend/resources/images/color-gamma.png

Removed in 196b4dd89b:
- frontend/resources/images/colorspecrum-400x300.png

Added in 35c172a06b but maybe never used:
- frontend/resources/images/favicon-preview.png

Removed in d93fa72e48:
- frontend/resources/images/pot.png
2022-02-09 13:55:19 +01:00
alonso.torres
ee5b341d0e 🐛 Fix problem with gradient handlers 2022-02-09 13:04:16 +01:00
Alejandro
85cab5031d Merge pull request #1564 from penpot/fix/missing_translation
🐛 Fixed missing translation texts
2022-02-09 11:26:35 +01:00
Eva
2f7029516b 🐛 Fixed missing translation texts 2022-02-09 11:14:24 +01:00
Andrey Antukh
a1da4d4233 ♻️ Refactor common.page.helpers namespace. 2022-02-08 15:30:13 +01:00
Andrey Antukh
24724e3340 📎 Add helpful require on user ns 2022-02-08 15:30:13 +01:00
Eva
048ab9a0fc 🐛 fix missing translace string 2022-02-08 15:17:40 +01:00
Eva
40b005f46e 🐛 fix color palette overflow 2022-02-08 15:11:06 +01:00
Alejandro
ae2a99acb0 Merge pull request #1558 from penpot/fix/problem-svg-import
🐛 Fix problem with svg icons
2022-02-08 12:49:52 +01:00
alonso.torres
a81b6db093 🐛 Fix problem with svg icons 2022-02-08 12:30:52 +01:00
alonso.torres
39b05f5f9f 🐛 Fix problem with selection rect 2022-02-08 12:11:56 +01:00
Andrey Antukh
979f61df99 Merge remote-tracking branch 'origin/main' into develop 2022-02-08 09:12:13 +01:00
Andrey Antukh
e665f4e285 🐛 Log correct deleted number value on recheck task 2022-02-08 00:18:48 +01:00
Andrey Antukh
2c25dfcf1b 📎 Add exec perms to build script 2022-02-07 23:42:26 +01:00
Andrey Antukh
0632028579 📎 Set version to 1.11.1-beta 2022-02-07 23:26:21 +01:00
Andrey Antukh
95b9085258 Merge pull request #1555 from penpot/fix/thumbnails_firefox_problem
🐛 Fix Firefox problem when rendering frames
2022-02-07 18:03:33 +01:00
alonso.torres
cdc91feb28 🐛 Fix Firefox problem when rendering frames 2022-02-07 17:48:51 +01:00
alonso.torres
4caf278da5 🐛 Fix problems with handoff layout 2022-02-07 16:34:31 +01:00
Andrey Antukh
809a3420c1 Merge pull request #1554 from penpot/feat/tablet-improvements
Tablet improvements
2022-02-07 15:42:55 +01:00
alonso.torres
af8e9058a3 Move selection with space 2022-02-07 15:32:27 +01:00
alonso.torres
2b1c8cafe9 Improved color picker 2022-02-07 15:18:30 +01:00
alonso.torres
1abcd5819b Enter in dashboard to open files 2022-02-07 15:18:30 +01:00
alonso.torres
76b34bb600 Workspace interactions improvements 2022-02-07 15:18:30 +01:00
alonso.torres
67c6a042a0 Improved incremental selection 2022-02-07 15:18:30 +01:00
alonso.torres
72c2a213b4 Curve tool improvements 2022-02-07 15:18:30 +01:00
alonso.torres
ec1cc8ec64 Adds new shortcut for zoom in 2022-02-07 15:18:30 +01:00
alonso.torres
fbbb079599 ♻️ Remove rx/first calls and replaced by safer rx/take 1 2022-02-07 15:18:30 +01:00
Eva
b8f2f3e34d Show recent fonts only on text edition area not in typographies 2022-02-07 15:06:05 +01:00
Alejandro
39b29ee3f0 Merge pull request #1552 from penpot/fix/shadow_type_text
🐛 Fix shadow type text in handoff section
2022-02-07 13:15:46 +01:00
Eva
5f6cb1e0d7 🐛 Fix shadow type text in handoff section 2022-02-07 13:04:52 +01:00
Andrey Antukh
46250e6fab ⬆️ Update nodejs version on docker images. 2022-02-07 11:40:53 +01:00
Alejandro Alonso
fc2a26f249 🎉 Add border radius support to image shapes 2022-02-07 11:33:23 +01:00
Andrey Antukh
341caa3489 🎉 Add docker images auxiliar build script. 2022-02-07 11:21:54 +01:00
Eva
38b7474f0b Add a little improvent in recent fonts selector 2022-02-07 09:34:22 +01:00
Andrey Antukh
c91e2d13c0 📎 Add temporal workaround on config.env file 2022-02-06 23:40:04 +01:00
Pablo Alba
7134bbf484 Disallow using same password as user email 2022-02-04 17:41:01 +01:00
Andrey Antukh
6b18b258a4 🐛 Set proper default http server host. 2022-02-04 16:02:51 +01:00
Eva
86e4826e48 Add configurable nudge amount 2022-02-04 15:15:48 +01:00
Andrey Antukh
6461ebe2b8 🔥 Remove unreachable code. 2022-02-04 15:04:47 +01:00
Andrey Antukh
bfb23ad60b ⬆️ Update backend and frontend clojure deps 2022-02-04 15:04:47 +01:00
Andrey Antukh
637d6a0076 ⬆️ Update common module deps 2022-02-04 15:04:47 +01:00
Andrey Antukh
cbb8d13570 ⬆️ Update frontend npm dependencies 2022-02-04 15:04:47 +01:00
Andrey Antukh
2a6ba79e9a Ignore EOF errors on writting streamed response 2022-02-04 15:04:47 +01:00
Andrey Antukh
1e0dacfe9b Add reusable helper for expound pretty printing 2022-02-04 15:04:47 +01:00
Andrey Antukh
b194c0c5d8 Merge pull request #1534 from penpot/feat/toolbars-redesign
Toolbars Redesign
2022-02-04 09:26:22 +01:00
alonso.torres
9789b7081a Post-review changes 2022-02-03 18:27:12 +01:00
alonso.torres
03052ddd28 Fixed hover on sidebar 2022-02-03 18:27:12 +01:00
alonso.torres
779f685f72 Update strings for the new tabs 2022-02-03 18:27:12 +01:00
alonso.torres
1dee767762 Selection area on rules 2022-02-03 18:27:12 +01:00
alonso.torres
5cac5eb26b New text typographies palette 2022-02-03 18:27:12 +01:00
alonso.torres
b26cbeccca Resizable color palette 2022-02-03 18:27:12 +01:00
alonso.torres
8d4612c683 🐛 Fix some problems with scroll into view for layers 2022-02-03 18:27:12 +01:00
alonso.torres
e352c70013 Move layers and assets to tabs 2022-02-03 18:27:12 +01:00
alonso.torres
8c3c9a8ca4 Refactor workspace header 2022-02-03 18:27:12 +01:00
alonso.torres
ada837f7e4 New rules styles, resize pages 2022-02-03 18:27:12 +01:00
alonso.torres
1599b2644a Resizeable panels 2022-02-03 18:27:12 +01:00
Alejandro Alonso
acc3d00fd5 🎉 Add stroke properties to image shape 2022-02-03 17:23:26 +01:00
Alejandro Alonso
0f459ede50 🐛 Fix issue in viewport-scrollbars 2022-02-03 13:24:51 +01:00
Pablo Alba
105cb6fa13 Enhance the behaviour of the artboards list on view mode 2022-02-03 11:52:04 +01:00
Pablo Alba
1797c702a7 Automatically open comments from dashboard notifications 2022-02-03 11:38:30 +01:00
Pablo Alba
5f580f10ca On user settings, hide the theme selector as long as we only have one theme 2022-02-03 11:26:45 +01:00
Andrey Antukh
bd359f42f5 📎 Add package-lock.json to .gitignore file 2022-02-02 19:17:51 +01:00
Andrey Antukh
34bf73210e 🔥 Remove package-lock.json file. 2022-02-02 19:14:12 +01:00
Andrey Antukh
f1db4aae35 Merge branch 'main' into develop 2022-02-02 16:23:11 +01:00
Andrey Antukh
7710ffcbf1 🐛 Fix issue on 400 error handler. 2022-02-02 15:31:54 +01:00
Andrey Antukh
e9f45a0d0a 🐛 Fix release modal. 2022-02-02 15:02:47 +01:00
Andrey Antukh
743c2c3385 Merge branch 'staging' 2022-02-02 14:29:30 +01:00
Andrey Antukh
6f714facf9 🐛 Fix many minor issues on telemetry task 2022-02-01 17:49:18 +01:00
Andrey Antukh
5f81c7bc2d Merge remote-tracking branch 'origin/staging' into develop 2022-02-01 16:14:52 +01:00
Pablo Alba
72b00fa9af On team settings set color of members count to black 2022-02-01 15:35:51 +01:00
Andrés Moya
449756a0e4 🐛 Fix ungroup a component leaves asterisk in layers 2022-02-01 15:32:50 +01:00
Andrey Antukh
75930a0ce9 📚 Minor changes on commit guidelines on CONTRIBUTING.md file 2022-02-01 14:25:50 +01:00
Eva
a2c3b0926b Add recent used fonts in font selection widget 2022-02-01 14:11:54 +01:00
Andrey Antukh
57666e9173 Minor improvements on http reporting on 400 responses 2022-02-01 13:53:10 +01:00
alonso.torres
37f4b83d96 🐛 Fix problem with hover shapes 2022-02-01 13:09:51 +01:00
elhombretecla
5576b7568c 🎉 Add new content for release dialog. 2022-02-01 13:03:02 +01:00
Eva Marco
99e067b863 Merge pull request #1523 from penpot/test-e2e-enter-dashboard
👷 e2e tests for dashboard
2022-02-01 12:47:29 +01:00
Pablo Alba
5103624fe0 👷 e2e tests for dashboard
Including test for signing/singup, projects, files, teams, and misc
2022-02-01 11:50:33 +01:00
Andrey Antukh
26e5d57ced 🐛 Fix incorrect alias on shape-attrs spec on workspace. 2022-01-28 16:19:30 +01:00
Andrey Antukh
b586f2552c Merge branch 'staging' into develop 2022-01-28 13:58:22 +01:00
Andrey Antukh
0fbcec667c 📎 Minor format change on changelog file. 2022-01-28 13:57:11 +01:00
Eva
f40c58c64a 💄 Remove dots at the end of each line in changes file in actual sprint 2022-01-28 11:40:22 +01:00
Eva
d66619fe6d 💄 Remove dots at the end of each line in changes file 2022-01-28 11:36:47 +01:00
Eva
5c1b007c1b Align item to it's parent 2022-01-28 10:54:31 +01:00
Pablo Alba
86c394f4ce Merge pull request #1514 from penpot/enhacement/add-profile-e2e-tests
👷 Add e2e test to profile area
2022-01-28 10:32:49 +01:00
Andrés Moya
90d130a3bc 📚 Remove unneeded section in changelog 2022-01-28 10:21:36 +01:00
Eva
f185836fd4 👷 Add e2e test to profile area 2022-01-28 10:20:48 +01:00
Andrey Antukh
4c851856ff Emit warning when an error is raised on formating body. 2022-01-27 17:40:03 +01:00
Andrey Antukh
bc2a0432b9 Allow connect to read-only databases. 2022-01-27 16:11:32 +01:00
Alejandro Alonso
f72e140327 Graphic tablet use improvements: add scroll bars 2022-01-27 16:02:40 +01:00
Andrey Antukh
a633ed3c9a More tweaks on the error reporter. 2022-01-27 15:17:44 +01:00
Andrey Antukh
a8a6882708 💄 Minor changes on repl script. 2022-01-27 15:00:42 +01:00
Andrés Moya
1b76ed97e1 🐛 Fix rotation when set to 0 again 2022-01-27 14:57:26 +01:00
Andrey Antukh
04f7169aef ♻️ Refactor and modularize all specs. 2022-01-27 13:03:44 +01:00
Andrés Moya
d83b362c9f 🐛 Fix "move to" menu when duplicated team or project names 2022-01-27 11:39:18 +01:00
Andrey Antukh
b1d55348dc Merge remote-tracking branch 'origin/staging' into develop 2022-01-26 18:13:48 +01:00
Andrey Antukh
f8a46c56e9 🎉 Add helper for offline validate files with specs. 2022-01-26 18:12:17 +01:00
Andrey Antukh
420525cdf0 🐛 Make the path command params optional. 2022-01-26 18:11:20 +01:00
Andrey Antukh
686cacd5ae Add truncate function to time ns on backend. 2022-01-26 18:10:47 +01:00
Andrey Antukh
0092806dda Prevent high cpu usage on reporting big errors. 2022-01-26 18:09:59 +01:00
Andrey Antukh
2f8c63505f 💄 Fix linter issues. 2022-01-26 14:45:22 +01:00
Andrey Antukh
d892be4971 💄 Fix linter issues. 2022-01-26 14:44:01 +01:00
Andrey Antukh
59ed833abc Merge remote-tracking branch 'origin/staging' into develop 2022-01-26 14:24:34 +01:00
Andrey Antukh
110fb2e8db Minor improvements on error reporting.
Add missing expound.
2022-01-26 14:18:44 +01:00
Andrey Antukh
9f7a04e330 🐛 Fix unexpected exception on handling export data validation. 2022-01-26 14:08:54 +01:00
Andrey Antukh
ccbc519c04 💄 Minor cosmetic changes on internal spec naming. 2022-01-26 12:59:26 +01:00
Eva
036860b91b 🐛 fix typo in zoom options 2022-01-26 12:59:02 +01:00
Eva
7ac2a55315 🐛 Fix header z-index in viewer mode fullscreen 2022-01-26 12:58:27 +01:00
Andrey Antukh
f6cf8d2b1b 🐛 Relax text shape content spec validation.
Allow to be null in some circumstances.
2022-01-26 12:56:42 +01:00
Andrey Antukh
16788d7ab7 🐛 Make a path command params property nilable on spec. 2022-01-26 12:41:54 +01:00
Andrey Antukh
3142d48f3c 💄 Minor cosmetic change on changelog file. 2022-01-26 12:19:10 +01:00
Andrey Antukh
e1a88ae899 Merge branch 'staging' into develop 2022-01-26 12:16:50 +01:00
Andrey Antukh
a2e80cee47 📚 Update changelog file. 2022-01-26 12:09:27 +01:00
alonso.torres
5f14769abc 🐛 Fix problem with hover-ids 2022-01-26 11:49:01 +01:00
Eva
406c4063de Add select layer to contest menu 2022-01-26 11:49:01 +01:00
Andrey Antukh
b4bc30e56f Improve shape specs and add missing specs for content. 2022-01-26 11:30:50 +01:00
Eva
3482d6c303 Add Update component in bulk option 2022-01-26 10:53:31 +01:00
Andrés Moya
9dfd5c0bcc 🐛 Avoid empty names in projects, files and pages 2022-01-26 09:31:55 +01:00
Eva
b2b3de2782 🐛 fix typo in zoom options 2022-01-26 09:30:10 +01:00
Eva
50c20e2290 🐛 Fix header z-index in viewer mode fullscreen 2022-01-26 09:30:10 +01:00
Andrey Antukh
a10dcbd918 Merge pull request #1508 from penpot/feat/guides
Guides
2022-01-25 14:58:36 +01:00
alonso.torres
6e0433a34b Review changes 2022-01-25 14:54:13 +01:00
alonso.torres
8833e19c7f 🐛 Small fixes for guides 2022-01-25 14:17:13 +01:00
alonso.torres
663358bdae 📚 Update changelog 2022-01-25 14:17:13 +01:00
alonso.torres
d9b1c0e2e6 More tests for snap data 2022-01-25 14:17:13 +01:00
alonso.torres
39334b81ac Guides cursors 2022-01-25 14:17:13 +01:00
alonso.torres
62f7323acf Move frames with guides move the guides 2022-01-25 14:17:13 +01:00
alonso.torres
3f89baa1fe Move guides together with frames 2022-01-25 14:17:13 +01:00
alonso.torres
f0fd1bb40c Add menu option for guides 2022-01-25 14:17:13 +01:00
alonso.torres
f303d7b33e Add support to export/import guides 2022-01-25 14:17:13 +01:00
alonso.torres
d356a3fa56 Spec definition for guides 2022-01-25 14:17:13 +01:00
alonso.torres
64e7cad292 ♻️ Redone the snap calculation and added guides 2022-01-25 14:17:13 +01:00
alonso.torres
0766938f98 Add guides UI 2022-01-25 14:17:13 +01:00
alonso.torres
918829ad0a Improve import with parallel media upload 2022-01-25 11:30:32 +01:00
Pablo Alba
540e1fc492 🐛 Fix missing entry of e2e fixtures on gitignore 2022-01-25 11:11:51 +01:00
alonso.torres
ac30754a96 🐛 Fix problem with import 2022-01-25 10:28:04 +01:00
Andrey Antukh
b470a0ebbf 🐛 Add missing dependency update (lost in some merge). 2022-01-24 22:22:43 +01:00
Andrey Antukh
69daee4137 Merge branch 'staging' into develop 2022-01-24 16:21:01 +01:00
Andrey Antukh
3d6c903273 Improve path rendering performance. 2022-01-24 16:14:28 +01:00
Andrey Antukh
bc04a0b9f0 🐛 Fix incorrect behavior of trim-file-data. 2022-01-24 16:08:52 +01:00
Andrey Antukh
bfef94dbfb Merge branch 'main' into staging. 2022-01-24 16:07:47 +01:00
Andrey Antukh
9e06275945 🐛 Prevent exception on broken path. 2022-01-24 15:41:44 +01:00
Andrey Antukh
6410bcf3c8 Minor change on debug endpoint. 2022-01-24 15:13:02 +01:00
Andrés Moya
20baf02726 🐛 Normalize zoom levels in workspace and viewer 2022-01-24 13:30:11 +01:00
Andrey Antukh
8f6fdf361b Improve path rendering performance. 2022-01-24 13:23:09 +01:00
Andrey Antukh
ffa134f824 🐛 Fix incorrect behavior of trim-file-data. 2022-01-24 13:23:09 +01:00
Andrés Moya
b4bf6b9235 🐛 Fix zoom in/out after fit or fill in viewer 2022-01-24 11:04:01 +01:00
Andrés Moya
c3e37b0e04 🐛 Fix auto hide header in viewer full screen 2022-01-24 11:04:01 +01:00
Andrés Moya
374bba763b 🐛 Fix edit blur attributes for multiselection 2022-01-24 11:04:01 +01:00
Pablo Alba
2d00e68b78 👷 Tests e2e for drawing basic forms 2022-01-24 10:56:56 +01:00
Andrey Antukh
9a965dc693 Merge remote-tracking branch 'origin/staging' into develop 2022-01-21 14:54:32 +01:00
Andrey Antukh
b96ad5b37f 💄 Minor cosmetic change on get-parents fn. 2022-01-21 14:47:13 +01:00
Andrey Antukh
07a0f67b32 💄 Minor cosmetic change on reg-object. 2022-01-21 14:47:13 +01:00
Andrey Antukh
c754a757eb Upgrade rumext and add some examples of syntax sugar. 2022-01-21 14:47:13 +01:00
Andrey Antukh
dcd53183a8 📎 Simplify distribute-objects fn impl. 2022-01-21 14:47:13 +01:00
Andrés Moya
5641132eb9 Reload viewer and reset starting frame from workspace 2022-01-21 14:42:11 +01:00
Andrés Moya
b4c23f3554 🐛 Display animations only for allowed actions 2022-01-21 14:41:21 +01:00
Andrés Moya
7385445aa8 💄 Format translation files and remove unneeded script 2022-01-21 14:40:37 +01:00
Eva
5409f83167 Divide file menu options in semantically groups 2022-01-21 12:36:09 +01:00
Voxybuns
43951aad69 🌐 Add translations for: French.
Currently translated at 79.9% (694 of 868 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-01-20 21:55:48 +01:00
Rubén
9681d8c805 🌐 Add translations for: Catalan.
Currently translated at 98.8% (858 of 868 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2022-01-20 21:55:48 +01:00
Andrey Antukh
ff4d3cfeac 🐛 Fix issue on fressian blob encoding. 2022-01-20 16:57:20 +01:00
Andrey Antukh
8e4338c1c9 🐛 Minor fix on version parsing impl. 2022-01-20 14:47:28 +01:00
Andrey Antukh
c27d709b6b Merge remote-tracking branch 'origin/staging' into develop 2022-01-20 14:30:16 +01:00
Andrey Antukh
8caa289586 Fix version logging on exporter. 2022-01-20 14:27:03 +01:00
Andrey Antukh
f7568f6348 Minor improvements on exporter. 2022-01-20 14:16:58 +01:00
Pablo Alba
6a6f079a84 👷 Create firsts e2e tests 2022-01-20 14:10:48 +01:00
Andrey Antukh
0f04b86316 Improve performance on import .penpot files. 2022-01-20 11:47:17 +01:00
Andrey Antukh
1dae8a0771 📎 Minor improvement on error logging on worker. 2022-01-20 11:47:17 +01:00
Andrey Antukh
9bc816fc1c 🐛 Remove duplicate audit action reporting. 2022-01-20 11:47:17 +01:00
Andrey Antukh
11ea4c7aec 🐛 Fix raw logging on cljs. 2022-01-20 11:47:17 +01:00
Andrey Antukh
0c53aa158b 🐛 Fix profile image generation issue. 2022-01-20 11:47:17 +01:00
Andrey Antukh
072e4a4f98 Revert some memoizes on page/helpers.
And improves base performance of get-children and
remove duplicated code. Also optimize the use
of get-children on react components with corresponding
use-memo hook.
2022-01-20 11:47:17 +01:00
Andrey Antukh
1b3b3b0ee6 Minor naming change on page query parameters. 2022-01-20 11:47:17 +01:00
Andrey Antukh
d1e4f0de3e Improve performance and resolve render issues on exporter. 2022-01-20 11:47:17 +01:00
Andrey Antukh
fd3f304e07 📎 Increase default devenv jvm heapsize. 2022-01-20 11:47:17 +01:00
alonso.torres
9e7551551f 🐛 Fix problem with SVG's import/export 2022-01-19 16:07:44 +01:00
Andrés Moya
36bb5cbe01 🐛 Fix several questions about frame background export 2022-01-19 11:01:23 +01:00
Alejandro Alonso
f754c12e8c Limit pasted object position to container boundaries 2022-01-18 17:07:46 +01:00
Andrey Antukh
6f5916e334 ♻️ Minor reorganization on export & render namespaces. 2022-01-18 16:48:17 +01:00
Andrés Moya
13dd1cb6b6 🐛 Disable embed images in export 2022-01-18 16:48:17 +01:00
Andrey Antukh
eb4e7e0f0c 🐛 Fix dashboard grid thumbnails cache invalidation. 2022-01-18 16:48:17 +01:00
Andrey Antukh
7afb3e2c6d Stream transit encoding to the response output-stream.
Instead of in-memmory encoding. This will prevent many OOM errors.
2022-01-18 16:48:17 +01:00
Andrey Antukh
9cf5258053 Improve logging on worker and import process. 2022-01-18 16:48:17 +01:00
Andrey Antukh
56dfdaecb7 💄 Add cosmetic improvements on project_menu component. 2022-01-18 16:48:17 +01:00
Andrey Antukh
1d174a4379 🐛 Fix unexpected exception on time equiv impl. 2022-01-18 16:48:17 +01:00
Andrés Moya
2aeded1940 🐛 Show view mode buttons correctly centered in viewer 2022-01-18 13:26:09 +01:00
Andrés Moya
c23691284c 🐛 Show correctly group types label in handoff 2022-01-18 13:26:09 +01:00
Andrey Antukh
f7f6515561 ⬆️ Update exporter dependencies. 2022-01-18 11:25:06 +01:00
Andrey Antukh
438c14d29d ⬆️ Update exporter dependencies. 2022-01-18 10:58:45 +01:00
Andrés Moya
87351000ae 🐛 Avoid marking component as touched when moving into a group 2022-01-18 09:39:02 +01:00
Pablo Alba
0895a69bac Add an option to hide artboards names on the viewport 2022-01-18 09:38:03 +01:00
Eva
4285972e41 Add new zoom options in workspace and viewer mode 2022-01-17 17:18:12 +01:00
Pablo Alba
d33542c4dc Allow decimals on stroke width and positions 2022-01-17 15:52:32 +01:00
Andrey Antukh
bda97adf4f Fix minor issues on error reporting. 2022-01-17 13:09:15 +01:00
Andrey Antukh
b6f460940f ⬆️ Update dependencies. 2022-01-17 13:09:15 +01:00
Alejandro Alonso
aa0e8ed8d6 Pressing enter to exit the fields 2022-01-17 10:00:28 +01:00
Yaron Shahrabani
b99fa16b96 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (868 of 868 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2022-01-15 17:53:28 +01:00
Oğuz Ersen
630d7a3220 🌐 Add translations for: Turkish.
Currently translated at 99.7% (866 of 868 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2022-01-15 17:53:27 +01:00
Andrey Antukh
03c91664cb 🐛 Fix session invalidation issue on password change. 2022-01-14 13:44:58 +01:00
Andrey Antukh
13773d829a Merge pull request #1473 from penpot/alotor/bugfixes_fixes
Bugfixes
2022-01-14 13:33:23 +01:00
Alejandro Alonso
d9e6e9b017 Ability to ignore background when exporting an artboard 2022-01-14 13:31:28 +01:00
alonso.torres
5d8982c734 🐛 Fix transform text 2022-01-14 12:10:55 +01:00
alonso.torres
f13c82da2a 🐛 Fix tab to apply changes in inputs 2022-01-14 11:58:56 +01:00
alonso.torres
363b0ba997 🐛 Fix problems with gradient and libraries 2022-01-14 11:57:07 +01:00
Alejandro Alonso
a4c45942c9 Fixing show color hex or name on hover 2022-01-14 11:57:07 +01:00
Andrey Antukh
a86e3a8636 📎 Sort translation files. 2022-01-14 11:32:43 +01:00
Andrey Antukh
db61d579e6 Merge remote-tracking branch 'weblate/develop' into translations 2022-01-14 11:30:25 +01:00
Andrey Antukh
e6e3f2cbd5 Add debugging pages for download/upload file data. 2022-01-14 10:54:57 +01:00
Andrey Antukh
ffdd539233 Minor improvements on error reporting mechanism. 2022-01-14 10:54:57 +01:00
Andrey Antukh
ef17af38a1 🎉 Add jvm-repl script to frontend directory. 2022-01-14 10:54:57 +01:00
Andrey Antukh
6dedfaea2f 📎 Minor changes on :reg-objects impl. 2022-01-14 10:54:57 +01:00
Andrey Antukh
cbb3783d84 📎 Add performance related helpers. 2022-01-14 10:54:57 +01:00
Eva
327c095d79 add V as shortcut to exit path editor 2022-01-13 22:39:05 +01:00
Andrey Antukh
88e222420c Merge pull request #1470 from penpot/alotor/bugfixes
Bugfixes
2022-01-13 18:33:32 +01:00
alonso.torres
045eec072b 🐛 Fix import/export with SVG edge cases 2022-01-13 18:28:47 +01:00
alonso.torres
5f3c381f88 🐛 Fix thumbnail not taking frame blending mode 2022-01-13 15:56:01 +01:00
alonso.torres
6090cf6c68 🐛 Fix problem with imported svgs with filters 2022-01-13 15:15:55 +01:00
alonso.torres
9ac4239c11 🐛 Restored rules color 2022-01-13 10:09:03 +01:00
alonso.torres
da2a3b6883 🐛 Fix handoff text with opacity 2022-01-13 09:56:46 +01:00
alonso.torres
b4accaad07 🐛 Fix scroll jumps in handoff mode 2022-01-13 09:56:46 +01:00
alonso.torres
edaef0096a 🐛 Improved copy for import/export files 2022-01-13 09:56:46 +01:00
alonso.torres
afba5ff083 🐛 Fix issues when updating selrect in paths 2022-01-13 09:56:46 +01:00
alonso.torres
8b8d614150 🐛 Fixed problem with stroke 2022-01-13 09:30:21 +01:00
Alejandro Alonso
090dbfda10 Upload/dismiss all custom fonts at once 2022-01-12 20:00:24 +01:00
Alejandro Alonso
04f5a6a9f9 Show color hex or name on hover 2022-01-12 18:20:31 +01:00
Andrey Antukh
d8311ac3fa Merge pull request #1462 from penpot/adding-prettier-to-ci
 Adding prettier to ci
2022-01-12 14:25:50 +01:00
Andrey Antukh
9c7f4dfd98 Merge pull request #1452 from penpot/bugfixes
Bugfixes
2022-01-12 14:24:24 +01:00
alonso.torres
8da66e1599 🐛 Fix problem when importing a file with grids 2022-01-12 13:47:50 +01:00
alonso.torres
2927b0cfc6 🐛 Fix default page id in workspace 2022-01-12 13:30:52 +01:00
alonso.torres
4663c296cd 🐛 Fix blur input field when click on viewport 2022-01-12 13:30:52 +01:00
alonso.torres
9403f8fd6e 🐛 Fix lossing changes when changing selection and an input was already changed 2022-01-12 13:13:28 +01:00
alonso.torres
badb5c6a9b 🐛 Fix keep name of component equal to the shape name 2022-01-12 13:13:27 +01:00
alonso.torres
e5430259e9 🐛 Changing pages while comments activated will not close the panel 2022-01-12 13:12:54 +01:00
alonso.torres
50fd44d3f2 🐛 Fix division by zero in bool operation 2022-01-12 13:12:16 +01:00
Andrés Moya
a8249b73b6 🐛 Fix nav to comments in a different page 2022-01-12 13:09:11 +01:00
Andrey Antukh
a15f867059 🐛 Fix open in new tab action on dashboard. 2022-01-12 13:00:50 +01:00
Andrey Antukh
4216e2e92b Properly set host on http server. 2022-01-12 12:42:11 +01:00
Alejandro Alonso
8ef20be9bd Adding prettier to ci 2022-01-12 12:32:06 +01:00
Alejandro Alonso
6413c9dddd Apply prettier to resources styles 2022-01-12 11:49:03 +01:00
Andrey Antukh
eb10f075b9 📎 Add some excludes to uber task. 2022-01-12 11:21:25 +01:00
Andrey Antukh
cd55ed7c8d 📎 Minor improvement on css on error report template. 2022-01-12 10:55:47 +01:00
Andrey Antukh
2fb96a1b7d 📎 Add debug to ws messages. 2022-01-12 10:55:47 +01:00
Andrey Antukh
c48da3d316 ♻️ Refactor backend bundle build process.
Now the final artifact is a single, compiled uberjar.
It considerably improves startup speed.
2022-01-12 10:55:47 +01:00
Andrey Antukh
9488a9a1ad Increase default upload size on docker imates. 2022-01-12 10:55:47 +01:00
Andrey Antukh
2feb22d3bd 🐛 Fix unexpected default cookies behavior on redirectiong to penpot.
The SameSite=Strict on chrome behaves differently than in Firefox and
makes the top-level url redirect not sending cookies if the user is
redirected from other page to penpot. The SameSite=Lax fixes the issue.
2022-01-12 10:55:47 +01:00
Andrey Antukh
f74569506e ⬆️ Update devenv dockerfile. 2022-01-12 10:55:47 +01:00
Andrés Moya
6633d0b4fb Right click on frame title to open its menu 2022-01-11 16:02:50 +01:00
Andrey Antukh
6fb35b40d7 Merge pull request #1445 from penpot/add-artboard-selection
 Add artboard selection
2022-01-11 16:02:12 +01:00
Alejandro Alonso
614d699098 Add artboard selection 2022-01-11 14:03:07 +01:00
Andrés Moya
8f4fbff40f 💄 Set consistent Login label 2022-01-10 16:58:15 +01:00
Andrés Moya
aaf8d2a233 🐛 Correct animations overflow 2022-01-10 16:02:36 +01:00
Andrey Antukh
0eb2336bc6 Merge pull request #1449 from penpot/bugfixes
Bugfixes
2022-01-10 15:36:59 +01:00
Andrey Antukh
f9cc9164b3 Merge pull request #1450 from penpot/fix-interactions-header
🐛 Fix viewer header controls when navigating from workspace
2022-01-10 15:31:28 +01:00
Andrés Moya
238ec60f89 🐛 Fix viewer header controls when navigating from workspace 2022-01-10 14:10:29 +01:00
alonso.torres
363a82d068 🐛 Fix decimal numbers in export viewbox 2022-01-10 13:32:29 +01:00
alonso.torres
4360c1fe4b 🐛 Improved behaviour on text options when not text is selected 2022-01-10 13:32:29 +01:00
alonso.torres
1d575ece06 🐛 Allow import to continue from recoverable failures 2022-01-10 13:32:29 +01:00
alonso.torres
d246788a35 🐛 Fix default state in viewer 2022-01-10 13:32:29 +01:00
alonso.torres
e9fa04dd1b 🐛 Fix problem with styles in the viewer 2022-01-10 13:32:29 +01:00
alonso.torres
8e57932966 🐛 Fix problem with multiple exports 2022-01-10 13:32:29 +01:00
alonso.torres
51ea354bcb 🐛 Fix problem when resizing texts inside groups 2022-01-10 13:32:29 +01:00
alonso.torres
6334520c66 🐛 Fix dotted style in strokes 2022-01-10 13:32:29 +01:00
alonso.torres
6354883a6f 🐛 Fix line-height/letter-spacing inputs behaviour 2022-01-10 13:32:29 +01:00
alonso.torres
477f553675 🐛 Fix problem with booleans 2022-01-10 13:32:29 +01:00
Eva
1ded4b2b28 🐛 Remove gradient if any when applyin solid color from library 2022-01-10 12:33:21 +01:00
eva
16c4116c15 🐛 fix enter key as a way to exit path editing mode 2022-01-07 15:51:50 +01:00
eva
f5cfbce1c2 🐛 Fix add fill and stroke color from palette to groups and components 2022-01-07 15:51:50 +01:00
eva
7bbf98dfb1 🐛 Fix default project name in dashboard Move to option 2022-01-07 15:51:50 +01:00
eva
533cac7881 🐛 Fix text inputs to allow negative values 2022-01-07 15:51:50 +01:00
eva
6afc734e91 🐛 Fix typo in handoff tooltip 2022-01-07 15:51:50 +01:00
eva
c4fb826d89 🐛 Fix crash when pressing Shift 1 in an empty file 2022-01-07 15:51:50 +01:00
Alejandro Alonso
1321bdeac5 Add opacity shortcuts 2022-01-07 11:31:21 +01:00
Alejandro Alonso
e0b7001a09 🐛 Fix default color fill for textx 2022-01-05 11:19:25 +01:00
eva
88120b83bd change reference to issue 2022-01-03 17:29:51 +01:00
eva
a952f7369c Add move shortcut 2022-01-03 17:16:03 +01:00
eva
d4fab3b46c Add alt as mod-key to add stroke color to a shape from color in menu library 2022-01-03 16:39:40 +01:00
eva
06b3499e7d Add detach components in bulk option 2022-01-03 13:43:11 +01:00
Eva Marco
fdd66bd513 Merge pull request #1430 from penpot/bugfixes
Bugfixes
2022-01-03 10:46:22 +01:00
alonso.torres
3b5aaf21fa 🐛 Fix problem when resizing texts 2022-01-03 10:00:17 +01:00
eva
59c46833ed Add penpot look and feel to multiuser pointers 2022-01-03 09:35:46 +01:00
alonso.torres
aee35cb456 🐛 Fix lock/hide elements in context menu when multiples shapes selected 2022-01-03 09:14:58 +01:00
alonso.torres
4a55ee2965 🐛 Fix problem exporting shapes from handoff mode 2022-01-03 09:14:58 +01:00
alonso.torres
4b490e3ca4 🐛 After team onboarding importing a file will import into the team drafts 2022-01-03 09:14:58 +01:00
alonso.torres
6727717d1a 🐛 Fix problem with join nodes 2022-01-03 09:14:58 +01:00
alonso.torres
d08891cffa 🐛 Disable running frontend tests in node 2022-01-03 09:10:18 +01:00
Andrey Antukh
799a83ba73 🔥 Remove unused import. 2021-12-31 13:47:49 +01:00
Andrey Antukh
261724e555 📎 Fix common tests. 2021-12-31 13:46:13 +01:00
Andrey Antukh
10e7d660ef Merge branch 'main' into develop 2021-12-31 13:23:42 +01:00
Andrey Antukh
bdfea7cda5 📎 Update version.txt file. 2021-12-31 13:04:15 +01:00
Andrey Antukh
fdb1c5e1f9 📎 Minor changes on error report http handler. 2021-12-31 12:24:46 +01:00
alonso.torres
71734df489 Backport changes from develop. 2021-12-31 12:06:15 +01:00
Andrey Antukh
071b81eadd ⬆️ Update dependencies. 2021-12-31 00:21:38 +01:00
Andrey Antukh
2abe3fde71 Remove unused stacktrace from commit-chages event. 2021-12-31 00:02:19 +01:00
Andrey Antukh
27e64ccaa8 🔥 Remove unused code. 2021-12-30 23:53:33 +01:00
Andrey Antukh
c9185f265c Add error report list template. 2021-12-30 23:51:39 +01:00
Andrey Antukh
79e5716f36 📎 Fix linter issues. 2021-12-30 19:43:05 +01:00
Andrey Antukh
9f0e156916 Improve error reporting. 2021-12-30 19:39:32 +01:00
Andrey Antukh
d24d45f4cb Terminate connection if incoming message cant be parsed. 2021-12-30 16:28:08 +01:00
Andrey Antukh
bf55250ae9 :sparkles Minor changes on websockets error handling. 2021-12-30 16:25:50 +01:00
Andrey Antukh
36016ad9ef Store changes on file_change table as vector. 2021-12-30 16:02:06 +01:00
Andrey Antukh
bf66b81702 Move dbg error http entrypoint handler to debug ns. 2021-12-30 16:01:36 +01:00
Andrey Antukh
758ffbf217 Add authentication to dbg entry points. 2021-12-30 16:00:55 +01:00
Andrey Antukh
f24563503a Parametrice file change snapshoting. 2021-12-30 13:03:49 +01:00
alonso.torres
a2dbc40571 🐛 Fixes problem with mov-objects 2021-12-30 13:03:49 +01:00
Andrey Antukh
a096b0777f 📎 Minor changes on debug ns. 2021-12-30 13:03:28 +01:00
Andrey Antukh
87690a534c 🎉 Add missing files. 2021-12-30 11:45:44 +01:00
Andrey Antukh
a70e416b0b Add more debug handlers. 2021-12-30 11:37:09 +01:00
alonso.torres
cd1170c543 Add a debug option to apply changes 2021-12-30 11:19:46 +01:00
Andrey Antukh
2962dc1faa Merge pull request #1429 from penpot/cache-thumbnails-v2
 Cache thumbnails with browser cache
2021-12-29 13:03:40 +01:00
alonso.torres
535c1fd007 Cache thumbnails with browser cache 2021-12-29 12:59:17 +01:00
Andrey Antukh
2bd94aff0e 🐛 Fix wring metrics usage on websocket module. 2021-12-29 12:55:41 +01:00
Andrey Antukh
9ea90c3400 🐛 Fix unexpected exception on websockets. 2021-12-29 12:16:48 +01:00
alonso.torres
0ac5d85117 🐛 Rollback thumbnail cache 2021-12-29 11:40:37 +01:00
Andrey Antukh
d3a83142ae Merge pull request #1428 from penpot/performance
 Improved thumbnails handling
2021-12-29 11:33:39 +01:00
alonso.torres
d5886123d8 Improved thumbnails handling 2021-12-29 11:21:57 +01:00
Andrey Antukh
dea090e7d3 📚 Update version.txt file. 2021-12-29 11:17:55 +01:00
Andrey Antukh
ba5e345677 Merge branch 'staging' 2021-12-29 11:17:06 +01:00
Andrey Antukh
13ae7b0976 📚 Update changelog. 2021-12-29 11:16:04 +01:00
Andrey Antukh
39c7bfb49f ⬆️ Finally update the prometheus client dependency. 2021-12-29 09:52:32 +01:00
Andrey Antukh
8479a6581d Make matrix use native doubles.
Increases 300x the performance of multiply on the JVM platform.
2021-12-29 09:52:32 +01:00
Andrey Antukh
e5885e83eb Add missing type hints on Matrix multiply function. 2021-12-29 09:52:32 +01:00
Andrey Antukh
914b41fcd4 🐛 Properly handle missing log/error id on database logger. 2021-12-29 09:52:32 +01:00
Andrey Antukh
224aa5b89a 🐛 Properly handle errors on body parsing middleware. 2021-12-29 09:52:32 +01:00
Andrey Antukh
01c89f6554 🐛 Set proper return value on validation error handler. 2021-12-29 09:52:32 +01:00
Andrey Antukh
f0e1bc1d59 🔥 Remove unused code. 2021-12-29 09:52:32 +01:00
Andrey Antukh
7b487e1bc3 📎 Fix unrelated linter issues. 2021-12-29 09:52:32 +01:00
Andrey Antukh
c394495a26 ♻️ Refactor websocket layer.
This commit replaces rj9a with funcool/yetti ring adapter.
Cleans the websocket api and makes it fully asynchronous.
Also a common websocket protocol abstraction that will allow
more easy path for creating new websocket based services.
2021-12-29 09:52:32 +01:00
eva
6dae420254 Add open components, and scroll into view to show main component acction 2021-12-28 16:19:07 +01:00
Andrey Antukh
c69d7f50a3 Merge remote-tracking branch 'origin/staging' into develop 2021-12-28 13:48:16 +01:00
elhombretecla
e9c654f30d Minor enhacements on onboarding modal. 2021-12-28 11:34:21 +01:00
Andrey Antukh
ae9b95f81b Merge pull request #1424 from penpot/performance
Performance improvements
2021-12-28 11:32:55 +01:00
Andrey Antukh
c240b69b5a 📎 Minor changes on error report template. 2021-12-28 11:19:38 +01:00
alonso.torres
493a7680e0 Improve interactions and after-review fixes 2021-12-28 11:12:33 +01:00
alonso.torres
c28a2acfc7 🐛 Fixes tests 2021-12-28 09:18:33 +01:00
alonso.torres
60af960f42 Thumbnail cache on dashboard 2021-12-28 09:18:33 +01:00
alonso.torres
4c86d5cfe3 🐛 Fixes some issues with bool shapes 2021-12-28 09:18:33 +01:00
alonso.torres
99a6142134 Improves boolean performance 2021-12-28 09:18:33 +01:00
alonso.torres
b2211aec59 Change resize to use DOM transformations 2021-12-28 09:18:33 +01:00
alonso.torres
fa09fff2b5 Performance improvements 2021-12-28 09:18:33 +01:00
Andrey Antukh
0204cdab83 🐛 Revert rj9a dep update. 2021-12-28 09:15:31 +01:00
Andrey Antukh
445195e9eb ⬆️ Update dependencies. 2021-12-27 11:55:42 +01:00
Andrey Antukh
7f5b0f359c Merge remote-tracking branch 'origin/staging' into develop 2021-12-27 11:42:44 +01:00
Andrey Antukh
d8f4176487 📎 Minor fixes on versions. 2021-12-27 11:41:13 +01:00
Andrey Antukh
220ab22115 🐛 Fix error reporting hook. 2021-12-27 11:30:22 +01:00
Andrey Antukh
67776c46d6 🐛 Fix NPE on email complains checking. 2021-12-27 11:13:08 +01:00
Andrey Antukh
2d118ecc65 Merge remote-tracking branch 'origin/staging' into develop 2021-12-27 09:50:15 +01:00
Andrey Antukh
4bc2d7444d 📎 Minor changes on dev tools. 2021-12-27 09:32:20 +01:00
Andrey Antukh
5c6d72b353 Improve logging performance and format. 2021-12-24 12:40:44 +01:00
Andrey Antukh
1839397ebc Minor enhacements on log processing. 2021-12-23 18:36:58 +01:00
Andrey Antukh
0ee34637f5 💄 Improve json namespace API (and fix linter). 2021-12-23 00:06:10 +01:00
Andrey Antukh
c6054f7ab2 💄 Improve json namespace API (and fix linter). 2021-12-23 00:04:37 +01:00
Andrey Antukh
9554dfbc5e :lisptick: Cosmetic changes. 2021-12-22 19:04:32 +01:00
Andrey Antukh
98d5789b1b :lisptick: Cosmetic changes. 2021-12-22 19:04:03 +01:00
Andrey Antukh
0cad1a1e7e Merge remote-tracking branch 'origin/staging' into develop 2021-12-22 18:49:46 +01:00
Andrey Antukh
31c07274cd 📎 Increase default session expiration to 15 days. 2021-12-22 18:44:49 +01:00
Andrey Antukh
37a736339e 🔥 Remove ALPHA and BETA batges. 2021-12-22 18:41:06 +01:00
Andrey Antukh
869abcc835 🐛 Fix incorrect grid calculation when size is 1. 2021-12-22 18:38:22 +01:00
Andrey Antukh
a6f05ea8c2 💄 Minor syntax cosmetic changes. 2021-12-22 18:37:29 +01:00
Andrey Antukh
6812099900 Simplify frames selection mechanism. 2021-12-22 18:37:08 +01:00
Andrey Antukh
53e6d7ef2a 🐛 Fix numeric-input component. 2021-12-22 17:06:59 +01:00
Andrey Antukh
c2f604cd01 Properly use take-until on shape movement streams. 2021-12-22 17:06:16 +01:00
Andrey Antukh
888ffa1bcd Merge remote-tracking branch 'origin/staging' into develop 2021-12-22 16:10:24 +01:00
Andrey Antukh
d06cfed50e 🐛 Add missing import. 2021-12-22 15:01:46 +01:00
Andrey Antukh
e06d063946 📎 Remove ALPHA label from feedback button. 2021-12-22 14:59:39 +01:00
Andrey Antukh
634ec1b113 Ensure valid messages on zmq listener. 2021-12-22 14:28:09 +01:00
Andrey Antukh
0bf883d5b2 📎 More updates to logging deps. 2021-12-22 14:09:23 +01:00
Andrey Antukh
c6d0e0124f ⬆️ Update log4j2 dependency to 2.17.0 2021-12-22 11:34:07 +01:00
Andrey Antukh
ce115c53e2 📎 Minor fixes on repl script. 2021-12-22 11:33:53 +01:00
Andrey Antukh
7014bc7a3c 🐛 Fix issue when typography name is empty. 2021-12-22 11:03:11 +01:00
Andrey Antukh
219f9c478d 🎉 Add version 4 of blob encoding.
The version 4 starts using the hight performance fressian
binary encoding with very lightweight compression layer.
2021-12-21 16:19:25 +01:00
Andrey Antukh
a9904c6ada Merge pull request #1386 from penpot/animations
Animations
2021-12-21 15:47:51 +01:00
Andrés Moya
81cbc33dbb 🎉 Add animations to interactions 2021-12-21 15:46:52 +01:00
eva
24062beebe 🐛 fix minor errors 2021-12-20 16:18:33 +01:00
eva
f3548aff8c Add shortcuts to viewer btns 2021-12-20 16:18:33 +01:00
eva
771bb20976 Add shortcuts for dashboard viewer and path 2021-12-20 16:18:33 +01:00
eva
8072caeff1 Add more workspace shortcuts 2021-12-20 16:18:33 +01:00
eva
d5568fcc25 Add alignement shortcuts 2021-12-20 16:18:33 +01:00
Andrey Antukh
eb1bcfba83 🎉 Backport questions form integration.
Among other related that need to be ported.
2021-12-20 16:16:29 +01:00
Andrey Antukh
a2d3616171 📎 Update changelog. 2021-12-20 11:55:32 +01:00
Andrey Antukh
a83e37493a ⬆️ Update log4j2 dependency. 2021-12-20 11:52:32 +01:00
Andrey Antukh
0feccc9d1c ⬆️ Update log4j2 dependency. 2021-12-20 11:49:30 +01:00
Andrey Antukh
e18ecb8c49 ⬆️ Update devenv (deps). 2021-12-20 11:16:26 +01:00
Andrés Moya
f5b87a9865 📚 Add docs contributor 2021-12-19 17:56:37 +01:00
Andy Li
3b93434dd3 🌐 Add translations for: Chinese (Traditional).
Currently translated at 33.5% (283 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2021-12-18 02:51:35 +01:00
Muhammad Insan Al-Amin
d522096caf 🌐 Add translations for: Indonesian.
Currently translated at 7.3% (62 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2021-12-18 02:51:35 +01:00
Andrey Antukh
6c67110dde 🐛 Fix middleware order that prevents multipart requests. 2021-12-10 15:06:42 +01:00
Andrey Antukh
963efc369b Merge branch 'main' into develop 2021-12-10 14:45:02 +01:00
Andrey Antukh
384f0a05c6 🐛 Fix race condition issues on workspace. 2021-12-10 12:32:10 +01:00
Andrey Antukh
a3016b8400 Make the media uploading idempotent. 2021-12-10 12:19:12 +01:00
Andrey Antukh
0df219c3ad 📎 Fix frontend tests. 2021-12-10 12:00:29 +01:00
Andrey Antukh
a0d527f795 📎 Add some minor changes to package.json scripts section. 2021-12-10 12:00:29 +01:00
Andrey Antukh
e44ea47497 🐛 Fix issue when typography name is empty. 2021-12-10 12:00:29 +01:00
Andrey Antukh
9ee5a3159c 💄 Add cosmetic changes on ungroup event. 2021-12-10 12:00:29 +01:00
Andrey Antukh
06d41c552b Simplify debuging information used on commit-changes. 2021-12-10 12:00:29 +01:00
Andrey Antukh
7874971550 🐛 Fix race condition issues on workspace. 2021-12-10 12:00:29 +01:00
Andrey Antukh
9925716134 💄 Add syntax improvements on libraries helpers. 2021-12-10 12:00:29 +01:00
Andrey Antukh
64c456678b Merge pull request #1401 from penpot/fix-destination
🐛 Fix error importing file with null destination in one interaction
2021-12-10 11:21:27 +01:00
Andrés Moya
16ed09a303 🐛 Fix error importing file with null destination in one interaction 2021-12-10 10:50:18 +01:00
Andy Li
1359a1aa7a 🌐 Add translations for: Chinese (Traditional).
Currently translated at 31.4% (265 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2021-12-10 09:52:18 +01:00
Andy Li
6ae36982b6 🌐 Add translations for: Chinese (Traditional).
Currently translated at 30.6% (258 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2021-12-09 05:50:27 +01:00
Maemolee
136d269605 🌐 Add translations for: Chinese (Simplified).
Currently translated at 80.6% (680 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2021-12-09 05:50:27 +01:00
Rubén
932c0ed4ad 🌐 Add translations for: Spanish.
Currently translated at 98.5% (831 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2021-12-09 05:50:26 +01:00
Andy Li
371875440f 🌐 Add translations for: Chinese (Traditional).
Currently translated at 25.7% (217 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2021-12-07 09:12:57 +01:00
Rubén
b01a9f2f95 🌐 Add translations for: Catalan.
Currently translated at 99.6% (840 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2021-12-06 12:26:52 +01:00
李安峻
0d2def102f 🌐 Add translations for: Chinese (Traditional).
Currently translated at 23.4% (198 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2021-12-04 03:52:36 +01:00
Andrey Antukh
beff3fe843 📎 Fix common tests. 2021-12-03 16:00:56 +01:00
Andrey Antukh
7a97c94f2b Merge branch 'main' into develop 2021-12-03 15:53:20 +01:00
Andrey Antukh
ce81908f02 📎 Minor changes on devenv. 2021-12-03 15:05:36 +01:00
Andrey Antukh
f8cecfd61f 🐛 Fix unexpected behavior of grid options on right sidebar. 2021-12-03 14:52:40 +01:00
Andrey Antukh
8a2a1d6d70 ♻️ Ensure a correct usage of concat/into operations. 2021-12-03 14:52:40 +01:00
Andrey Antukh
76dafea8a6 Add the abilty to enable verbose output using query param (api). 2021-12-03 11:13:08 +01:00
Andrey Antukh
86bbfde19e 📎 More fixes on ci config. 2021-12-03 11:09:01 +01:00
Andrey Antukh
71d6f7b1a2 📎 Fix linter issues. 2021-12-03 09:43:02 +01:00
Andrey Antukh
0c0ab612c0 📎 Minor fixes to previous commits. 2021-12-03 09:36:39 +01:00
Andrey Antukh
73042115e0 🎉 Add benchmark helper function (cljs only). 2021-12-03 09:30:33 +01:00
Andrey Antukh
0f7166d34a 📎 More fixes on CI. 2021-12-02 18:20:45 +01:00
Andrey Antukh
f35f2c95f0 📎 Fix issues with jvm on CI. 2021-12-02 18:15:44 +01:00
Andrey Antukh
4d280bdb6d Merge branch 'test-e2e' into develop 2021-12-01 16:14:46 +01:00
Andrey Antukh
47acab766d ⬆️ Update to OpenJDK17. 2021-12-01 16:13:46 +01:00
Andrés Moya
1cc3819e65 🧪 Add e2e tests with Cypress 2021-12-01 16:13:46 +01:00
Andrey Antukh
16fa6259ea 🚧 Update docker deps and start working on test coverage. 2021-12-01 16:13:46 +01:00
Andrey Antukh
95717c4c32 Merge pull request #1378 from penpot/performance
Performance Improvements
2021-12-01 14:43:43 +01:00
alonso.torres
7564f27f95 Improvements after review 2021-12-01 14:39:20 +01:00
alonso.torres
565046aaa6 Memoize transform-shape 2021-12-01 14:39:20 +01:00
alonso.torres
fb9b023fae Improve selection performance 2021-12-01 14:39:20 +01:00
alonso.torres
b05908a760 Improved performance for options and area selection 2021-12-01 14:39:20 +01:00
alonso.torres
3bbcd235e1 Improved selection rect calculation 2021-12-01 14:39:20 +01:00
alonso.torres
9d66984c62 Improved set-modifiers-recursive and some utils memoization 2021-12-01 14:39:20 +01:00
alonso.torres
9024408ed2 Improved frame defered handling 2021-12-01 14:39:20 +01:00
alonso.torres
2b32e864fd Performance improvements 2021-12-01 14:39:20 +01:00
alonso.torres
626d0cba46 🐛 Add to spec opacity and blend-mode 2021-12-01 14:39:20 +01:00
alonso.torres
2a11e9962d ♻️ Moved debug utils to debug namespace 2021-12-01 14:39:20 +01:00
Andrés Moya
7dffddd437 📖 Add doc contributors to CHANGES.md 2021-12-01 12:48:48 +01:00
Andrey Antukh
6a7600fd52 ♻️ Ensure a correct usage of concat/into operations. 2021-12-01 11:30:48 +01:00
Andrey Antukh
b897f202dd 📎 Minor changes on exporter shadow-cljs config. 2021-11-30 13:11:52 +01:00
Andrey Antukh
eb396f2367 🐛 Properly show message on password recovery request.
Additionally, add the ability to reset form passing `nil`
on `reset!` function of Atom interface.
2021-11-30 09:44:03 +01:00
Andrey Antukh
95bf3e3af4 🐛 Fix issues related to signup questions form. 2021-11-30 09:44:03 +01:00
Andrey Antukh
19944202fb Handle properly the user redirect after login. 2021-11-30 09:44:03 +01:00
Andrey Antukh
2596ad27c3 ♻️ Minor refactor of auth data-flow.
This fixes many issues related to using penpot on-premise
instances on different domain than localhost. This changes
ensures correct data flow of authenticated and not authenticated
sessions.
2021-11-30 09:44:03 +01:00
Andrey Antukh
ece914303a ⬆️ Update exporter dependencies. 2021-11-29 15:53:24 +01:00
eva
7a0c12e073 ♻️ Create a colors file to save constants of color that can no be refactored into sass variables 2021-11-29 11:21:12 +01:00
eva
14b23b491f use those new css variables in our cljs files 2021-11-29 11:21:12 +01:00
eva
039b03249b Create css variables fron our sass varibles to use them in cljs files 2021-11-29 11:21:12 +01:00
Andrés Moya
3919cf4f86 🐛 Fix resize behavior of masked groups 2021-11-25 15:53:09 +01:00
Yaron Shahrabani
319a9fd2de 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (843 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2021-11-25 14:50:58 +01:00
Oğuz Ersen
cf1f9f93aa 🌐 Add translations for: Turkish.
Currently translated at 99.7% (841 of 843 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2021-11-25 14:50:57 +01:00
Andrey Antukh
0dd805da7f Improve team deletion process. 2021-11-25 14:19:06 +01:00
Andrey Antukh
e7a1833c44 🐛 Add missing "solid" font-weight parsing style.
Related issue: #1338
2021-11-25 14:19:06 +01:00
Andrey Antukh
54f8487b46 Make team leave & reassing as atomic operation. 2021-11-25 14:19:06 +01:00
alonso.torres
b68d721b39 🐛 Fix problem when importing in shared libraries 2021-11-25 13:28:37 +01:00
Andrey Antukh
b9ccb4e52c 🐛 Fix issues on login being logged. 2021-11-25 13:24:40 +01:00
Andrey Antukh
c4a11f73a0 ⬆️ Update deps. 2021-11-25 13:24:40 +01:00
Andrey Antukh
f96d4198c3 📎 Add scripts/repl to common. 2021-11-25 13:24:40 +01:00
Andrey Antukh
fe6a0ec5b8 📎 Fix linter issues. 2021-11-25 13:24:40 +01:00
Andrey Antukh
e7b4010eba 🐛 Fix undo on page relocate/sorting. 2021-11-25 13:24:40 +01:00
elhombretecla
c4947d3737 Add basic css sf structure 2021-11-25 13:24:40 +01:00
Andrey Antukh
8a8d677f85 🎉 Integrate questions form. 2021-11-25 13:24:40 +01:00
eva
baf4393310 🔨 Fix some spanish terms 2021-11-24 16:42:48 +01:00
eva
723916d930 Allow ungroup groups in bulk and unmask mask in group 2021-11-24 16:42:48 +01:00
Andrey Antukh
591d66564d 📎 Set develop branch to version 1.11.0-beta. 2021-11-24 13:23:36 +01:00
Andrey Antukh
79a2d522bf Merge remote-tracking branch 'origin/main' into develop 2021-11-24 13:23:14 +01:00
Andrey Antukh
4ad34ab5c8 📎 Update version number. 2021-11-24 13:06:36 +01:00
Andrey Antukh
33c7847dfc 🐛 Fix team deletion flow on dashboard. 2021-11-24 13:05:54 +01:00
alonso.torres
7a04f15710 🐛 Fix problems with team management. 2021-11-24 13:05:48 +01:00
Andrey Antukh
7e5b10eb3e 🐛 Fix team deletion flow on dashboard. 2021-11-24 12:45:06 +01:00
alonso.torres
896a07fa9a 🐛 Fix problems with team management 2021-11-24 12:27:14 +01:00
Andrey Antukh
07e8bb00fb Improve debug information of oidc provider. 2021-11-23 16:41:31 +01:00
Andrey Antukh
5d2742dd37 📎 Sort translation files. 2021-11-23 14:29:09 +01:00
Andrey Antukh
9ae3f1eb68 Merge remote-tracking branch 'weblate/develop' into translations 2021-11-23 14:27:29 +01:00
Andrey Antukh
8c6e0cf43a Merge branch 'jsoref-spelling' into develop 2021-11-23 14:23:29 +01:00
Andrey Antukh
1e220fd506 📎 Update changes.md file. 2021-11-23 14:22:46 +01:00
Andrey Antukh
4ff7855fd4 📎 Adapt translation files to spelling fixes. 2021-11-23 14:21:05 +01:00
Andrey Antukh
eb57354109 📎 Adapt migrations file. 2021-11-23 14:19:48 +01:00
Andrey Antukh
a82a33cecf Merge branch 'spelling' of https://github.com/jsoref/penpot into jsoref-spelling 2021-11-23 14:02:16 +01:00
Andrés Moya
c90fc2a9bf :white-check-mark: Add tests for interaction model and some fixes 2021-11-23 13:58:43 +01:00
Paul Schulz
c1a40e4aeb Add preset for ReMarkable 2 screen size
Logged in issue: https://tree.taiga.io/project/penpot/issue/2298

This page size (840x1120)(for the ReMarkable2 was tested with an exported PDF
test page, and does not appear to correspond directly to the resolution of the
device.

Signed-off-by: Paul Schulz <paul@mawsonlakes.org>
2021-11-23 13:57:26 +01:00
Andrey Antukh
9999b8bfab 📎 Update email on contributing file. 2021-11-22 15:42:50 +01:00
Andrey Antukh
cf62008acf Merge remote-tracking branch 'origin/main' into develop 2021-11-22 08:59:48 +01:00
Andrey Antukh
1c959a6653 📎 Add commented keycloak docker compose entry. 2021-11-22 08:57:20 +01:00
Andrey Antukh
b8043a2432 📎 Update ci config. 2021-11-18 17:19:55 +01:00
Andrey Antukh
ed5de525aa 📎 Increase default db pool size to 50. 2021-11-18 17:19:55 +01:00
Andrey Antukh
8105d9388b ♻️ Refactor rlimit usage (backend). 2021-11-18 17:19:55 +01:00
Andrey Antukh
8151dcc05f 📎 Improve services defmethod linter hook. 2021-11-18 17:19:55 +01:00
Andrés Moya
f566d2a0da 🐛 Remove unneeded style causing visual glitch 2021-11-17 14:06:15 +01:00
Andrey Antukh
0be2b2791f ♻️ Refactor error handling on exporter browser module. 2021-11-17 10:09:36 +01:00
Andrey Antukh
bf51e3db60 Invalidate all other sessions on password change. 2021-11-17 10:09:36 +01:00
Andrey Antukh
abca69f408 🐛 Fix tab reuse issue (viewer <-> workspace). 2021-11-17 10:09:36 +01:00
Andrey Antukh
6eac9102c9 🐛 Prevent conflict errors just ignoring repeated requests.
On media image uploading.
2021-11-17 10:09:36 +01:00
Andrey Antukh
0a7da1b7f2 🔥 Remove unused var. 2021-11-17 10:09:36 +01:00
Andrey Antukh
b4361cb202 📎 Increase idle-in-transaction default timeout. 2021-11-17 10:09:36 +01:00
Andrey Antukh
d2d4090e27 🐛 Don't raise exception when profile is not found. 2021-11-17 10:09:36 +01:00
Josh Soref
583eb53c9d 🐛 Fix typos 2021-11-15 09:54:36 -05:00
Josh Soref
39246f2beb 🐛 Fix typos in common 2021-11-15 09:53:42 -05:00
Josh Soref
cd2d3d5fa3 🐛 Fix typos in backend 2021-11-15 09:53:10 -05:00
Josh Soref
589e646023 🐛 Fix typos in frontend 2021-11-15 09:51:34 -05:00
Maciek Baron
b7ba3098ae 🐛 Fix typo in workspace.scss
Correct the spelling of 'width' to ensure it is set to '100%' properly and does not default to 'auto'.
2021-11-15 12:50:52 +01:00
Andrés Moya
631c5ecae3 💄 Change word in german for a shorter one 2021-11-15 12:50:07 +01:00
Andrés Moya
4962e45bd9 Change tool for exporting artboards and fix some issues 2021-11-15 11:55:58 +01:00
Andrey Antukh
c57219a356 📎 Allow specify the build optimization leven using env vars. 2021-11-12 13:31:28 +01:00
Andrey Antukh
03e6a187c5 Merge branch 'main' into develop 2021-11-12 13:31:13 +01:00
Andrey Antukh
66b0039566 📎 Fix linter issues. 2021-11-12 12:36:32 +01:00
Andrey Antukh
17da51440c 📎 Change loggling level on oauth ns. 2021-11-12 12:34:50 +01:00
曹恩逢
40326177fd 🌐 Add translations for: Chinese (Traditional).
Currently translated at 23.2% (192 of 826 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2021-11-01 04:34:53 +01:00
Yaron Shahrabani
4ab0272fa6 🌐 Add translations for: Hebrew.
Currently translated at 99.8% (825 of 826 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2021-11-01 04:34:52 +01:00
Marius
fb33366c91 🌐 Add translations for: German.
Currently translated at 99.3% (821 of 826 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2021-11-01 04:34:51 +01:00
Oğuz Ersen
75352c9afe 🌐 Add translations for: Turkish.
Currently translated at 99.7% (824 of 826 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2021-11-01 04:34:50 +01:00
Yaron Shahrabani
bfb30fe68d 🌐 Add translations for: Hebrew.
Currently translated at 95.8% (792 of 826 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2021-10-27 11:49:02 +02:00
760 changed files with 63344 additions and 26001 deletions

View File

@@ -2,20 +2,13 @@ version: 2
jobs:
build:
docker:
# specify the version you desire here
- image: penpotapp/devenv:latest
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/postgres:9.4
- image: circleci/postgres:13.3-ram
- image: cimg/postgres:13.5
environment:
POSTGRES_USER: penpot_test
POSTGRES_PASSWORD: penpot_test
POSTGRES_DB: penpot_test
- image: circleci/redis:6.0.8
- image: cimg/redis:6.2.6
working_directory: ~/repo
@@ -36,17 +29,30 @@ jobs:
- run:
name: common lint
working_directory: "./common"
command: "clj-kondo --parallel --lint src/"
command: |
clj-kondo --version
clj-kondo --parallel --lint src/
- run:
name: frontend lint
working_directory: "./frontend"
command: "clj-kondo --parallel --lint src/"
command: |
clj-kondo --version
clj-kondo --parallel --lint src/
- run:
name: frontend styles prettier
working_directory: "./frontend"
command: |
yarn install
yarn run lint-scss
- run:
name: backend lint
working_directory: "./backend"
command: "clj-kondo --parallel --lint src/"
command: |
clj-kondo --version
clj-kondo --parallel --lint src/
# run backend test
- run:
@@ -68,21 +74,27 @@ jobs:
node target/tests.js
environment:
JAVA_HOME: /usr/lib/jvm/openjdk16
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin
# - run:
# working_directory: "./common"
# name: common tests (cljs)
# command: |
# yarn install
# yarn run compile-test
# node target/test.js
#
# environment:
# PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin
- run:
working_directory: "./common"
name: common tests
name: common tests (clj)
command: |
yarn install
clojure -M:dev:shadow-cljs compile test
node target/tests.js
clojure -X:dev:test
environment:
JAVA_HOME: /usr/lib/jvm/openjdk16
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin
- save_cache:
paths:

View File

@@ -1,15 +1,19 @@
{:lint-as
{promesa.core/let clojure.core/let
promesa.core/->> clojure.core/->>
promesa.core/-> clojure.core/->
rumext.alpha/defc clojure.core/defn
rumext.alpha/fnc clojure.core/fn
app.common.data/export clojure.core/def
app.db/with-atomic clojure.core/with-open}
app.db/with-atomic clojure.core/with-open
app.common.data.macros/get-in clojure.core/get-in
app.common.data.macros/select-keys clojure.core/select-keys
app.common.logging/with-context clojure.core/do}
:hooks
{:analyze-call
{app.common.data/export hooks.export/export
{app.common.data.macros/export hooks.export/export
potok.core/reify hooks.export/potok-reify
cljs.core/specify! hooks.export/clojure-specify
app.util.services/defmethod hooks.export/service-defmethod
}}

View File

@@ -51,18 +51,26 @@
(defn service-defmethod
[{:keys [:node]}]
(let [[rnode rtype & other] (:children node)
(let [[rnode rtype ?meta & other] (:children node)
rsym (gensym (name (:k rtype)))
result (api/list-node
[(api/token-node (symbol "do"))
(api/list-node
[(api/token-node (symbol "declare"))
(api/token-node rsym)])
(if (= :map (:tag ?meta))
(api/list-node
[(api/token-node (symbol "reset-meta!"))
(api/token-node rsym)
?meta])
(api/list-node
[(api/token-node (symbol "comment"))
(api/token-node rsym)]))
(api/list-node
(into [(api/token-node (symbol "defmethod"))
(api/token-node rsym)
rtype]
other))])]
(cons ?meta other)))])]
;; (prn "==============" rtype (into {} ?meta))
;; (prn (api/sexpr result))
{:node result}))

View File

@@ -8,49 +8,48 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Actual behavior**
A clear and concise description of what happens instead; what the bug is.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: (e.g. iOS)
- Browser (e.g. chrome, safari)
- Version (e.g. 22)
- OS (e.g. iOS):
- Browser & version (e.g. Chrome 89.0):
**Smartphone (please complete the following information):**
- Device: (e.g. iPhone6)
- OS: (e.g. iOS8.1)
- Browser (e.g. stock browser, safari)
- Version (e.g. 22)
- Device & model (e.g. iPhone 6):
- OS & version (e.g. iOS 8.1):
- Browser & version (e.g. stock browser 22):
**Environment (please complete the following information):**
Specify if using SAAS (https://design.penpot.app) or self-hosted instance.
- Host (e.g. https://design.penpot.app, local instance):
If self-hosted instance, add OS and runtime information to help explain your problem.
*If self-hosted:*
- OS Version (e.g. Ubuntu 16.04):
- Docker / Docker-compose version (e.g. Docker version 18.03.0-ce, build 0520e24):
- Image version (e.g. Alpine):
- OS Version: (e.g. Ubuntu 16.04)
Docker commands or docker-compose file (if possible and if proceed.x):
```
Also provide Docker commands or docker-compose file if possible and if proceed.x
- Docker / Docker-compose Version: (e.g. Docker version 18.03.0-ce, build 0520e24)
- Image (e.g. alpine)
**Frontend Stack Trace (if self-hosted)**
```
Frontend Stack Trace:
<details>
```
@@ -59,8 +58,7 @@ Also provide Docker commands or docker-compose file if possible and if proceed.x
</details>
**Backend Stack Trace (if self-hosted)**
Backend Stack Trace:
<details>
```
@@ -69,5 +67,6 @@ Also provide Docker commands or docker-compose file if possible and if proceed.x
</details>
**Additional context**
Add any other context about the problem here.
**Additional context:**
Any other context about the problem.

75
.gitignore vendored
View File

@@ -1,45 +1,54 @@
figwheel_server.log
*jar
*-init.clj
*.jar
*.penpot
*.orig
.calva
.clj-kondo
.cpcache
.lein-deps-sum
.lein-failures
.lein-repl-history
.lein-plugins/
.repl
.lein-repl-history
.lsp
.nrepl-port
.cpcache
.nyc_output
.rebel_readline_history
/vendor/**/target
/cd.md
node_modules
/backend/target/
/backend/resources/public/media
/backend/resources/public/assets
.repl
/.clj-kondo/.cache
/_dump
/backend/-
/backend/assets/
/backend/dist/
/backend/logs/
/backend/-
/telemetry/
/frontend/npm-debug.log
/frontend/target/
/frontend/dist/
/frontend/out/
/frontend/.shadow-cljs
/frontend/resources/public/*
/frontend/resources/fonts/experiments
/exporter/target
/exporter/.shadow-cljs
/docker/images/bundle*
/common/.shadow-cljs
/common/target
/.clj-kondo/.cache
/backend/resources/public/assets
/backend/resources/public/media
/backend/target/
/bundle*
/media
/cd.md
/clj-profiler/
/common/.shadow-cljs
/common/coverage
/common/target
/deploy
/web
/_dump
/docker/images/bundle*
/exporter/.shadow-cljs
/exporter/target
/frontend/.shadow-cljs
/frontend/package-lock.json
/frontend/cypress/videos/*/
/frontend/cypress/fixtures/validuser.json
/frontend/dist/
/frontend/npm-debug.log
/frontend/out/
/frontend/resources/fonts/experiments
/frontend/resources/public/*
/frontend/target/
/frontend/cypress/videos/*/
/media
/telemetry/
/vendor/**/target
/vendor/svgclean/bundle*.js
.calva
.clj-kondo
.lsp
/web
clj-profiler/
figwheel_server.log
node_modules

View File

@@ -9,6 +9,401 @@
### :heart: Community contributions by (Thank you!)
## 1.13.5-beta
### :bug: Bugs fixed
- Fix orientation artboard preset not working with differently sized artboards [Taiga #3548](https://tree.taiga.io/project/penpot/issue/3548)
## 1.13.4-beta
### :bug: Bugs fixed
- Fix undo when drawing curves [Taiga #3523](https://tree.taiga.io/project/penpot/issue/3523)
- Fix issue with text edition and certain fonts (WorkSans, Raleway, ...) and foreign objects [Taiga #3521](https://tree.taiga.io/project/penpot/issue/3521)
- Fix thumbnail generation when concurrent edition [Taiga #3522](https://tree.taiga.io/project/penpot/issue/3522)
- Fix environment imporot for exporter in Docker
- Fix auto scroll layers in Firefox [Taiga #3531](https://tree.taiga.io/project/penpot/issue/3531)
- Fix base background not visible for imported SVG
## 1.13.3-beta
### :bug: Bugs fixed
- Fix docker dependencies
- Sets invitations expirations to 7 days
- Add safety measure for text positions
- Fix old texts with opacity and no fill
- Remove default font on team change
- Fix github auth without name
- Fix problems with font loading in Firefox 95
## 1.13.2-beta
### :bug: Bugs fixed
- Improved performance when out of focus mode
- Improved performance for thumbnail generation
- Fix problem with out of sync thumbnails
## 1.13.1-beta
### :bug: Bugs fixed
- Fix problem with text positioning
- Fix issue with thumbnail generation before fonts loading
- Fix unable to hide artboards
- Fix problem with fonts cache causing hanging in certain pages
## 1.13.0-beta
### :boom: Breaking changes
- We've changed the behaviour of the border-radius so it works as CSS that [has some limits](https://www.w3.org/TR/css-backgrounds-3/#corner-overlap).
- Now exported text are SVG's native `text` tag instead of paths. This could break when opening the file depending on your engine. Some SVG's may require fonts to be installed at system level.
### :sparkles: New features
- Search and filter layers [Taiga #2564](https://tree.taiga.io/project/penpot/us/2564)
- Exporting big files flow [Taiga #2218](https://tree.taiga.io/project/penpot/us/2218)
- Multiexport from main menu [Taiga #520](https://tree.taiga.io/project/penpot/us/28541)
- Multiexport assets (aka bulk export) [Taiga #520](https://tree.taiga.io/project/penpot/us/520)
- Set the artboard layer fixed at the top side of the layers [Taiga #2636](https://tree.taiga.io/project/penpot/us/2636)
- Set an artboard as the file thumbnail [Taiga #1526](https://tree.taiga.io/project/penpot/us/1526)
- Social login redesign [Taiga #2974](https://tree.taiga.io/project/penpot/task/2974)
- Add border radius to artboards [Taiga #2056](https://tree.taiga.io/project/penpot/us/2056)
- Allow send multiple team invitations at once [Taiga #2798](https://tree.taiga.io/project/penpot/us/2798)
- Persist color palette and color picker across refresh [Taiga #1660](https://tree.taiga.io/project/penpot/issue/1660)
- Ability to add multiple strokes to a shape [Taiga #2778](https://tree.taiga.io/project/penpot/us/2778)
- Scroll to selected size in font size selector [Taiga #2825](https://tree.taiga.io/project/penpot/us/2825)
- Add new invitations section [Taiga #2797](https://tree.taiga.io/project/penpot/us/2797)
- Ability to add multiple fills to a shape [Taiga #1394](https://tree.taiga.io/project/penpot/us/1394)
- Team members redesign [Taiga #2283](https://tree.taiga.io/project/penpot/us/2283)
- New focus mode in workspace [Taiga #2748](https://tree.taiga.io/project/penpot/us/2748)
- Changed text shapes to be displayed as natives SVG text elements [Taiga #2759](https://tree.taiga.io/project/penpot/us/2759)
- Texts now can have strokes, multiple fills and can be used as masks
- Add the ability to specify the attribute for retrieve the email on OIDC integration [#1460](https://github.com/penpot/penpot/issues/1460)
- Allow registration with invitation token when registration is disabled
- Add the ability to disable standard, password login [Taiga #2999](https://tree.taiga.io/project/penpot/us/2999)
- Don't stop SVG import when an image cannot be imported [#1531](https://github.com/penpot/penpot/issues/1531)
- Show Penpot color in Safari tab bar [#1803](https://github.com/penpot/penpot/issues/1803)
- Added option to disable snap to pixel and improved behaviour for sub-pixel drawing [#2552](https://tree.taiga.io/project/penpot/us/2552)
- Delete guides while supr on hover [#2823](https://tree.taiga.io/project/penpot/us/2823)
- Opt-in subscription on on-premise instances [#2772](https://tree.taiga.io/project/penpot/us/2772)
- Optimizations in frame thumbnails [#3147](https://tree.taiga.io/project/penpot/us/3147)
### :bug: Bugs fixed
- Fix typo in viewer comment section [Taiga #3401](https://tree.taiga.io/project/penpot/issue/3401)
- Do not show team-up modal for users already on a team [Taiga #3311](https://tree.taiga.io/project/penpot/issue/3311)
- Constraints are not well assigned when default and multiselection [Taiga #3069](https://tree.taiga.io/project/penpot/issue/3069)
- Duplicate artboards create new flows if needed [Taiga #2221](https://tree.taiga.io/project/penpot/issue/2221)
- Round the size values on handoff to two decimals [Taiga #3227](https://tree.taiga.io/project/penpot/issue/3227)
- Fix paste shapes while editing text [Taiga #2396](https://tree.taiga.io/project/penpot/issue/2396)
- Fix blend modes ignored in component updates [Taiga #2626](https://tree.taiga.io/project/penpot/issue/2626)
- Fix internal error when hoverin over shape [Taiga #3237](https://tree.taiga.io/project/penpot/issue/3237)
- Fix mouse leave in handoff close overlay animation breaks [Taiga #3173](https://tree.taiga.io/project/penpot/issue/3173)
- Fix different behaviour during image drag [Taiga #2279](https://tree.taiga.io/project/penpot/issue/2279)
- Fix hidden file name on import [Taiga #3172](https://tree.taiga.io/project/penpot/issue/3172)
- Fix unneccessary scrollbars at the color list [Taiga #3211](https://tree.taiga.io/project/penpot/issue/3211)
- "Show in exports" is showing in multiselections [Taiga #3194](https://tree.taiga.io/project/penpot/issue/3194)
- Edit file name navigates to the file workspace [Taiga #3183](https://tree.taiga.io/project/penpot/issue/3183)
- Fix scroll into view behind fixed element [Taiga #3170](https://tree.taiga.io/project/penpot/issue/3170)
- Fix sidebar icon in viewer mode [Taiga #3184](https://tree.taiga.io/project/penpot/issue/3184)
- Fix send to back several shapes at a time [Taiga #3077](https://tree.taiga.io/project/penpot/issue/3077)
- Fix duplicate multi selected elements [Taiga #3155](https://tree.taiga.io/project/penpot/issue/3155)
- Fix add fills to artboard modify children [Taiga #3151](https://tree.taiga.io/project/penpot/issue/3151)
- Avoid numeric inputs to allow big numbers [Taiga #2858](https://tree.taiga.io/project/penpot/issue/2858)
- Fix component contex menu size [Taiga #2480](https://tree.taiga.io/project/penpot/issue/2480)
- Add shadow to artboard make it lose the fill [Taiga #3139](https://tree.taiga.io/project/penpot/issue/3139)
- Avoid numeric inputs to change its value without focusing them [Taiga #3140](https://tree.taiga.io/project/penpot/issue/3140)
- Fix comments modal when changing pages [Taiga #2597](https://tree.taiga.io/project/penpot/issue/2508)
- Copy paste inside a text layer leaves pasted text transparent [Taiga #3096](https://tree.taiga.io/project/penpot/issue/3096)
- On dashboard enter on empty search refresh the page [Taiga #2597](https://tree.taiga.io/project/penpot/issue/2597)
- Pencil cursor changes when activated [Taiga #2276](https://tree.taiga.io/project/penpot/issue/2276)
- Fix icon placement in Mixed message [Taiga #3037](https://tree.taiga.io/project/penpot/issue/3037)
- Fix scroll in comment section [Taiga #3068](https://tree.taiga.io/project/penpot/issue/3068)
- Remove a decimal sets value to 0 [Taiga #3059](https://tree.taiga.io/project/penpot/issue/3054)
- Go to style library file to edit in a new tab [Taiga #2639](https://tree.taiga.io/project/penpot/issue/2639)
- Inner shadow with border not working properly [Taiga #2883](https://tree.taiga.io/project/penpot/issue/2883)
- Fix ellipsis in long page names [Taiga #2962](https://tree.taiga.io/project/penpot/issue/2962)
- Fix color palette animation [Taiga #2852](https://tree.taiga.io/project/penpot/issue/2852)
- Fix display code icon on preview hover [Taiga #2838](https://tree.taiga.io/project/penpot/us/2838)
- Fix crash on iOS when displaying viewer [#1522](https://github.com/penpot/penpot/issues/1522)
- Fix problem when importing a SVG with text [#1532](https://github.com/penpot/penpot/issues/1532)
- Fix problem when adding shadows to imported text [#Taiga 3057](https://tree.taiga.io/project/penpot/issue/3057)
- Fix problem when importing SVG's with uses with overriding properties [#Taiga 2884](https://tree.taiga.io/project/penpot/issue/2884)
- Fix inconsistency with radius in SVG an CSS [#1587](https://github.com/penpot/penpot/issues/1587)
- Fix clickable area in layers [#1680](https://github.com/penpot/penpot/issues/1680)
- Fix problems with trackpad zoom and scroll in MacOS [#1161](https://github.com/penpot/penpot/issues/1161)
- Fix problem with copy/paste in Safari [#1209](https://github.com/penpot/penpot/issues/1209)
- Fix paste ordering for frames not being respected [Taiga #3097](https://tree.taiga.io/project/penpot/issue/3097)
- Improved command support for MacOS [Taiga #2789](https://tree.taiga.io/project/penpot/issue/2789)
- Fix shift+2 shortcut in MacOS with non-english keyboards [Taiga #3038](https://tree.taiga.io/project/penpot/issue/3038)
- Some fixes to SVG imports [Taiga #3122](https://tree.taiga.io/project/penpot/issue/3122) [#1720](https://github.com/penpot/penpot/issues/1720) [Taiga #2884](https://tree.taiga.io/project/penpot/issue/2884)
- Fix drag guides to delete target area [#1679](https://github.com/penpot/penpot/issues/1679)
- Fix undo when rotating groups [Taiga #3136](https://tree.taiga.io/project/penpot/issue/3136)
- Fix component name in sidebar widget [Taiga #3144](https://tree.taiga.io/project/penpot/issue/3144)
- Fix resize rotated shape with top&down constraints [Taiga #3167](https://tree.taiga.io/project/penpot/issue/3167)
- Fix multi user not working [Taiga #3195](https://tree.taiga.io/project/penpot/issue/3195)
- Fix guides are not duplicated with the artboard [Taiga #3072](https://tree.taiga.io/project/penpot/issue/3072)
- Fix problem when changing group size with decimal values [Taiga #3203](https://tree.taiga.io/project/penpot/issue/3203)
- Fix error when drawing curves with only one point [Taiga #3282](https://tree.taiga.io/project/penpot/issue/3282)
- Fix issue with paste ordering sometimes not being respected [Taiga #3268](https://tree.taiga.io/project/penpot/issue/3268)
- Fix problem when export/importing guides attached to frame [#1838](https://github.com/penpot/penpot/issues/1838)
- Fix problem when resizing a group with texts with auto-width/height [#3171](https://tree.taiga.io/project/penpot/issue/3171)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.12.4-beta
### :bug: Bugs fixed
- Fix crash on iOS when displaying viewer [#1522](https://github.com/penpot/penpot/issues/1522)
- Fix problems with trackpad zoom and scroll in MacOS [#1161](https://github.com/penpot/penpot/issues/1161)
- Fix problem with copy/paste in Safari [#1209](https://github.com/penpot/penpot/issues/1209)
- Improved command support for MacOS [Taiga #2789](https://tree.taiga.io/project/penpot/issue/2789)
- Fix shift+2 shortcut in MacOS with non-english keyboards [Taiga #3038](https://tree.taiga.io/project/penpot/issue/3038)
## 1.12.3-beta
### :bug: Bugs fixed
- Fix issue with shift+select to deselect shapes [Taiga #3154](https://tree.taiga.io/project/penpot/issue/3154)
- Fix issue with drag-select shapes [Taiga #3165](https://tree.taiga.io/project/penpot/issue/3165)
- Fix issue on password persistence after registration process on private instances
## 1.12.2-beta
### :bug: Bugs fixed
- Fix issue with guides over shape handlers [Taiga #3032](https://tree.taiga.io/project/penpot/issue/3032)
- Fix problem with shift+ctrl+click to select [#1671](https://github.com/penpot/penpot/issues/1671)
- Fix ellipsis in long page names [Taiga #2962](https://tree.taiga.io/project/penpot/issue/2962)
## 1.12.1-beta
### :bug: Bugs fixed
- Fix length of names in sidebar [Taiga #2962](https://tree.taiga.io/project/penpot/issue/2962)
- Fix issues on loki integration
## 1.12.0-beta
### :boom: Breaking changes
### :sparkles: New features
- Open feedback in a new window [Taiga #2901](https://tree.taiga.io/project/penpot/us/2901)
- Improve usage of file menu [Taiga #2853](https://tree.taiga.io/project/penpot/us/2853)
- Rotation to snap to 15º intervals with shift [Taiga #2437](https://tree.taiga.io/project/penpot/issue/2437)
- Support border radius and stroke properties for images [Taiga #497](https://tree.taiga.io/project/penpot/us/497)
- Disallow using same password as user email [Taiga #2454](https://tree.taiga.io/project/penpot/us/2454)
- Add configurable nudge amount [Taiga #910](https://tree.taiga.io/project/penpot/us/910)
- Add stroke properties for image shapes [Taiga #497](https://tree.taiga.io/project/penpot/us/497)
- On user settings, hide the theme selector as long as we only have one theme [Taiga #2610](https://tree.taiga.io/project/penpot/us/2610)
- Automatically open comments from dashboard notifications [Taiga #2605](https://tree.taiga.io/project/penpot/us/2605)
- Enhance the behaviour of the artboards list on view mode [Taiga #2634](https://tree.taiga.io/project/penpot/us/2634)
- Add recent used fonts in font selection widget [Taiga #1381](https://tree.taiga.io/project/penpot/us/1381)
- Allow to align items relative to groups [Taiga #2533](https://tree.taiga.io/project/penpot/us/2533)
- Scroll bars [Taiga #2550](https://tree.taiga.io/project/penpot/task/2550)
- Add select layer option to context menu [Taiga #2474](https://tree.taiga.io/project/penpot/us/2474)
- Guides [Taiga #290](https://tree.taiga.io/project/penpot/us/290)
- Improve file menu by adding semantically groups [Github #1203](https://github.com/penpot/penpot/issues/1203)
- Add update components in bulk option in context menu [Taiga #1975](https://tree.taiga.io/project/penpot/us/1975)
- Create first E2E tests [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608), [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608)
- Redesign of workspace toolbars [Taiga #2319](https://tree.taiga.io/project/penpot/us/2319)
- Graphic Tablet usability improvements [Taiga #1913](https://tree.taiga.io/project/penpot/us/1913)
- Improved mouse collision detection for groups and text shapes [Taiga #2452](https://tree.taiga.io/project/penpot/us/2452), [Taiga #2453](https://tree.taiga.io/project/penpot/us/2453)
- Add support for alternative S3 storage providers and all aws regions [#1267](https://github.com/penpot/penpot/issues/1267)
### :bug: Bugs fixed
- Fixed ungroup typography when editing it [Taiga #2391](https://tree.taiga.io/project/penpot/issue/2391)
- Fixed error when trying to post an empty comment [Taiga #2603](https://tree.taiga.io/project/penpot/issue/2603)
- Fixed missing translation strings [Taiga #2786](https://tree.taiga.io/project/penpot/issue/2786)
- Fixed color palette outside viewport [Taiga #2715](https://tree.taiga.io/project/penpot/issue/2715)
- Fixed missing translate string [Taiga #2780](https://tree.taiga.io/project/penpot/issue/2780)
- Fixed handoff shadow type text [Taiga #2717](https://tree.taiga.io/project/penpot/issue/2717)
- Fixed components get "dirty" marker when moved [Taiga #2764](https://tree.taiga.io/project/penpot/issue/2764)
- Fixed cannot align objects in a group that is not part of a frame [Taiga #2762](https://tree.taiga.io/project/penpot/issue/2762)
- Fix problem with double click on exit path editing [Taiga #2906](https://tree.taiga.io/project/penpot/issue/2906)
- Fixed alignment of layers with children [Taiga #2862](https://tree.taiga.io/project/penpot/issue/2862)
### :heart: Community contributions by (Thank you!)
- Cleanup unused static images (by @rhcarvalho) [#1561](https://github.com/penpot/penpot/pull/1561)
- Compress static images to save space (by @rhcarvalho) [#1562](https://github.com/penpot/penpot/pull/1562)
## 1.11.2-beta
### :bug: Bugs fixed
- Fix issue on handling empty content on boolean shapes
- Fix race condition issue on component renaming
- Handle EOF errors on writting streamed response
- Handle EOF errors on websocket send/ping methods
- Disable parallel upload of file media on import (causes too much
contention on the rlimit subsistem that does not works as expected
on high load).
### :sparkles: New features
- Add health check endpoint on API
- Increase default max connection pool size to 60
- Reduce resource usage of the error reporter.
## 1.11.1-beta
### :bug: Bugs fixed
- Fix issue related to default http host config value.
- Fix issue on rendering frames on firefox.
### :arrow_up: Deps updates
- Update nodejs version to 16.13.1 on docker images.
## 1.11.0-beta
### :sparkles: New features
- Add an option to hide artboards names on the viewport [Taiga #2034](https://tree.taiga.io/project/penpot/issue/2034)
- Limit pasted object position to container boundaries [Taiga #2449](https://tree.taiga.io/project/penpot/us/2449)
- Add new options for zoom widget in workspace and viewer mode [Taiga #896](https://tree.taiga.io/project/penpot/us/896)
- Allow decimals on stroke width and positions [Taiga #2035](https://tree.taiga.io/project/penpot/issue/2035)
- Ability to ignore background when exporting an artboard [Taiga #1395](https://tree.taiga.io/project/penpot/us/1395)
- Show color hex or name on hover [Taiga #2413](https://tree.taiga.io/project/penpot/us/2413)
- Add shortcut to create artboard from selected objects [Taiga #2412](https://tree.taiga.io/project/penpot/us/2412)
- Add shortcut for opacity [Taiga #2442](https://tree.taiga.io/project/penpot/us/2442)
- Setting fill automatically for new texts [Taiga #2441](https://tree.taiga.io/project/penpot/us/2441)
- Add shortcut to move action [Github #1213](https://github.com/penpot/penpot/issues/1213)
- Add alt as mod key to add stroke color from library menu [Taiga #2207](https://tree.taiga.io/project/penpot/us/2207)
- Add detach in bulk option to context menu [Taiga #2210](https://tree.taiga.io/project/penpot/us/2210)
- Add penpot look and feel to multiuser cursors [Taiga #1387](https://tree.taiga.io/project/penpot/us/1387)
- Add actions to go to main component context menu option [Taiga #2053](https://tree.taiga.io/project/penpot/us/2053)
- Add contrast between component select color and shape select color [Taiga #2121](https://tree.taiga.io/project/penpot/issue/2121)
- Add animations in interactions [Taiga #2244](https://tree.taiga.io/project/penpot/us/2244)
- Add performance improvements on .penpot file import process [Taiga #2497](https://tree.taiga.io/project/penpot/us/2497)
- On team settings set color of members count to black [Taiga #2607](https://tree.taiga.io/project/penpot/us/2607)
### :bug: Bugs fixed
- Fix remove gradient if any when applying color from library [Taiga #2299](https://tree.taiga.io/project/penpot/issue/2299)
- Fix Enter as key action to exit edit path [Taiga #2444](https://tree.taiga.io/project/penpot/issue/2444)
- Fix add fill color from palette to groups and components [Taiga #2313](https://tree.taiga.io/project/penpot/issue/2313)
- Fix default project name in all languages [Taiga #2280](https://tree.taiga.io/project/penpot/issue/2280)
- Fix line-height and letter-spacing inputs to allow negative values [Taiga #2381](https://tree.taiga.io/project/penpot/issue/2381)
- Fix typo in Handoff tooltip [Taiga #2428](https://tree.taiga.io/project/penpot/issue/2428)
- Fix crash when pressing Shift+1 on empty file [#1435](https://github.com/penpot/penpot/issues/1435)
- Fix masked group resize strange behavior [Taiga #2317](https://tree.taiga.io/project/penpot/issue/2317)
- Fix problems when exporting all artboards [Taiga #2234](https://tree.taiga.io/project/penpot/issue/2234)
- Fix problems with team management [#1353](https://github.com/penpot/penpot/issues/1353)
- Fix problem when importing in shared libraries [#1362](https://github.com/penpot/penpot/issues/1362)
- Fix problem with join nodes [#1422](https://github.com/penpot/penpot/issues/1422)
- After team onboarding importing a file will import into the team drafts [Taiga #2408](https://tree.taiga.io/project/penpot/issue/2408)
- Fix problem exporting shapes from handoff mode [Taiga #2386](https://tree.taiga.io/project/penpot/issue/2386)
- Fix lock/hide elements in context menu when multiples shapes selected [Taiga #2340](https://tree.taiga.io/project/penpot/issue/2340)
- Fix problem with booleans [Taiga #2356](https://tree.taiga.io/project/penpot/issue/2356)
- Fix line-height/letter-spacing inputs behaviour [Taiga #2331](https://tree.taiga.io/project/penpot/issue/2331)
- Fix dotted style in strokes [Taiga #2312](https://tree.taiga.io/project/penpot/issue/2312)
- Fix problem when resizing texts inside groups [Taiga #2310](https://tree.taiga.io/project/penpot/issue/2310)
- Fix problem with multiple exports [Taiga #2468](https://tree.taiga.io/project/penpot/issue/2468)
- Allow import to continue from recoverable failures [#1412](https://github.com/penpot/penpot/issues/1412)
- Improved behaviour on text options when not text is selected [Taiga #2390](https://tree.taiga.io/project/penpot/issue/2390)
- Fix decimal numbers in export viewbox [Taiga #2290](https://tree.taiga.io/project/penpot/issue/2290)
- Right click over artboard name to open its menu [Taiga #1679](https://tree.taiga.io/project/penpot/issue/1679)
- Make the default session cookue use SameSite=Lax instead of Strict (causes some issues in latest versions of Chrome)
- Fix "open in new tab" on dashboard [Taiga #2235](https://tree.taiga.io/project/penpot/issue/2355)
- Changing pages while comments activated will not close the panel [#1350](https://github.com/penpot/penpot/issues/1350)
- Fix navigate comments in right sidebar [Taiga #2163](https://tree.taiga.io/project/penpot/issue/2163)
- Fix keep name of component equal to the shape name [Taiga #2341](https://tree.taiga.io/project/penpot/issue/2341)
- Fix lossing changes when changing selection and an input was already changed [Taiga #2329](https://tree.taiga.io/project/penpot/issue/2329), [Taiga #2330](https://tree.taiga.io/project/penpot/issue/2330)
- Fix blur input field when click on viewport [Taiga #2164](https://tree.taiga.io/project/penpot/issue/2164)
- Fix default page id in workspace [Taiga #2205](https://tree.taiga.io/project/penpot/issue/2205)
- Fix problem when importing a file with grids [Taiga #2314](https://tree.taiga.io/project/penpot/issue/2314)
- Fix problem with imported svgs with filters [Taiga #2478](https://tree.taiga.io/project/penpot/issue/2478)
- Fix issues when updating selrect in paths [Taiga #2366](https://tree.taiga.io/project/penpot/issue/2366)
- Fix scroll jumps in handoff mode [Taiga #2383](https://tree.taiga.io/project/penpot/issue/2383)
- Fix handoff text with opacity [Taiga #2384](https://tree.taiga.io/project/penpot/issue/2384)
- Restored rules color [Taiga #2460](https://tree.taiga.io/project/penpot/issue/2460)
- Fix thumbnail not taking frame blending mode [Taiga #2301](https://tree.taiga.io/project/penpot/issue/2301)
- Fix import/export with SVG edge cases [Taiga #2389](https://tree.taiga.io/project/penpot/issue/2389)
- Avoid modifying component when moving into a group [Taiga #2534](https://tree.taiga.io/project/penpot/issue/2534)
- Show correctly group types label in handoff [Taiga #2482](https://tree.taiga.io/project/penpot/issue/2482)
- Display view mode buttons always centered in viewer [#Taiga 2466](https://tree.taiga.io/project/penpot/issue/2466)
- Fix default profile image generation issue [Taiga #2601](https://tree.taiga.io/project/penpot/issue/2601)
- Fix edit blur attributes for multiselection [Taiga #2625](https://tree.taiga.io/project/penpot/issue/2625)
- Fix auto hide header in viewer full screen [Taiga #2632](https://tree.taiga.io/project/penpot/issue/2632)
- Fix zoom in/out after fit or fill [Taiga #2630](https://tree.taiga.io/project/penpot/issue/2630)
- Normalize zoom levels in workspace and viewer [Taiga #2631](https://tree.taiga.io/project/penpot/issue/2631)
- Avoid empty names in projects, files and pages [Taiga #2594](https://tree.taiga.io/project/penpot/issue/2594)
- Fix "move to" menu when duplicated team or project names [Taiga #2655](https://tree.taiga.io/project/penpot/issue/2655)
- Fix ungroup a component leaves an asterisk in layers [Taiga #2694](https://tree.taiga.io/project/penpot/issue/2694)
### :arrow_up: Deps updates
- Update devenv docker image dependencies
### :heart: Community contributions by (Thank you!)
- Spelling fixes (by @jsoref) [#1340](https://github.com/penpot/penpot/pull/1340)
- Explain folders in components (by @candideu) [Penpot-docs #42](https://github.com/penpot/penpot-docs/pull/42)
- Readability improvements of user guide (by @PaulSchulz) [Penpot-docs #50](https://github.com/penpot/penpot-docs/pull/50)
## 1.10.4-beta
### :sparkles: Enhacements
- Allow parametrice file snapshoting interval
### :bug: Bugs fixed
- Fix issue on :mov-object change impl
- Minor fix on how file changes log is persisted
- Fix many issues on error reporting
## 1.10.3-beta
### :sparkles: Enhacements
- Make all logging asynchronous, this avoid some overhead on jetty threads at cost of logging latency.
- Increase default session time to 15 days.
### :bug: Bugs fixed
- Fix unexpected exception on saving pages with default grids [#2409](https://tree.taiga.io/project/penpot/issue/2409)
- Fix react warnings on setting size 1 on row and column grids.
- Fix minor issues on ZMQ logging listener (used in error reporting service)
- Remove "ALPHA" from the code.
- Fix value and nil handling on numeric-input component. This fixes many issues related to typography, components, etc. renaming.
- Fix NPE on email complains processing.
- Fix white page after leaving a team.
- Fix missing leave team button outside members page.
### :arrow_up: Deps updates
- Update log4j2 dependency.
## 1.10.2-beta
### :bug: Bugs fixed
- Fix corner case issues with media file uploads.
- Fix issue with default page grids validation.
- Fix issue related to some raceconditions on workspace navigation events.
### :arrow_up: Deps updates
- Update log4j2 dependency.
## 1.10.1-beta
### :bug: Bugs fixed
- Fix problems with team management [#1353](https://github.com/penpot/penpot/issues/1353)
## 1.10.0-beta
### :boom: Breaking changes
@@ -20,18 +415,19 @@
### :sparkles: New features
- Enhance corner radius behavior [Taiga #2190](https://tree.taiga.io/project/penpot/issue/2190).
- Allow preserve scroll position in interactions [Taiga #2250](https://tree.taiga.io/project/penpot/us/2250).
- Allow ungroup groups in bulk [Taiga #2211](https://tree.taiga.io/project/penpot/us/2211)
- Enhance corner radius behavior [Taiga #2190](https://tree.taiga.io/project/penpot/issue/2190)
- Allow preserve scroll position in interactions [Taiga #2250](https://tree.taiga.io/project/penpot/us/2250)
- Add new onboarding modals.
### :bug: Bugs fixed
- Fix problem with exporting before the document is saved [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189).
- Fix undo stacking when changing color from color-picker [Taiga #2191](https://tree.taiga.io/project/penpot/issue/2191).
- Fix pages dropdown in viewer [Taiga #2087](https://tree.taiga.io/project/penpot/issue/2087).
- Fix problem when exporting texts with gradients or opacity [Taiga #2200](https://tree.taiga.io/project/penpot/issue/2200).
- Fix problem with view mode comments [Taiga #2226](https://tree.taiga.io/project/penpot/issue/2226).
- Disallow to create a component when already has one [Taiga #2237](https://tree.taiga.io/project/penpot/issue/2237).
- Fix problem with exporting before the document is saved [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189)
- Fix undo stacking when changing color from color-picker [Taiga #2191](https://tree.taiga.io/project/penpot/issue/2191)
- Fix pages dropdown in viewer [Taiga #2087](https://tree.taiga.io/project/penpot/issue/2087)
- Fix problem when exporting texts with gradients or opacity [Taiga #2200](https://tree.taiga.io/project/penpot/issue/2200)
- Fix problem with view mode comments [Taiga #2226](https://tree.taiga.io/project/penpot/issue/2226)
- Disallow to create a component when already has one [Taiga #2237](https://tree.taiga.io/project/penpot/issue/2237)
- Add ellipsis in long labels for input fields [Taiga #2224](https://tree.taiga.io/project/penpot/issue/2224)
- Fix problem with text rendering on export [Taiga #2223](https://tree.taiga.io/project/penpot/issue/2223)
- Fix problem when flattening booleans losing styles [Taiga #2217](https://tree.taiga.io/project/penpot/issue/2217)
@@ -44,61 +440,66 @@
- Add placeholder to create shareable link
- Fix project files count not refreshing correctly after import [Taiga #2216](https://tree.taiga.io/project/penpot/issue/2216)
- Remove button after import process finish [Taiga #2215](https://tree.taiga.io/project/penpot/issue/2215)
- Fix problem with styles in the viewer [Taiga #2467](https://tree.taiga.io/project/penpot/issue/2467)
- Fix default state in viewer [Taiga #2465](https://tree.taiga.io/project/penpot/issue/2465)
- Fix division by zero in bool operation [Taiga #2349](https://tree.taiga.io/project/penpot/issue/2349)
### :heart: Community contributions by (Thank you!)
- To the translation community for the hard work on making penpot
available on so many languages.
- Guide to integrate with Azure Directory (by @skrzyneckik) [Penpot-docs #33](https://github.com/penpot/penpot-docs/pull/33)
- Improve libraries section readability (by @PaulSchulz) [Penpot-docs #39](https://github.com/penpot/penpot-docs/pull/39)
## 1.9.0-alpha
### :boom: Breaking changes
- Some stroke-caps can change behaviour.
- Text display bug fix could potentialy make some texts jump a line.
- Text display bug fix could potentially make some texts jump a line.
### :sparkles: New features
- Add boolean shapes: intersections, unions, difference and exclusions[Taiga #748](https://tree.taiga.io/project/penpot/us/748).
- Add advanced prototyping [Taiga #244](https://tree.taiga.io/project/penpot/us/244).
- Add multiple flows [Taiga #2091](https://tree.taiga.io/project/penpot/us/2091).
- Add boolean shapes: intersections, unions, difference and exclusions[Taiga #748](https://tree.taiga.io/project/penpot/us/748)
- Add advanced prototyping [Taiga #244](https://tree.taiga.io/project/penpot/us/244)
- Add multiple flows [Taiga #2091](https://tree.taiga.io/project/penpot/us/2091)
- Change order of the teams menu so it's in the joined time order.
### :bug: Bugs fixed
- Enhance duplicating prototype connections behaviour [Taiga #2093](https://tree.taiga.io/project/penpot/us/2093).
- Ignore constraints in horizontal or vertical flip [Taiga #2038](https://tree.taiga.io/project/penpot/issue/2038).
- Fix color and typographies refs lost when duplicated file [Taiga #2165](https://tree.taiga.io/project/penpot/issue/2165).
- Fix problem with overflow dropdown on stroke-cap [#1216](https://github.com/penpot/penpot/issues/1216).
- Fix menu context for single element nested in components [#1186](https://github.com/penpot/penpot/issues/1186).
- Fix error screen when operations over comments fail [#1219](https://github.com/penpot/penpot/issues/1219).
- Fix undo problem when changing typography/color from library [#1230](https://github.com/penpot/penpot/issues/1230).
- Fix problem with text margin while rendering [#1231](https://github.com/penpot/penpot/issues/1231).
- Fix problem with masked texts on exporting [Taiga #2116](https://tree.taiga.io/project/penpot/issue/2116).
- Fix text editor enter behaviour with centered texts [Taiga #2126](https://tree.taiga.io/project/penpot/issue/2126).
- Fix residual stroke on imported svg [Taiga #2125](https://tree.taiga.io/project/penpot/issue/2125).
- Add links for terms of service and privacy policy in register checkbox [Taiga #2020](https://tree.taiga.io/project/penpot/issue/2020).
- Allow three character hex and web colors in color picker hex input [#1184](https://github.com/penpot/penpot/issues/1184).
- Allow lowercase search for fonts [#1180](https://github.com/penpot/penpot/issues/1180).
- Fix group renaming problem [Taiga #1969](https://tree.taiga.io/project/penpot/issue/1969).
- Fix export group with shadows on children [Taiga #2036](https://tree.taiga.io/project/penpot/issue/2036).
- Fix zoom context menu in viewer [Taiga #2041](https://tree.taiga.io/project/penpot/issue/2041).
- Fix stroke caps adjustments in relation with stroke size [Taiga #2123](https://tree.taiga.io/project/penpot/issue/2123).
- Fix problem duplicating paths [Taiga #2147](https://tree.taiga.io/project/penpot/issue/2147).
- Fix problem inheriting attributes from SVG root when importing [Taiga #2124](https://tree.taiga.io/project/penpot/issue/2124).
- Fix problem with lines and inside/outside stroke [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146).
- Add stroke width in selection calculation [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146).
- Fix shift+wheel to horizontal scrolling in MacOS [#1217](https://github.com/penpot/penpot/issues/1217).
- Fix path stroke is not working properly with high thickness [Taiga #2154](https://tree.taiga.io/project/penpot/issue/2154).
- Fix bug with transformation operations [Taiga #2155](https://tree.taiga.io/project/penpot/issue/2155).
- Fix bug in firefox when a text box is inside a mask [Taiga #2152](https://tree.taiga.io/project/penpot/issue/2152).
- Enhance duplicating prototype connections behaviour [Taiga #2093](https://tree.taiga.io/project/penpot/us/2093)
- Ignore constraints in horizontal or vertical flip [Taiga #2038](https://tree.taiga.io/project/penpot/issue/2038)
- Fix color and typographies refs lost when duplicated file [Taiga #2165](https://tree.taiga.io/project/penpot/issue/2165)
- Fix problem with overflow dropdown on stroke-cap [#1216](https://github.com/penpot/penpot/issues/1216)
- Fix menu context for single element nested in components [#1186](https://github.com/penpot/penpot/issues/1186)
- Fix error screen when operations over comments fail [#1219](https://github.com/penpot/penpot/issues/1219)
- Fix undo problem when changing typography/color from library [#1230](https://github.com/penpot/penpot/issues/1230)
- Fix problem with text margin while rendering [#1231](https://github.com/penpot/penpot/issues/1231)
- Fix problem with masked texts on exporting [Taiga #2116](https://tree.taiga.io/project/penpot/issue/2116)
- Fix text editor enter behaviour with centered texts [Taiga #2126](https://tree.taiga.io/project/penpot/issue/2126)
- Fix residual stroke on imported svg [Taiga #2125](https://tree.taiga.io/project/penpot/issue/2125)
- Add links for terms of service and privacy policy in register checkbox [Taiga #2020](https://tree.taiga.io/project/penpot/issue/2020)
- Allow three character hex and web colors in color picker hex input [#1184](https://github.com/penpot/penpot/issues/1184)
- Allow lowercase search for fonts [#1180](https://github.com/penpot/penpot/issues/1180)
- Fix group renaming problem [Taiga #1969](https://tree.taiga.io/project/penpot/issue/1969)
- Fix export group with shadows on children [Taiga #2036](https://tree.taiga.io/project/penpot/issue/2036)
- Fix zoom context menu in viewer [Taiga #2041](https://tree.taiga.io/project/penpot/issue/2041)
- Fix stroke caps adjustments in relation with stroke size [Taiga #2123](https://tree.taiga.io/project/penpot/issue/2123)
- Fix problem duplicating paths [Taiga #2147](https://tree.taiga.io/project/penpot/issue/2147)
- Fix problem inheriting attributes from SVG root when importing [Taiga #2124](https://tree.taiga.io/project/penpot/issue/2124)
- Fix problem with lines and inside/outside stroke [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146)
- Add stroke width in selection calculation [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146)
- Fix shift+wheel to horizontal scrolling in MacOS [#1217](https://github.com/penpot/penpot/issues/1217)
- Fix path stroke is not working properly with high thickness [Taiga #2154](https://tree.taiga.io/project/penpot/issue/2154)
- Fix bug with transformation operations [Taiga #2155](https://tree.taiga.io/project/penpot/issue/2155)
- Fix bug in firefox when a text box is inside a mask [Taiga #2152](https://tree.taiga.io/project/penpot/issue/2152)
- Fix problem with stroke inside/outside [Taiga #2186](https://tree.taiga.io/project/penpot/issue/2186)
- Fix masks export area [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189)
- Fix paste in place in arboards [Taiga #2188](https://tree.taiga.io/project/penpot/issue/2188)
- Fix paste in place in artboards [Taiga #2188](https://tree.taiga.io/project/penpot/issue/2188)
- Fix font size input stuck on selection change [Taiga #2184](https://tree.taiga.io/project/penpot/issue/2184)
- Fix stroke cut on shapes export [Taiga #2171](https://tree.taiga.io/project/penpot/issue/2171)
- Fix no color when boolean with an SVG [Taiga #2193](https://tree.taiga.io/project/penpot/issue/2193)
- Fix unlink color styles at strokes [Taiga #2206](https://tree.taiga.io/project/penpot/issue/2206).
- Fix unlink color styles at strokes [Taiga #2206](https://tree.taiga.io/project/penpot/issue/2206)
### :arrow_up: Deps updates
@@ -107,13 +508,11 @@
- To the translation community for the hard work on making penpot
available on so many languages.
## 1.8.4-alpha
### :bug: Bugs fixed
- Fix problem importing components [Taiga #2151](https://tree.taiga.io/project/penpot/issue/2151).
- Fix problem importing components [Taiga #2151](https://tree.taiga.io/project/penpot/issue/2151)
## 1.8.3-alpha
@@ -125,18 +524,17 @@
### :bug: Bugs fixed
- Fix problem with masking images in viewer [#1238](https://github.com/penpot/penpot/issues/1238).
- Fix problem with masking images in viewer [#1238](https://github.com/penpot/penpot/issues/1238)
## 1.8.1-alpha
### :bug: Bugs fixed
- Fix project renaming issue (and some other related to the same underlying bug).
- Fix project renaming issue (and some other related to the same underlying bug)
- Fix internal exception on audit log persistence layer.
- Set proper environment variable on docker images for chrome executable.
- Fix internal metrics on websocket connections.
## 1.8.0-alpha
### :boom: Breaking changes
@@ -147,25 +545,25 @@
### :sparkles: New features
- Add tooltips to color picker tabs [Taiga #1814](https://tree.taiga.io/project/penpot/us/1814).
- Add styling to the end point of any open paths [Taiga #1107](https://tree.taiga.io/project/penpot/us/1107).
- Allow to zoom with ctrl + middle button [Taiga #1428](https://tree.taiga.io/project/penpot/us/1428).
- Auto placement of duplicated objects [Taiga #1386](https://tree.taiga.io/project/penpot/us/1386).
- Enable penpot SVG metadata only when exporting complete files [Taiga #1914](https://tree.taiga.io/project/penpot/us/1914?milestone=295883).
- Export to PDF all artboards of one page [Taiga #1895](https://tree.taiga.io/project/penpot/us/1895).
- Go to a undo step clicking on a history element of the list [Taiga #1374](https://tree.taiga.io/project/penpot/us/1374).
- Increment font size by 10 with shift+arrows [1047](https://github.com/penpot/penpot/issues/1047).
- New shortcut to detach components Ctrl+Shift+K [Taiga #1799](https://tree.taiga.io/project/penpot/us/1799).
- Set email inputs to type "email", to aid keyboard entry [Taiga #1921](https://tree.taiga.io/project/penpot/issue/1921).
- Use shift+move to move element orthogonally [#823](https://github.com/penpot/penpot/issues/823).
- Use space + mouse drag to pan, instead of only space [Taiga #1800](https://tree.taiga.io/project/penpot/us/1800).
- Allow navigate through pages on the viewer [Taiga #1550](https://tree.taiga.io/project/penpot/us/1550).
- Allow create share links with specific pages [Taiga #1844](https://tree.taiga.io/project/penpot/us/1844).
- Add tooltips to color picker tabs [Taiga #1814](https://tree.taiga.io/project/penpot/us/1814)
- Add styling to the end point of any open paths [Taiga #1107](https://tree.taiga.io/project/penpot/us/1107)
- Allow to zoom with ctrl + middle button [Taiga #1428](https://tree.taiga.io/project/penpot/us/1428)
- Auto placement of duplicated objects [Taiga #1386](https://tree.taiga.io/project/penpot/us/1386)
- Enable penpot SVG metadata only when exporting complete files [Taiga #1914](https://tree.taiga.io/project/penpot/us/1914?milestone=295883)
- Export to PDF all artboards of one page [Taiga #1895](https://tree.taiga.io/project/penpot/us/1895)
- Go to a undo step clicking on a history element of the list [Taiga #1374](https://tree.taiga.io/project/penpot/us/1374)
- Increment font size by 10 with shift+arrows [1047](https://github.com/penpot/penpot/issues/1047)
- New shortcut to detach components Ctrl+Shift+K [Taiga #1799](https://tree.taiga.io/project/penpot/us/1799)
- Set email inputs to type "email", to aid keyboard entry [Taiga #1921](https://tree.taiga.io/project/penpot/issue/1921)
- Use shift+move to move element orthogonally [#823](https://github.com/penpot/penpot/issues/823)
- Use space + mouse drag to pan, instead of only space [Taiga #1800](https://tree.taiga.io/project/penpot/us/1800)
- Allow navigate through pages on the viewer [Taiga #1550](https://tree.taiga.io/project/penpot/us/1550)
- Allow create share links with specific pages [Taiga #1844](https://tree.taiga.io/project/penpot/us/1844)
### :bug: Bugs fixed
- Prevent adding numeric suffix to layer names when not needed [Taiga #1929](https://tree.taiga.io/project/penpot/us/1929).
- Prevent deleting or moving the drafts project [Taiga #1935](https://tree.taiga.io/project/penpot/issue/1935).
- Prevent adding numeric suffix to layer names when not needed [Taiga #1929](https://tree.taiga.io/project/penpot/us/1929)
- Prevent deleting or moving the drafts project [Taiga #1935](https://tree.taiga.io/project/penpot/issue/1935)
- Fix problem with zoom and selection [Taiga #1919](https://tree.taiga.io/project/penpot/issue/1919)
- Fix problem with borders on shape export [#1092](https://github.com/penpot/penpot/issues/1092)
- Fix thumbnail cropping issue [Taiga #1964](https://tree.taiga.io/project/penpot/issue/1964)
@@ -178,11 +576,12 @@
- Fix problem while moving imported SVG's [#1199](https://github.com/penpot/penpot/issues/1199)
### :arrow_up: Deps updates
### :boom: Breaking changes
### :heart: Community contributions by (Thank you!)
- eduayme [#1129](https://github.com/penpot/penpot/pull/1129).
- eduayme [#1129](https://github.com/penpot/penpot/pull/1129)
## 1.7.4-alpha
@@ -191,14 +590,12 @@
- Fix demo user creation (self-hosted only)
- Add better ldap response validation and reporting (self-hosted only)
## 1.7.3-alpha
### :bug: Bugs fixed
- Fix font uploading issue on Windows.
## 1.7.2-alpha
### :sparkles: New features
@@ -207,8 +604,8 @@
### :bug: Bugs fixed
- Add scroll bar to Teams menu [Taiga #1894](https://tree.taiga.io/project/penpot/issue/1894).
- Fix repeated names when duplicating artboards or groups [Taiga #1892](https://tree.taiga.io/project/penpot/issue/1892).
- Add scroll bar to Teams menu [Taiga #1894](https://tree.taiga.io/project/penpot/issue/1894)
- Fix repeated names when duplicating artboards or groups [Taiga #1892](https://tree.taiga.io/project/penpot/issue/1892)
- Fix properly messages lifecycle on navigate.
- Fix handling repeated names on duplicate object trees.
- Fix group naming on group creation.
@@ -222,7 +619,6 @@
- soultipsy [#1100](https://github.com/penpot/penpot/pull/1100)
## 1.7.1-alpha
### :bug: Bugs fixed
@@ -232,19 +628,18 @@
- Fix issue on undo page deletion.
- Fix some issues related to constraints.
## 1.7.0-alpha
### :sparkles: New features
- Allow nested asset groups [Taiga #1716](https://tree.taiga.io/project/penpot/us/1716).
- Allow to ungroup assets [Taiga #1719](https://tree.taiga.io/project/penpot/us/1719).
- Allow to rename assets groups [Taiga #1721](https://tree.taiga.io/project/penpot/us/1721).
- Component constraints (left, right, left and right, center, scale...) [Taiga #1125](https://tree.taiga.io/project/penpot/us/1125).
- Export elements to PDF [Taiga #519](https://tree.taiga.io/project/penpot/us/519).
- Memorize collapse state of assets in panel [Taiga #1718](https://tree.taiga.io/project/penpot/us/1718).
- Headers button sets and menus review [Taiga #1663](https://tree.taiga.io/project/penpot/us/1663).
- Preserve components if possible, when pasted into a different file [Taiga #1063](https://tree.taiga.io/project/penpot/issue/1063).
- Allow nested asset groups [Taiga #1716](https://tree.taiga.io/project/penpot/us/1716)
- Allow to ungroup assets [Taiga #1719](https://tree.taiga.io/project/penpot/us/1719)
- Allow to rename assets groups [Taiga #1721](https://tree.taiga.io/project/penpot/us/1721)
- Component constraints (left, right, left and right, center, scale...) [Taiga #1125](https://tree.taiga.io/project/penpot/us/1125)
- Export elements to PDF [Taiga #519](https://tree.taiga.io/project/penpot/us/519)
- Memorize collapse state of assets in panel [Taiga #1718](https://tree.taiga.io/project/penpot/us/1718)
- Headers button sets and menus review [Taiga #1663](https://tree.taiga.io/project/penpot/us/1663)
- Preserve components if possible, when pasted into a different file [Taiga #1063](https://tree.taiga.io/project/penpot/issue/1063)
- Add the ability to offload file data to a cheaper storage when file becomes inactive.
- Import/Export Penpot files from dashboard.
- Double click won't make a shape a path until you change a node [Taiga #1796](https://tree.taiga.io/project/penpot/us/1796)
@@ -253,21 +648,20 @@
### :bug: Bugs fixed
- Process numeric input changes only if the value actually changed.
- Remove unnecesary redirect from history when user goes to workspace from dashboard [Taiga #1820](https://tree.taiga.io/project/penpot/issue/1820).
- Detach shapes from deleted assets [Taiga #1850](https://tree.taiga.io/project/penpot/issue/1850).
- Fix tooltip position on view application [Taiga #1819](https://tree.taiga.io/project/penpot/issue/1819).
- Fix dashboard navigation on moving file to other team [Taiga #1817](https://tree.taiga.io/project/penpot/issue/1817).
- Fix workspace header presence styles and invalid link [Taiga #1813](https://tree.taiga.io/project/penpot/issue/1813).
- Fix color-input wrong behavior (on workspace page color) [Taiga #1795](https://tree.taiga.io/project/penpot/issue/1795).
- Fix file contextual menu in shared libraries at dashboard [Taiga #1865](https://tree.taiga.io/project/penpot/issue/1865).
- Remove unnecessary redirect from history when user goes to workspace from dashboard [Taiga #1820](https://tree.taiga.io/project/penpot/issue/1820)
- Detach shapes from deleted assets [Taiga #1850](https://tree.taiga.io/project/penpot/issue/1850)
- Fix tooltip position on view application [Taiga #1819](https://tree.taiga.io/project/penpot/issue/1819)
- Fix dashboard navigation on moving file to other team [Taiga #1817](https://tree.taiga.io/project/penpot/issue/1817)
- Fix workspace header presence styles and invalid link [Taiga #1813](https://tree.taiga.io/project/penpot/issue/1813)
- Fix color-input wrong behavior (on workspace page color) [Taiga #1795](https://tree.taiga.io/project/penpot/issue/1795)
- Fix file contextual menu in shared libraries at dashboard [Taiga #1865](https://tree.taiga.io/project/penpot/issue/1865)
- Fix problem with color picker and fonts [#1049](https://github.com/penpot/penpot/issues/1049)
- Fix negative values in blur [Taiga #1815](https://tree.taiga.io/project/penpot/issue/1815)
- Fix problem when editing color in group [Taiga #1816](https://tree.taiga.io/project/penpot/issue/1816)
- Fix resize/rotate with mouse buttons different than left [#1060](https://github.com/penpot/penpot/issues/1060)
- Fix header partialy visible on fullscreen viewer mode [Taiga #1875](https://tree.taiga.io/project/penpot/issue/1875)
- Fix header partially visible on fullscreen viewer mode [Taiga #1875](https://tree.taiga.io/project/penpot/issue/1875)
- Fix dynamic alignment enabled with hidden objects [#1063](https://github.com/penpot/penpot/issues/1063)
## 1.6.5-alpha
### :bug: Bugs fixed
@@ -278,8 +672,8 @@
### :sparkles: Minor improvements
- Decrease default bulk buffers on storage tasks.
- Reduce file_change preserve interval to 24h.
- Decrease default bulk buffers on storage tasks.
- Reduce file_change preserve interval to 24h.
### :bug: Bugs fixed
@@ -292,7 +686,6 @@
- Properly handle nil values on `update-shapes` function.
- Replace frame term usage by artboard on viewer app.
## 1.6.3-alpha
### :bug: Bugs fixed
@@ -319,42 +712,39 @@
- Minor fix on previous commit.
- Minor improvements on svg uploading on libraries.
## 1.6.1-alpha
### :bug: Bugs fixed
- Add safety check on reg-objects change impl.
- Fix custom fonts embbedding issue.
- Fix custom fonts embedding issue.
- Fix dashboard ordering issue.
- Fix problem when creating a component with empty data.
- Fix problem with moving shapes into frames.
- Fix problems with mov-objects.
- Fix unexpected excetion related to rounding integers.
- Fix unexpected exception related to rounding integers.
- Fix wrong type usage on libraries changes.
- Improve editor lifecycle management.
- Make the navigation async by default.
## 1.6.0-alpha
### :sparkles: New features
- Add improved workspace font selector [Taiga US #292](https://tree.taiga.io/project/penpot/us/292).
- Add improved workspace font selector [Taiga US #292](https://tree.taiga.io/project/penpot/us/292)
- Add option to interactively scale text [Taiga #1527](https://tree.taiga.io/project/penpot/us/1527)
- Add performance improvements on dashboard data loading.
- Add performance improvements to indexes handling on workspace.
- Add the ability to upload/use custom fonts (and automatically generate all needed webfonts) [Taiga US #292](https://tree.taiga.io/project/penpot/us/292).
- Add the ability to upload/use custom fonts (and automatically generate all needed webfonts) [Taiga US #292](https://tree.taiga.io/project/penpot/us/292)
- Transform shapes to path on double click
- Translate automatic names of new files and projects.
- Use shift instead of ctrl/cmd to keep aspect ratio [Taiga 1697](https://tree.taiga.io/project/penpot/issue/1697).
- New translations: Portuguese (Brazil) and Romanias.
- Use shift instead of ctrl/cmd to keep aspect ratio [Taiga 1697](https://tree.taiga.io/project/penpot/issue/1697)
- New translations: Portuguese (Brazil) and Romanias.
### :bug: Bugs fixed
- Remove interactions when the destination artboard is deleted [Taiga #1656](https://tree.taiga.io/project/penpot/issue/1656).
- Fix problem with fonts that ends with numbers [#940](https://github.com/penpot/penpot/issues/940).
- Remove interactions when the destination artboard is deleted [Taiga #1656](https://tree.taiga.io/project/penpot/issue/1656)
- Fix problem with fonts that ends with numbers [#940](https://github.com/penpot/penpot/issues/940)
- Fix problem with imported SVG on editing paths [#971](https://github.com/penpot/penpot/issues/971)
- Fix problem with color picker positioning
- Fix order on color palette [#961](https://github.com/penpot/penpot/issues/961)
@@ -368,7 +758,6 @@
- Update exporter dependencies (puppeteer), that fixes some unexpected exceptions.
- Update string manipulation library.
### :boom: Breaking changes
- The OIDC setting `PENPOT_OIDC_SCOPES` has changed the default semantics. Before this
@@ -379,7 +768,6 @@
- Translations: Portuguese (Brazil) and Romanias.
## 1.5.4-alpha
### :bug: Bugs fixed
@@ -387,7 +775,6 @@
- Fix issues on group rendering.
- Fix problem with text editing auto-height [Taiga #1683](https://tree.taiga.io/project/penpot/issue/1683)
## 1.5.3-alpha
### :bug: Bugs fixed
@@ -411,7 +798,6 @@
- Increase default team invitation token expiration to 48h.
- Fix wrong error message when an expired token is used.
## 1.5.0-alpha
### :sparkles: New features
@@ -457,7 +843,6 @@
- madmath03 (by [Monogramm](https://github.com/Monogramm)) [#807](https://github.com/penpot/penpot/pull/807)
- zzkt [#814](https://github.com/penpot/penpot/pull/814)
## 1.4.1-alpha
### :bug: Bugs fixed
@@ -469,7 +854,6 @@
- Fix incorrect state management of user lang selection.
- Fix email validation usability issue on team invitation lightbox.
## 1.4.0-alpha
### :sparkles: New features
@@ -478,7 +862,7 @@
- Add http caching layer on top of Query RPC.
- Add layer opacity and blend mode to shapes [Taiga #937](https://tree.taiga.io/project/penpot/us/937)
- Add more chinese translations [#726](https://github.com/penpot/penpot/pull/726)
- Add native support for text-direction (RTL, LTR & auto).
- Add native support for text-direction (RTL, LTR & auto)
- Add several enhancements in shape selection [Taiga #1195](https://tree.taiga.io/project/penpot/us/1195)
- Add thumbnail in memory caching mechanism.
- Add turkish translation strings [#759](https://github.com/penpot/penpot/pull/759), [#794](https://github.com/penpot/penpot/pull/794)
@@ -486,13 +870,12 @@
- Hide viewer navbar on fullscreen [Taiga 1375](https://tree.taiga.io/project/penpot/us/1375)
- Import SVG will create Penpot's shapes [Taiga #1006](https://tree.taiga.io/project/penpot/us/1066)
- Improve french translations [#731](https://github.com/penpot/penpot/pull/731)
- Reimplement workspace presence (remove database state).
- Reimplement workspace presence (remove database state)
- Remember last visited team when you re-enter the application [Taiga #1376](https://tree.taiga.io/project/penpot/us/1376)
- Rename artboard with double click on the title [Taiga #1392](https://tree.taiga.io/project/penpot/us/1392)
- Replace Slate-Editor with DraftJS [Taiga #1346](https://tree.taiga.io/project/penpot/us/1346)
- Set proper page title [Taiga #1377](https://tree.taiga.io/project/penpot/us/1377)
### :bug: Bugs fixed
- Disable buttons in view mode for users without permissions [Taiga #1328](https://tree.taiga.io/project/penpot/issue/1328)
@@ -533,15 +916,13 @@
- The LDAP configuration variables interpolation starts using `:`
(example `:username`) instead of `$`. The main reason is avoid
unnecesary conflict with bash interpolation.
unnecessary conflict with bash interpolation.
### :arrow_up: Deps updates
- Update backend to JDK16.
- Update exporter nodejs to v14.16.0
### :heart: Community contributions by (Thank you!)
- iblueer [#726](https://github.com/penpot/penpot/pull/726)
@@ -549,27 +930,25 @@
- girafic [#748](https://github.com/penpot/penpot/pull/748)
- mbrksntrk [#794](https://github.com/penpot/penpot/pull/794)
## 1.3.0-alpha
### :sparkles: New features
- Add emailcatcher and ldap test containers to devenv. [#506](https://github.com/penpot/penpot/pull/506)
- Add major refactor of internal pubsub/redis code; improves scalability and performance [#640](https://github.com/penpot/penpot/pull/640)
- Add more chinese transtions [#687](https://github.com/penpot/penpot/pull/687)
- Add more chinese translations [#687](https://github.com/penpot/penpot/pull/687)
- Add more presets for artboard [#654](https://github.com/penpot/penpot/pull/654)
- Add optional loki integration [#645](https://github.com/penpot/penpot/pull/645)
- Add proper http session lifecycle handling.
- Allow to set border radius of each rect corner individually
- Bounce & Complaint handling [#635](https://github.com/penpot/penpot/pull/635)
- Disable groups interactions when holding "Ctrl" key (deep selection)
- New action in context menu to "edit" some shapes (binded to key "Enter")
- New action in context menu to "edit" some shapes (bound to key "Enter")
### :bug: Bugs fixed
- Add more improvements to french translation strings [#591](https://github.com/penpot/penpot/pull/591)
- Add some missing database indexes (mainly improves performance on large databases on file-update rpc method, and some background tasks).
- Add some missing database indexes (mainly improves performance on large databases on file-update rpc method, and some background tasks)
- Disables filters in masking elements (issue with Firefox rendering)
- Drawing tool will have priority over resize/rotate handlers [Taiga #1225](https://tree.taiga.io/project/penpot/issue/1225)
- Fix broken bounding box on editing paths [Taiga #1254](https://tree.taiga.io/project/penpot/issue/1254)
@@ -583,16 +962,14 @@
- Have language change notification written in the new language [Taiga #1205](https://tree.taiga.io/project/penpot/issue/1205)
- Hide register screen when registration is disabled [#598](https://github.com/penpot/penpot/issues/598)
- Properly handle errors on github, gitlab and ldap auth backends.
- Properly mark profile auth backend (on first register/ auth with 3rd party auth provider).
- Properly mark profile auth backend (on first register/ auth with 3rd party auth provider)
- Refactor LDAP auth backend.
### :heart: Community contributions by (Thank you!)
- girafic [#538](https://github.com/penpot/penpot/pull/654)
- arkhi [#591](https://github.com/penpot/penpot/pull/591)
## 1.2.0-alpha
### :sparkles: New features
@@ -607,7 +984,6 @@
- Show a pixel grid when zoom greater than 800% [#519](https://github.com/penpot/penpot/discussions/519)
- Fix behavior of select all command when there are objects outside frames [Taiga #1209](https://tree.taiga.io/project/penpot/issue/1209)
### :bug: Bugs fixed
- Fix 404 when access shared link [#615](https://github.com/penpot/penpot/issues/615)
@@ -643,7 +1019,6 @@
- Improved MacOS shortcuts and helpers
- Small changes to shape creation
## 1.0.0-alpha
Initial release

View File

@@ -19,9 +19,9 @@ If you found a bug, please report it, as far as possible with:
- a browser and the browser version used
- a dev tools console exception stack trace (if it is available)
If you found a bug that you consider better discuse in private (for
If you found a bug that you consider better discuss in private (for
example: security bugs), consider first send an email to
`info@penpot.app`.
`support@penpot.app`.
**We don't have formal bug bounty program for security reports; this
is an open source application and your contribution will be recognized
@@ -54,7 +54,7 @@ We will use the `easy fix` mark for tag for indicate issues that are
easy for beginners.
## Commit Message Guidelines ##
## Commit Guidelines ##
We have very precise rules over how our git commit messages can be formatted.
@@ -78,7 +78,6 @@ Where type is:
- :ambulance: `:ambulance:` a commit that fixes critical bug
- :books: `:books:` a commit that improves or adds documentation
- :construction: `:construction:`: a wip commit
- :construction_worker: `:construction_worker:` a commit with CI related stuff
- :boom: `:boom:` a commit with breaking changes
- :wrench: `:wrench:` a commit for config updates
- :zap: `:zap:` a commit with performance improvements
@@ -91,12 +90,25 @@ More info:
- https://gist.github.com/parmentf/035de27d6ed1dce0b36a
- https://gist.github.com/rxaviers/7360908
The subject should be:
Each commit should have:
- Use the imperative mood.
- Capitalize the first letter.
- Don't put a period at the end of the subject line.
- Put a blank line between the subject line and the body.
- A concise subject using imperative mood.
- The subject should have capitalized the first letter, without period
at the end and no larger than 65 characters.
- A blank line between the subject line and the body.
- An entry on the CHANGES.md file if applicable, referencing the
github or taiga issue/user-story using the these same rules.
Examples of good commit messags:
- :bug: Fix unexpected error on launching modal
- :bug: Set proper error message on generic error
- :sparkles: Enable new modal for profile
- :zap: Improve performance of dashboard navigation
- :wrench: Update default backend configuration
- :books: Add more documentation for authentication process
- :ambulance: Fix critical bug on user registration process
- :tada: Add new approach for user registration
## Code of conduct ##

View File

@@ -70,9 +70,9 @@ You can ask and answer questions, have open-ended conversations, and follow alon
✉️ [Mail us](mailto:info@penpot.app)
💬 [Github discussions](https://github.com/penpot/penpot/discussions)
💬 [GitHub discussions](https://github.com/penpot/penpot/discussions)
🐞 [Github issues](mailto:info@penpot.apphttps://github.com/penpot/penpot/issues)
🐞 [GitHub issues](https://github.com/penpot/penpot/issues)
✍️️ [Gitter](https://gitter.im/penpot/community)
@@ -81,7 +81,7 @@ You can ask and answer questions, have open-ended conversations, and follow alon
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
Would you like to know more about Penpot? We recommend you to visit our youtube channel and learn more about the functionalities and possibilities of Penpot with our video tutorials.
🎞️ [Youtube channel](https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g)
🎞️ [YouTube channel](https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g)
## License ##

36
backend/build.clj Normal file
View File

@@ -0,0 +1,36 @@
(ns build
(:refer-clojure :exclude [compile])
(:require
[clojure.tools.build.api :as b]
[clojure.java.io]))
(def class-dir "target/classes")
(def basis (b/create-basis {:project "deps.edn"}))
(def jar-file "target/penpot.jar")
(defn clean [_]
(b/delete {:path "target"}))
(defn jar [_]
(b/copy-dir
{:src-dirs ["src" "resources"]
:target-dir class-dir})
(b/compile-clj
{:basis basis
:src-dirs ["src"]
:class-dir class-dir})
(b/uber
{:class-dir class-dir
:uber-file jar-file
:main 'clojure.main
:exclude [#"goog.*" #"^javasist.*"]
:basis basis}))
(defn compile [_]
(b/javac
{:src-dirs ["dev/java"]
:class-dir class-dir
:basis basis
:javac-opts ["-source" "11" "-target" "11"]}))

View File

@@ -1,81 +1,77 @@
{
;; :mvn/repos
;; {"central" {:url "https://repo1.maven.org/maven2/"}
;; "clojars" {:url "https://clojars.org/repo"}
;; "jcenter" {:url "https://jcenter.bintray.com/"}
;; }
:deps
{penpot/common
{:local/root "../common"}
{:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/core.async {:mvn/version "1.5.648"}
;; Logging
org.zeromq/jeromq {:mvn/version "0.5.2"}
com.taoensso/nippy {:mvn/version "3.1.1"}
com.github.luben/zstd-jni {:mvn/version "1.5.0-4"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-2"}
org.clojure/data.fressian {:mvn/version "1.0.0"}
;; NOTE: don't upgrade to latest version, breaking change is
;; introduced on 0.10.0 that suffixes counters with _total if they
;; are not already has this suffix.
io.prometheus/simpleclient {:mvn/version "0.9.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.9.0"}
io.prometheus/simpleclient_jetty {:mvn/version "0.9.0"
io.prometheus/simpleclient {:mvn/version "0.15.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.15.0"}
io.prometheus/simpleclient_jetty {:mvn/version "0.15.0"
:exclusions [org.eclipse.jetty/jetty-server
org.eclipse.jetty/jetty-servlet]}
io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"}
io.prometheus/simpleclient_httpserver {:mvn/version "0.15.0"}
io.lettuce/lettuce-core {:mvn/version "6.1.5.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.1.6.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
info.sunng/ring-jetty9-adapter {:mvn/version "0.15.2"}
com.github.seancorfield/next.jdbc {:mvn/version "1.2.709"}
metosin/reitit-ring {:mvn/version "0.5.15"}
org.postgresql/postgresql {:mvn/version "42.2.23"}
com.zaxxer/HikariCP {:mvn/version "5.0.0"}
funcool/yetti {:git/tag "v9.1" :git/sha "63f35d9"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.2.772"}
metosin/reitit-core {:mvn/version "0.5.16"}
org.postgresql/postgresql {:mvn/version "42.3.3"}
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
funcool/datoteka {:mvn/version "2.0.0"}
buddy/buddy-core {:mvn/version "1.10.1"}
buddy/buddy-hashers {:mvn/version "1.8.1"}
buddy/buddy-sign {:mvn/version "3.4.1"}
buddy/buddy-hashers {:mvn/version "1.8.158"}
buddy/buddy-sign {:mvn/version "3.4.333"}
org.jsoup/jsoup {:mvn/version "1.14.2"}
org.jsoup/jsoup {:mvn/version "1.14.3"}
org.im4java/im4java {:mvn/version "1.4.0"}
org.lz4/lz4-java {:mvn/version "1.8.0"}
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
integrant/integrant {:mvn/version "0.8.0"}
io.sentry/sentry {:mvn/version "5.1.2"}
io.sentry/sentry {:mvn/version "5.6.1"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.11.0"}
;; Pretty Print specs
fipp/fipp {:mvn/version "0.6.24"}
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.17.136"}}
software.amazon.awssdk/s3 {:mvn/version "2.17.40"}}
:paths ["src" "resources"]
:paths ["src" "resources" "target/classes"]
:aliases
{:dev
{:extra-deps
{com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
org.clojure/test.check {:mvn/version "RELEASE"}
org.clojure/data.csv {:mvn/version "1.0.0"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "0.5.1"}
criterium/criterium {:mvn/version "RELEASE"}
clojure-humanize/clojure-humanize {:mvn/version "0.2.2"}
org.clojure/data.csv {:mvn/version "RELEASE"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
mockery/mockery {:mvn/version "RELEASE"}}
:extra-paths ["test" "dev"]}
:kaocha
{:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.887"}}
:main-opts ["-m" "kaocha.runner"]}
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.7.7" :git/sha "1474ad6"}}
:ns-default build}
:test
{:extra-deps {io.github.cognitect-labs/test-runner
{:git/url "https://github.com/cognitect-labs/test-runner.git"
:git/sha "dd6da11611eeb87f08780a30ac8ea6012d4c05ce"}}
{:extra-paths ["test"]
:extra-deps
{io.github.cognitect-labs/test-runner
{:git/tag "v0.5.0" :git/sha "b3fd0d2"}}
:exec-fn cognitect.test-runner.api/test}
:outdated

View File

@@ -6,13 +6,19 @@
(ns user
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.matrix :as gmt]
[app.common.perf :as perf]
[app.common.transit :as t]
[app.config :as cfg]
[app.main :as main]
[app.util.blob :as blob]
[app.util.fressian :as fres]
[app.util.json :as json]
[app.util.time :as dt]
[app.util.transit :as t]
[clj-async-profiler.core :as prof]
[clojure.contrib.humanize :as hum]
[clojure.java.io :as io]
[clojure.pprint :refer [pprint print-table]]
[clojure.repl :refer :all]
@@ -22,31 +28,14 @@
[clojure.test :as test]
[clojure.tools.namespace.repl :as repl]
[clojure.walk :refer [macroexpand-all]]
[criterium.core :refer [quick-bench bench with-progress-reporting]]
[datoteka.core]
[integrant.core :as ig]))
(repl/disable-reload! (find-ns 'integrant.core))
(set! *warn-on-reflection* true)
(defonce system nil)
;; --- Benchmarking Tools
(defmacro run-quick-bench
[& exprs]
`(with-progress-reporting (quick-bench (do ~@exprs) :verbose)))
(defmacro run-quick-bench'
[& exprs]
`(quick-bench (do ~@exprs)))
(defmacro run-bench
[& exprs]
`(with-progress-reporting (bench (do ~@exprs) :verbose)))
(defmacro run-bench'
[& exprs]
`(bench (do ~@exprs)))
;; --- Development Stuff
(defn- run-tests
@@ -91,7 +80,16 @@
(defn compression-bench
[data]
(print-table
[{:v1 (alength (blob/encode data {:version 1}))
:v2 (alength (blob/encode data {:version 2}))
:v3 (alength (blob/encode data {:version 3}))}]))
(let [humanize (fn [v] (hum/filesize v :binary true :format " %.4f "))]
(print-table
[{:v1 (humanize (alength (blob/encode data {:version 1})))
:v2 (humanize (alength (blob/encode data {:version 2})))
:v3 (humanize (alength (blob/encode data {:version 3})))
:v4 (humanize (alength (blob/encode data {:version 4})))
}])))
(defonce debug-tap
(do
(add-tap #(locking debug-tap
(prn "tap debug:" %)))
1))

View File

@@ -1,195 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex,nofollow">
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>penpot - error report {{id}}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
<style>
body {
margin: 0px;
padding: 0px;
}
pre {
margin: 0px;
}
* {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
}
.table {
display: flex;
flex-direction: column;
margin: 10px;
}
.table-row {
display: flex;
/* width: 100%; */
/* border: 1px solid red; */
}
.table-key {
font-weight: 600;
width: 60px;
padding: 4px;
}
.table-val {
font-weight: 200;
color: #333;
padding: 4px;
}
.multiline {
margin-top: 15px;
flex-direction: column;
}
.multiline .table-key {
margin-bottom: 10px;
border-bottom: 1px dashed #dddddd;
/* padding: 4px; */
width: unset;
}
</style>
</head>
<body>
<div class="table">
<div class="table-row">
<div class="table-key" title="Error ID">ERID: </div>
<div class="table-val">{{id}}</div>
</div>
{% if profile-id %}
<div class="table-row">
<div class="table-key" title="Profile ID">PFID: </div>
<div class="table-val">{{profile-id}}</div>
</div>
{% endif %}
{% if user-agent %}
<div class="table-row">
<div class="table-key">UAGT: </div>
<div class="table-val">{{user-agent}}</div>
</div>
{% endif %}
{% if frontend-version %}
<div class="table-row">
<div class="table-key">FVER: </div>
<div class="table-val">{{frontend-version}}</div>
</div>
{% endif %}
<div class="table-row">
<div class="table-key">BVER: </div>
<div class="table-val">{{version}}</div>
</div>
{% if host %}
<div class="table-row">
<div class="table-key">HOST: </div>
<div class="table-val">{{host}}</div>
</div>
{% endif %}
{% if tenant %}
<div class="table-row">
<div class="table-key">ENV: </div>
<div class="table-val">{{tenant}}</div>
</div>
{% endif %}
{% if public-uri %}
<div class="table-row">
<div class="table-key">PURI: </div>
<div class="table-val">{{public-uri}}</div>
</div>
{% endif %}
{% if type %}
<div class="table-row">
<div class="table-key">TYPE: </div>
<div class="table-val">{{type}}</div>
</div>
{% endif %}
{% if code %}
<div class="table-row">
<div class="table-key">CODE: </div>
<div class="table-val">{{code}}</div>
</div>
{% endif %}
{% if error %}
<div class="table-row">
<div class="table-key">CLSS: </div>
<div class="table-val">{{error.class}}</div>
</div>
{% endif %}
{% if error %}
<div class="table-row">
<div class="table-key">HINT: </div>
<div class="table-val">{{error.message}}</div>
</div>
{% endif %}
{% if method %}
<div class="table-row">
<div class="table-key">PATH: </div>
<div class="table-val">{{method|upper}} {{path}}</div>
</div>
{% endif %}
{% if explain %}
<div>(<a href="#explain">go to explain</a>)</div>
{% endif %}
{% if data %}
<div>(<a href="#edata">go to edata</a>)</div>
{% endif %}
{% if error %}
<div>(<a href="#trace">go to trace</a>)</div>
{% endif %}
{% if params %}
<div id="params" class="table-row multiline">
<div class="table-key">PARAMS: </div>
<div class="table-val">
<pre>{{params}}</pre>
</div>
</div>
{% endif %}
{% if explain %}
<div id="explain" class="table-row multiline">
<div class="table-key">EXPLAIN: </div>
<div class="table-val">
<pre>{{explain}}</pre>
</div>
</div>
{% endif %}
{% if data %}
<div id="edata" class="table-row multiline">
<div class="table-key">EDATA: </div>
<div class="table-val">
<pre>{{data}}</pre>
</div>
</div>
{% endif %}
{% if error %}
<div id="trace" class="table-row multiline">
<div class="table-key">TRACE:</div>
<div class="table-val">
<pre>{{error.trace}}</pre>
</div>
</div>
{% endif %}
</div>
</body>
</html>

View File

@@ -2,7 +2,7 @@
<Configuration status="info" monitorInterval="30">
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] [%t] %level{length=1} %logger{36} - %msg%n"/>
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"/>
</Console>
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">

View File

@@ -2,7 +2,7 @@
<Configuration status="info" monitorInterval="60">
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] [%t] %level{length=1} %logger{36} - %msg%n"/>
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"/>
</Console>
</Appenders>

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex,nofollow">
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
<style>
{% include "templates/styles.css" %}
</style>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,32 @@
{% extends "templates/base.tmpl" %}
{% block title %}
Debug Main Page
{% endblock %}
{% block content %}
<nav>
<h1>Debug INDEX:</h1>
<div>[<a href="/dbg/error">ERRORS</a>]</div>
</nav>
<main class="index">
<section>
<h2>Download file data:</h2>
<desc>Given an FILE-ID, downloads the file data as file. The file data is encoded using transit.</desc>
<form method="get" action="/dbg/file/data">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
<input type="hidden" name="download" value="1" />
<input type="submit" value="Download" />
</form>
</section>
<section>
<h2>Upload File Data:</h2>
<desc>Create a new file on your draft projects using the file downloaded from the previous section.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/data">
<input type="file" name="file" value="" />
<input type="submit" value="Upload" />
</form>
</section>
</main>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "templates/base.tmpl" %}
{% block title %}
penpot - error list
{% endblock %}
{% block content %}
<nav>
<h1>Latest error reports:</h1>
</nav>
<main class="horizontal-list">
<ul>
{% for item in items %}
<li><a href="/dbg/error/{{item.id}}">{{item.created-at}}</a></li>
{% endfor %}
</ul>
</main>
{% endblock %}

View File

@@ -0,0 +1,98 @@
{% extends "templates/base.tmpl" %}
{% block title %}
penpot - error report {{id}}
{% endblock %}
{% block content %}
<nav>
<div>[<a href="/dbg/error">⮜</a>]</div>
<div>[<a href="#context">context</a>]</div>
<div>[<a href="#params">request params</a>]</div>
{% if data %}
<div>[<a href="#edata">error data</a>]</div>
{% endif %}
{% if spec-explain %}
<div>[<a href="#spec-explain">spec explain</a>]</div>
{% endif %}
{% if spec-problems %}
<div>[<a href="#spec-problems">spec problems</a>]</div>
{% endif %}
{% if spec-value %}
<div>[<a href="#spec-value">spec value</a>]</div>
{% endif %}
{% if trace %}
<div>[<a href="#trace">error trace</a>]</div>
{% endif %}
</nav>
<main>
<div class="table">
<div class="table-row multiline">
<div id="context" class="table-key">CONTEXT: </div>
<div class="table-val">
<h1>{{hint}}</h1>
</div>
<div class="table-val">
<pre>{{context}}</pre>
</div>
</div>
{% if params %}
<div class="table-row multiline">
<div id="params" class="table-key">REQUEST PARAMS: </div>
<div class="table-val">
<pre>{{params}}</pre>
</div>
</div>
{% endif %}
{% if data %}
<div class="table-row multiline">
<div id="edata" class="table-key">ERROR DATA: </div>
<div class="table-val">
<pre>{{data}}</pre>
</div>
</div>
{% endif %}
{% if spec-explain %}
<div class="table-row multiline">
<div id="spec-explain" class="table-key">SPEC EXPLAIN: </div>
<div class="table-val">
<pre>{{spec-explain}}</pre>
</div>
</div>
{% endif %}
{% if spec-problems %}
<div class="table-row multiline">
<div id="spec-problems" class="table-key">SPEC PROBLEMS: </div>
<div class="table-val">
<pre>{{spec-problems}}</pre>
</div>
</div>
{% endif %}
{% if spec-value %}
<div class="table-row multiline">
<div id="spec-value" class="table-key">SPEC VALUE: </div>
<div class="table-val">
<pre>{{spec-value}}</pre>
</div>
</div>
{% endif %}
{% if trace %}
<div class="table-row multiline">
<div id="trace" class="table-key">TRACE:</div>
<div class="table-val">
<pre>{{trace}}</pre>
</div>
</div>
{% endif %}
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,150 @@
* {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
}
body {
margin: 0px;
padding: 0px;
}
pre {
margin: 0px;
line-height: 16px;
}
desc {
display: flex;
margin-bottom: 10px;
font-size: 10px;
color: #666;
}
input[type=text], input[type=submit] {
padding: 3px;
}
main {
margin: 20px;
}
nav {
position: fixed;
width: 100vw;
top: 0;
left: 0;
padding: 5px 20px;
display: flex;
background: #e3e3e3;
}
nav > h1 {
padding: 0px;
margin: 0px;
font-size: 11px;
}
nav > div {
text-transform: uppercase;
font-weight: bold;
}
nav > div:not(:last-child) {
margin-right: 10px;
}
.table {
margin-top: 25px;
display: flex;
flex-direction: column;
}
.table-row {
display: flex;
padding-bottom: 15px;
/* width: 100%; */
/* border: 1px solid red; */
}
.table-key {
font-weight: 600;
width: 60px;
padding: 4px;
padding-top: 40px;
margin-top: -40px;
}
.table-val {
font-weight: 200;
color: #333;
padding: 4px;
}
.multiline {
margin-top: 15px;
flex-direction: column;
}
.multiline .table-key {
margin-bottom: 10px;
border-bottom: 1px dashed #dddddd;
/* padding: 4px; */
width: unset;
}
.index {
margin-top: 40px;
}
.index > section {
padding: 10px;
background-color: #e3e3e3;
}
.index > section:not(:last-child) {
margin-bottom: 10px;
}
.index > section > h2 {
margin-top: 0px;
}
.horizontal-list {
margin: 20px;
margin-top: 40px;
}
.horizontal-list ul {
display: flex;
margin: 0px;
padding: 0px;
flex-direction: column;
flex-wrap: wrap;
height: calc(100vh - 75px);
justify-content: flex-start;
}
.horizontal-list li {
list-style: none;
padding: 0px;
margin: 0px;
line-height: 18px;
min-width: 210px;
margin: 0px 20px;
cursor: pointer;
display: flex;
justify-content: center;
border-radius: 3px;
}
.horizontal-list li:hover {
background-color: #e9e9e9;
}
.horizontal-list li > a {
text-decoration: none;
color: inherit;
}

View File

@@ -1,79 +1,21 @@
#!/usr/bin/env bb
#!/usr/bin/env bash
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
CURRENT_VERSION=$1;
(ns build
(:require
[clojure.string :as str]
[clojure.java.io :as io]
[clojure.pprint :refer [pprint]]
[babashka.fs :as fs]
[babashka.process :refer [$ check]]))
set -ex
(defn split-cp
[data]
(str/split data #":"))
rm -rf target;
mkdir -p target/classes;
mkdir -p target/dist;
echo "$CURRENT_VERSION" > target/classes/version.txt;
cp ../CHANGES.md target/classes/changelog.md;
(def classpath
(->> ($ clojure -Spath)
(check)
(:out)
(slurp)
(split-cp)
(map str/trim)))
clojure -T:build jar;
mv target/penpot.jar target/dist/penpot.jar
cp scripts/run.template.sh target/dist/run.sh;
cp scripts/manage.template.sh target/dist/manage.sh;
chmod +x target/dist/run.sh;
chmod +x target/dist/manage.sh;
(def classpath-jars
(let [xfm (filter #(str/ends-with? % ".jar"))]
(into #{} xfm classpath)))
(def classpath-paths
(let [xfm (comp (remove #(str/ends-with? % ".jar"))
(filter #(.isDirectory (io/file %))))]
(into #{} xfm classpath)))
(def version
(or (first *command-line-args*) "%version%"))
;; Clean previous dist
(-> ($ rm -rf "./target/dist") check)
;; Create a new dist
(-> ($ mkdir -p "./target/dist/deps") check)
;; Copy all jar deps into dist
(run! (fn [item] (-> ($ cp ~item "./target/dist/deps/") check)) classpath-jars)
;; Create the application jar
(spit "./target/dist/version.txt" version)
(-> ($ jar cvf "./target/dist/deps/app.jar" -C ~(first classpath-paths) ".") check)
(-> ($ jar uvf "./target/dist/deps/app.jar" -C "./target/dist" "version.txt") check)
(run! (fn [item]
(-> ($ jar uvf "./target/dist/deps/app.jar" -C ~item ".") check))
(rest classpath-paths))
;; Copy logging configuration
(-> ($ cp "./resources/log4j2.xml" "./target/dist/") check)
;; Create classpath file
(let [jars (->> (into ["app.jar"] classpath-jars)
(map fs/file-name)
(map #(fs/path "deps" %))
(map str))]
(spit "./target/dist/classpath" (str/join ":" jars)))
;; Copy run script template
(-> ($ cp "./scripts/run.template.sh" "./target/dist/run.sh") check)
;; Copy run script template
(-> ($ cp "./scripts/manage.template.sh" "./target/dist/manage.sh") check)
;; Add exec permisions to scripts.
(-> ($ chmod +x "./target/dist/run.sh") check)
(-> ($ chmod +x "./target/dist/manage.sh") check)
nil

View File

@@ -16,4 +16,4 @@ if [ -f ./environ ]; then
source ./environ
fi
exec $JAVA_CMD $JVM_OPTS -classpath $(cat classpath) -Dlog4j2.configurationFile=./log4j2.xml clojure.main -m app.cli.manage "$@"
exec $JAVA_CMD $JVM_OPTS -jar penpot.jar -m app.cli.manage "$@"

View File

@@ -1,10 +1,44 @@
#!/usr/bin/env bash
export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS"
export PENPOT_HOST=devenv
export PENPOT_TENANT=dev
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies"
export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Dlog4j2.configurationFile=log4j2-devenv.xml -J-Djdk.attach.allowAttachSelf -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m";
# export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions";
# export OPTIONS="$OPTIONS -J-XX:-TieredCompilation -J-XX:CompileThreshold=10000";
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot"
# export PENPOT_DATABASE_USERNAME="penpot"
# export PENPOT_DATABASE_PASSWORD="penpot"
# export PENPOT_DATABASE_READONLY=true
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot_pre"
# export PENPOT_DATABASE_USERNAME="penpot_pre"
# export PENPOT_DATABASE_PASSWORD="penpot_pre"
# export PENPOT_LOGGERS_LOKI_URI="http://172.17.0.1:3100/loki/api/v1/push"
# export PENPOT_AUDIT_LOG_ARCHIVE_URI="http://localhost:6070/api/audit"
# Initialize MINIO config
mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin
mc admin user add penpot-s3 penpot-devenv penpot-devenv
mc admin policy set penpot-s3 readwrite user=penpot-devenv
mc mb penpot-s3/penpot -p
export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
export PENPOT_STORAGE_ASSETS_S3_REGION=eu-central-1
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
export OPTIONS="
-A:dev:jmx-remote \
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
-J-XX:+UseG1GC \
-J-XX:-OmitStackTraceInFastThrow \
-J-Xms50m -J-Xmx1024m \
-J-Djdk.attach.allowAttachSelf \
-J-XX:+UnlockDiagnosticVMOptions \
-J-XX:+DebugNonSafepoints";
export OPTIONS_EVAL="nil"
# export OPTIONS_EVAL="(set! *warn-on-reflection* true)"

View File

@@ -17,4 +17,4 @@ if [ -f ./environ ]; then
fi
set -x
exec $JAVA_CMD $JVM_OPTS -classpath "$(cat classpath)" -Dlog4j2.configurationFile=./log4j2.xml "$@" clojure.main -m app.main
exec $JAVA_CMD $JVM_OPTS "$@" -jar penpot.jar -m app.main

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env bash
export PENPOT_FLAGS="$PENPOT_FLAGS enable-asserts"
export PENPOT_HOST=devenv
export PENPOT_TENANT=dev
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies"
set -ex

View File

@@ -140,7 +140,6 @@
indicating the action the program should take and the options provided."
[args]
(let [{:keys [options arguments errors summary] :as opts} (parse-opts args cli-options)]
;; (pp/pprint opts)
(cond
(:help options) ; help => exit OK with usage summary
{:exit-message (usage summary) :ok? true}

View File

@@ -1,129 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.cli.migrate-media
(:require
[app.common.logging :as l]
[app.common.media :as cm]
[app.config :as cf]
[app.db :as db]
[app.main :as main]
[app.storage :as sto]
[cuerdas.core :as str]
[datoteka.core :as fs]
[integrant.core :as ig]))
(declare migrate-profiles)
(declare migrate-teams)
(declare migrate-file-media)
(defn run-in-system
[system]
(db/with-atomic [conn (:app.db/pool system)]
(let [system (assoc system ::conn conn)]
(migrate-profiles system)
(migrate-teams system)
(migrate-file-media system))
system))
(defn run
[]
(let [config (select-keys main/system-config
[:app.db/pool
:app.migrations/migrations
:app.metrics/metrics
:app.storage.s3/backend
:app.storage.db/backend
:app.storage.fs/backend
:app.storage/storage])]
(ig/load-namespaces config)
(try
(-> (ig/prep config)
(ig/init)
(run-in-system)
(ig/halt!))
(catch Exception e
(l/error :hint "unhandled exception" :cause e)))))
;; --- IMPL
(defn migrate-profiles
[{:keys [::conn] :as system}]
(letfn [(retrieve-profiles [conn]
(->> (db/exec! conn ["select * from profile"])
(filter #(not (str/empty? (:photo %))))
(seq)))]
(let [base (fs/path (cf/get :storage-fs-old-directory))
storage (-> (:app.storage/storage system)
(assoc :conn conn))]
(doseq [profile (retrieve-profiles conn)]
(let [path (fs/path (:photo profile))
full (-> (fs/join base path)
(fs/normalize))
ext (fs/ext path)
mtype (cm/format->mtype (keyword ext))
obj (sto/put-object storage {:content (sto/content full)
:content-type mtype})]
(db/update! conn :profile
{:photo-id (:id obj)}
{:id (:id profile)}))))))
(defn migrate-teams
[{:keys [::conn] :as system}]
(letfn [(retrieve-teams [conn]
(->> (db/exec! conn ["select * from team"])
(filter #(not (str/empty? (:photo %))))
(seq)))]
(let [base (fs/path (cf/get :storage-fs-old-directory))
storage (-> (:app.storage/storage system)
(assoc :conn conn))]
(doseq [team (retrieve-teams conn)]
(let [path (fs/path (:photo team))
full (-> (fs/join base path)
(fs/normalize))
ext (fs/ext path)
mtype (cm/format->mtype (keyword ext))
obj (sto/put-object storage {:content (sto/content full)
:content-type mtype})]
(db/update! conn :team
{:photo-id (:id obj)}
{:id (:id team)}))))))
(defn migrate-file-media
[{:keys [::conn] :as system}]
(letfn [(retrieve-media-objects [conn]
(->> (db/exec! conn ["select fmo.id, fmo.path, fth.path as thumbnail_path
from file_media_object as fmo
join file_media_thumbnail as fth on (fth.media_object_id = fmo.id)"])
(seq)))]
(let [base (fs/path (cf/get :storage-fs-old-directory))
storage (-> (:app.storage/storage system)
(assoc :conn conn))]
(doseq [mobj (retrieve-media-objects conn)]
(let [img-path (fs/path (:path mobj))
thm-path (fs/path (:thumbnail-path mobj))
img-path (-> (fs/join base img-path)
(fs/normalize))
thm-path (-> (fs/join base thm-path)
(fs/normalize))
img-ext (fs/ext img-path)
thm-ext (fs/ext thm-path)
img-mtype (cm/format->mtype (keyword img-ext))
thm-mtype (cm/format->mtype (keyword thm-ext))
img-obj (sto/put-object storage {:content (sto/content img-path)
:content-type img-mtype})
thm-obj (sto/put-object storage {:content (sto/content thm-path)
:content-type thm-mtype})]
(db/update! conn :file-media-object
{:media-id (:id img-obj)
:thumbnail-id (:id thm-obj)}
{:id (:id mobj)}))))))

View File

@@ -41,19 +41,22 @@
data))
(def defaults
{:http-server-port 6060
:host "devenv"
:tenant "dev"
{
:database-uri "postgresql://postgres/penpot"
:database-username "penpot"
:database-password "penpot"
:default-blob-version 3
:default-blob-version 4
:loggers-zmq-uri "tcp://localhost:45556"
:public-uri "http://localhost:3449"
:redis-uri "redis://redis/0"
:file-change-snapshot-every 5
:file-change-snapshot-timeout "3h"
:public-uri "http://localhost:3449"
:host "localhost"
:tenant "main"
:redis-uri "redis://redis/0"
:srepl-host "127.0.0.1"
:srepl-port 6062
@@ -61,10 +64,6 @@
:storage-assets-fs-directory "assets"
:assets-path "/internal/assets/"
:rlimits-password 10
:rlimits-image 2
:smtp-default-reply-to "Penpot <no-reply@example.com>"
:smtp-default-from "Penpot <no-reply@example.com>"
@@ -87,7 +86,7 @@
(s/def ::flags ::us/set-of-keywords)
;; DEPRECATED PROPERTIES: should be removed in 1.10
;; DEPRECATED PROPERTIES
(s/def ::registration-enabled ::us/boolean)
(s/def ::smtp-enabled ::us/boolean)
(s/def ::telemetry-enabled ::us/boolean)
@@ -97,12 +96,25 @@
(s/def ::audit-log-archive-uri ::us/string)
(s/def ::audit-log-gc-max-age ::dt/duration)
(s/def ::admins ::us/set-of-str)
(s/def ::file-change-snapshot-every ::us/integer)
(s/def ::file-change-snapshot-timeout ::dt/duration)
(s/def ::default-executor-parallelism ::us/integer)
(s/def ::blocking-executor-parallelism ::us/integer)
(s/def ::worker-executor-parallelism ::us/integer)
(s/def ::secret-key ::us/string)
(s/def ::allow-demo-users ::us/boolean)
(s/def ::assets-path ::us/string)
(s/def ::authenticated-cookie-domain ::us/string)
(s/def ::database-password (s/nilable ::us/string))
(s/def ::database-uri ::us/string)
(s/def ::database-username (s/nilable ::us/string))
(s/def ::database-readonly ::us/boolean)
(s/def ::database-min-pool-size ::us/integer)
(s/def ::database-max-pool-size ::us/integer)
(s/def ::default-blob-version ::us/integer)
(s/def ::error-report-webhook ::us/string)
(s/def ::user-feedback-destination ::us/string)
@@ -122,8 +134,15 @@
(s/def ::oidc-scopes ::us/set-of-str)
(s/def ::oidc-roles ::us/set-of-str)
(s/def ::oidc-roles-attr ::us/keyword)
(s/def ::oidc-email-attr ::us/keyword)
(s/def ::oidc-name-attr ::us/keyword)
(s/def ::host ::us/string)
(s/def ::http-server-port ::us/integer)
(s/def ::http-server-host ::us/string)
(s/def ::http-server-max-body-size ::us/integer)
(s/def ::http-server-max-multipart-body-size ::us/integer)
(s/def ::http-server-io-threads ::us/integer)
(s/def ::http-server-worker-threads ::us/integer)
(s/def ::http-session-idle-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-size ::us/integer)
@@ -151,8 +170,10 @@
(s/def ::public-uri ::us/string)
(s/def ::redis-uri ::us/string)
(s/def ::registration-domain-whitelist ::us/set-of-str)
(s/def ::rlimits-image ::us/integer)
(s/def ::rlimits-password ::us/integer)
(s/def ::rlimit-font ::us/integer)
(s/def ::rlimit-file-update ::us/integer)
(s/def ::rlimit-image ::us/integer)
(s/def ::rlimit-password ::us/integer)
(s/def ::smtp-default-from ::us/string)
(s/def ::smtp-default-reply-to ::us/string)
(s/def ::smtp-host ::us/string)
@@ -168,9 +189,11 @@
(s/def ::storage-assets-fs-directory ::us/string)
(s/def ::storage-assets-s3-bucket ::us/string)
(s/def ::storage-assets-s3-region ::us/keyword)
(s/def ::storage-assets-s3-endpoint ::us/string)
(s/def ::storage-fdata-s3-bucket ::us/string)
(s/def ::storage-fdata-s3-region ::us/keyword)
(s/def ::storage-fdata-s3-prefix ::us/string)
(s/def ::storage-fdata-s3-endpoint ::us/string)
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
@@ -183,14 +206,24 @@
(s/def ::config
(s/keys :opt-un [::secret-key
::flags
::admins
::allow-demo-users
::audit-log-archive-uri
::audit-log-gc-max-age
::authenticated-cookie-domain
::database-password
::database-uri
::database-username
::database-readonly
::database-min-pool-size
::database-max-pool-size
::default-blob-version
::error-report-webhook
::default-executor-parallelism
::blocking-executor-parallelism
::worker-executor-parallelism
::file-change-snapshot-every
::file-change-snapshot-timeout
::user-feedback-destination
::github-client-id
::github-client-secret
@@ -207,9 +240,16 @@
::oidc-user-uri
::oidc-scopes
::oidc-roles-attr
::oidc-email-attr
::oidc-name-attr
::oidc-roles
::host
::http-server-host
::http-server-port
::http-server-max-body-size
::http-server-max-multipart-body-size
::http-server-io-threads
::http-server-worker-threads
::http-session-idle-max-age
::http-session-updater-batch-max-age
::http-session-updater-batch-max-size
@@ -237,8 +277,10 @@
::redis-uri
::registration-domain-whitelist
::registration-enabled
::rlimits-image
::rlimits-password
::rlimit-font
::rlimit-file-update
::rlimit-image
::rlimit-password
::sentry-dsn
::sentry-debug
::sentry-attach-stack-trace
@@ -258,10 +300,12 @@
::storage-assets-fs-directory
::storage-assets-s3-bucket
::storage-assets-s3-region
::storage-assets-s3-endpoint
::fdata-storage-backend
::storage-fdata-s3-bucket
::storage-fdata-s3-region
::storage-fdata-s3-prefix
::storage-fdata-s3-endpoint
::telemetry-enabled
::telemetry-uri
::telemetry-referer
@@ -269,8 +313,7 @@
::tenant]))
(def default-flags
[:enable-backend-asserts
:enable-backend-api-doc
[:enable-backend-api-doc
:enable-secure-session-cookies])
(defn- parse-flags
@@ -301,8 +344,8 @@
(when (ex/ex-info? e)
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
(println "Error on validating configuration:")
(println (:explain (ex-data e))
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")))
(println (us/pretty-explain (ex-data e)))
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"))
(throw e))))
(def version

View File

@@ -27,14 +27,16 @@
com.zaxxer.hikari.HikariConfig
com.zaxxer.hikari.HikariDataSource
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
java.io.InputStream
java.io.OutputStream
java.lang.AutoCloseable
java.sql.Connection
java.sql.Savepoint
org.postgresql.PGConnection
org.postgresql.geometric.PGpoint
org.postgresql.jdbc.PgArray
org.postgresql.largeobject.LargeObject
org.postgresql.largeobject.LargeObjectManager
org.postgresql.jdbc.PgArray
org.postgresql.util.PGInterval
org.postgresql.util.PGobject))
@@ -45,43 +47,63 @@
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare instrument-jdbc!)
(declare apply-migrations!)
(s/def ::name keyword?)
(s/def ::uri ::us/not-empty-string)
(s/def ::min-pool-size ::us/integer)
(s/def ::max-pool-size ::us/integer)
(s/def ::connection-timeout ::us/integer)
(s/def ::max-size ::us/integer)
(s/def ::min-size ::us/integer)
(s/def ::migrations map?)
(s/def ::name keyword?)
(s/def ::password ::us/string)
(s/def ::read-only ::us/boolean)
(s/def ::uri ::us/not-empty-string)
(s/def ::username ::us/string)
(s/def ::validation-timeout ::us/integer)
(defmethod ig/pre-init-spec ::pool [_]
(s/keys :req-un [::uri ::name ::min-pool-size ::max-pool-size]
:opt-un [::migrations ::mtx/metrics ::read-only]))
(s/keys :req-un [::uri ::name
::min-size
::max-size
::connection-timeout
::validation-timeout]
:opt-un [::migrations
::username
::password
::mtx/metrics
::read-only]))
(defmethod ig/prep-key ::pool
[_ cfg]
(merge {:name :main
:min-size 0
:max-size 30
:connection-timeout 10000
:validation-timeout 10000
:idle-timeout 120000 ; 2min
:max-lifetime 1800000 ; 30m
:read-only false}
(d/without-nils cfg)))
(defmethod ig/init-key ::pool
[_ {:keys [migrations metrics name] :as cfg}]
(l/info :action "initialize connection pool" :name (d/name name) :uri (:uri cfg))
(some-> metrics :registry instrument-jdbc!)
[_ {:keys [migrations name read-only] :as cfg}]
(l/info :hint "initialize connection pool"
:name (d/name name)
:uri (:uri cfg)
:read-only read-only
:with-credentials (and (contains? cfg :username)
(contains? cfg :password))
:min-size (:min-size cfg)
:max-size (:max-size cfg))
(let [pool (create-pool cfg)]
(some->> (seq migrations) (apply-migrations! pool))
(when-not read-only
(some->> (seq migrations) (apply-migrations! pool)))
pool))
(defmethod ig/halt-key! ::pool
[_ pool]
(.close ^HikariDataSource pool))
(defn- instrument-jdbc!
[registry]
(mtx/instrument-vars!
[#'next.jdbc/execute-one!
#'next.jdbc/execute!]
{:registry registry
:type :counter
:name "database_query_total"
:help "An absolute counter of database queries."}))
(defn- apply-migrations!
[pool migrations]
(with-open [conn ^AutoCloseable (open pool)]
@@ -94,26 +116,23 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def initsql
(str "SET statement_timeout = 200000;\n"
"SET idle_in_transaction_session_timeout = 200000;"))
(str "SET statement_timeout = 300000;\n"
"SET idle_in_transaction_session_timeout = 300000;"))
(defn- create-datasource-config
[{:keys [metrics read-only] :or {read-only false} :as cfg}]
(let [dburi (:uri cfg)
username (:username cfg)
password (:password cfg)
config (HikariConfig.)]
[{:keys [metrics uri] :as cfg}]
(let [config (HikariConfig.)]
(doto config
(.setJdbcUrl (str "jdbc:" dburi))
(.setPoolName (d/name (:name cfg)))
(.setJdbcUrl (str "jdbc:" uri))
(.setPoolName (d/name (:name cfg)))
(.setAutoCommit true)
(.setReadOnly read-only)
(.setConnectionTimeout 10000) ;; 10seg
(.setValidationTimeout 10000) ;; 10seg
(.setIdleTimeout 120000) ;; 2min
(.setMaxLifetime 1800000) ;; 30min
(.setMinimumIdle (:min-pool-size cfg 0))
(.setMaximumPoolSize (:max-pool-size cfg 30))
(.setReadOnly (:read-only cfg))
(.setConnectionTimeout (:connection-timeout cfg))
(.setValidationTimeout (:validation-timeout cfg))
(.setIdleTimeout (:idle-timeout cfg))
(.setMaxLifetime (:max-lifetime cfg))
(.setMinimumIdle (:min-size cfg))
(.setMaximumPoolSize (:max-size cfg))
(.setConnectionInitSql initsql)
(.setInitializationFailTimeout -1))
@@ -123,8 +142,8 @@
(PrometheusMetricsTrackerFactory.)
(.setMetricsTrackerFactory config)))
(when username (.setUsername config username))
(when password (.setPassword config password))
(some->> ^String (:username cfg) (.setUsername config))
(some->> ^String (:password cfg) (.setPassword config))
config))
@@ -134,10 +153,14 @@
(s/def ::pool pool?)
(defn pool-closed?
(defn closed?
[pool]
(.isClosed ^HikariDataSource pool))
(defn read-only?
[pool]
(.isReadOnly ^HikariDataSource pool))
(defn create-pool
[cfg]
(let [dsc (create-datasource-config cfg)]
@@ -210,21 +233,21 @@
([ds table params opts]
(exec-one! ds
(sql/insert table params opts)
(assoc opts :return-keys true))))
(merge {:return-keys true} opts))))
(defn insert-multi!
([ds table cols rows] (insert-multi! ds table cols rows nil))
([ds table cols rows opts]
(exec! ds
(sql/insert-multi table cols rows opts)
(assoc opts :return-keys true))))
(merge {:return-keys true} opts))))
(defn update!
([ds table params where] (update! ds table params where nil))
([ds table params where opts]
(exec-one! ds
(sql/update table params where opts)
(assoc opts :return-keys true))))
(merge {:return-keys true} opts))))
(defn delete!
([ds table params] (delete! ds table params nil))
@@ -356,7 +379,7 @@
val (.getValue o)]
(if (or (= typ "json")
(= typ "jsonb"))
(json/decode-str val)
(json/read val)
val)))
(defn decode-transit-pgobject
@@ -392,7 +415,7 @@
[data]
(doto (org.postgresql.util.PGobject.)
(.setType "jsonb")
(.setValue (json/encode-str data))))
(.setValue (json/write-str data))))
;; --- Locks

View File

@@ -8,6 +8,7 @@
"Main api for send emails."
(:require
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
@@ -66,8 +67,8 @@
(:id profile)
(db/interval bounce-max-age)])]
(and (< complaints complaint-threshold)
(< bounces bounce-threshold)))))
(and (< (or complaints 0) complaint-threshold)
(< (or bounces 0) bounce-threshold)))))
(defn has-complaint-reports?
([conn email] (has-complaint-reports? conn email nil))
@@ -165,19 +166,25 @@
(let [enabled? (or (contains? cf/flags :smtp)
(cf/get :smtp-enabled)
(:enabled task))]
(if enabled?
(emails/send! cfg props)
(when enabled?
(emails/send! cfg props))
(when (contains? cf/flags :log-emails)
(send-console! cfg props)))))
(defn- send-console!
[cfg email]
(let [baos (java.io.ByteArrayOutputStream.)
mesg (emails/smtp-message cfg email)]
(.writeTo mesg baos)
(let [out (with-out-str
(println "email console dump:")
(println "******** start email" (:id email) "**********")
(println (.toString baos))
(println "******** end email "(:id email) "**********"))]
(l/info :email out))))
[_ email]
(let [body (:body email)
out (with-out-str
(println "email console dump:")
(println "******** start email" (:id email) "**********")
(pp/pprint (dissoc email :body))
(if (string? body)
(println body)
(println (->> body
(filter #(= "text/plain" (:type %)))
(map :content)
first)))
(println "******** end email" (:id email) "**********"))]
(l/info ::l/raw out)))

View File

@@ -7,162 +7,176 @@
(ns app.http
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.transit :as t]
[app.http.doc :as doc]
[app.http.errors :as errors]
[app.http.middleware :as middleware]
[app.metrics :as mtx]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[reitit.ring :as rr]
[ring.adapter.jetty9 :as jetty])
(:import
org.eclipse.jetty.server.Server
org.eclipse.jetty.server.handler.ErrorHandler
org.eclipse.jetty.server.handler.StatisticsHandler))
[reitit.core :as r]
[reitit.middleware :as rr]
[yetti.adapter :as yt]
[yetti.request :as yrq]
[yetti.response :as yrs]))
(declare router-handler)
(declare wrap-router)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP SERVER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::handler fn?)
(s/def ::router some?)
(s/def ::ws (s/map-of ::us/string fn?))
(s/def ::port ::us/integer)
(s/def ::name ::us/string)
(s/def ::port integer?)
(s/def ::host string?)
(s/def ::name string?)
(defmethod ig/pre-init-spec ::server [_]
(s/keys :req-un [::port]
:opt-un [::ws ::name ::mtx/metrics ::router ::handler]))
(s/def ::max-body-size integer?)
(s/def ::max-multipart-body-size integer?)
(s/def ::io-threads integer?)
(s/def ::worker-threads integer?)
(defmethod ig/prep-key ::server
[_ cfg]
(merge {:name "http"} (d/without-nils cfg)))
(merge {:name "http"
:port 6060
:host "0.0.0.0"
:max-body-size (* 1024 1024 30) ; 30 MiB
:max-multipart-body-size (* 1024 1024 120)} ; 120 MiB
(d/without-nils cfg)))
(defmethod ig/pre-init-spec ::server [_]
(s/and
(s/keys :req-un [::port ::host ::name ::max-body-size ::max-multipart-body-size]
:opt-un [::router ::handler ::io-threads ::worker-threads ::wrk/executor])
(fn [cfg]
(or (contains? cfg :router)
(contains? cfg :handler)))))
(defmethod ig/init-key ::server
[_ {:keys [handler router ws port name metrics] :as opts}]
(l/info :msg "starting http server" :port port :name name)
(let [pre-start (fn [^Server server]
(let [handler (doto (ErrorHandler.)
(.setShowStacks true)
(.setServer server))]
(.setErrorHandler server ^ErrorHandler handler)
(when metrics
(let [stats (StatisticsHandler.)]
(.setHandler ^StatisticsHandler stats (.getHandler server))
(.setHandler server stats)
(mtx/instrument-jetty! (:registry metrics) stats)))))
options (merge
{:port port
:h2c? true
:join? false
:allow-null-path-info true
:configurator pre-start}
(when (seq ws)
{:websockets ws}))
handler (cond
(fn? handler) handler
(some? router) (router-handler router)
:else (ex/raise :type :internal
:code :invalid-argument
:hint "Missing `handler` or `router` option."))
server (jetty/run-jetty handler options)]
(assoc opts :server server)))
[_ {:keys [handler router port name host] :as cfg}]
(l/info :hint "starting http server" :port port :host host :name name)
(let [options {:http/port port
:http/host host
:http/max-body-size (:max-body-size cfg)
:http/max-multipart-body-size (:max-multipart-body-size cfg)
:xnio/io-threads (:io-threads cfg)
:xnio/worker-threads (:worker-threads cfg)
:xnio/dispatch (:executor cfg)
:ring/async true}
handler (if (some? router)
(wrap-router router)
handler)
server (yt/server handler (d/without-nils options))]
(assoc cfg :server (yt/start! server))))
(defmethod ig/halt-key! ::server
[_ {:keys [server name port] :as opts}]
(l/info :msg "stoping http server"
:name name
:port port)
(jetty/stop-server server))
[_ {:keys [server name port] :as cfg}]
(l/info :msg "stoping http server" :name name :port port)
(yt/stop! server))
(defn- router-handler
(defn- not-found-handler
[_ respond _]
(respond (yrs/response 404)))
(defn- wrap-router
[router]
(let [handler (rr/ring-handler router
(rr/routes
(rr/create-resource-handler {:path "/"})
(rr/create-default-handler))
{:middleware [middleware/server-timing]})]
(fn [request]
(try
(handler request)
(catch Throwable e
(try
(let [cdata (errors/get-error-context request e)]
(l/update-thread-context! cdata)
(l/error :hint "unhandled exception"
:message (ex-message e)
:error-id (str (:id cdata))
:cause e))
{:status 500 :body "internal server error"}
(catch Throwable e
(l/error :hint "unhandled exception"
:message (ex-message e)
:cause e)
{:status 500 :body "internal server error"})))))))
(letfn [(handler [request respond raise]
(if-let [match (r/match-by-path router (yrq/path request))]
(let [params (:path-params match)
result (:result match)
handler (or (:handler result) not-found-handler)
request (-> request
(assoc :path-params params)
(update :params merge params))]
(handler request respond raise))
(not-found-handler request respond raise)))
(on-error [cause request respond]
(let [{:keys [body] :as response} (errors/handle cause request)]
(respond
(cond-> response
(map? body)
(-> (update :headers assoc "content-type" "application/transit+json")
(assoc :body (t/encode-str body {:type :json-verbose})))))))]
(fn [request respond _]
(try
(handler request respond #(on-error % request respond))
(catch Throwable cause
(on-error cause request respond))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Http Main Handler (Router)
;; HTTP ROUTER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::rpc map?)
(s/def ::session map?)
(s/def ::oauth map?)
(s/def ::storage map?)
(s/def ::assets map?)
(s/def ::feedback fn?)
(s/def ::error-report-handler fn?)
(s/def ::audit-http-handler fn?)
(s/def ::ws fn?)
(s/def ::audit-handler fn?)
(s/def ::debug map?)
(s/def ::awsns-handler fn?)
(s/def ::session map?)
(defmethod ig/pre-init-spec ::router [_]
(s/keys :req-un [::rpc ::session ::mtx/metrics
::oauth ::storage ::assets ::feedback
::error-report-handler
::audit-http-handler]))
(s/keys :req-un [::rpc ::mtx/metrics ::ws ::oauth ::storage ::assets
::session ::feedback ::awsns-handler ::debug ::audit-handler]))
(defmethod ig/init-key ::router
[_ {:keys [session rpc oauth metrics assets feedback] :as cfg}]
[_ {:keys [ws session rpc oauth metrics assets feedback debug] :as cfg}]
(rr/router
[["/metrics" {:get (:handler metrics)}]
["/assets" {:middleware [[middleware/format-response-body]
[middleware/errors errors/handle]
[middleware/cookies]
(:middleware session)]}
["/by-id/:id" {:get (:objects-handler assets)}]
["/by-file-media-id/:id" {:get (:file-objects-handler assets)}]
["/by-file-media-id/:id/thumbnail" {:get (:file-thumbnails-handler assets)}]]
[["" {:middleware [[middleware/server-timing]
[middleware/format-response]
[middleware/params]
[middleware/parse-request]
[middleware/errors errors/handle]
[middleware/restrict-methods]]}
["/metrics" {:handler (:handler metrics)}]
["/assets" {:middleware [(:middleware session)]}
["/by-id/:id" {:handler (:objects-handler assets)}]
["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}]
["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]]
["/dbg"
["/error-by-id/:id" {:get (:error-report-handler cfg)}]]
["/dbg" {:middleware [(:middleware session)]}
["" {:handler (:index debug)}]
["/changelog" {:handler (:changelog debug)}]
["/error-by-id/:id" {:handler (:retrieve-error debug)}]
["/error/:id" {:handler (:retrieve-error debug)}]
["/error" {:handler (:retrieve-error-list debug)}]
["/file/data" {:handler (:file-data debug)}]
["/file/changes" {:handler (:retrieve-file-changes debug)}]]
["/webhooks"
["/sns" {:post (:sns-webhook cfg)}]]
["/webhooks"
["/sns" {:handler (:awsns-handler cfg)
:allowed-methods #{:post}}]]
["/api" {:middleware [[middleware/cors]
[middleware/etag]
[middleware/format-response-body]
[middleware/params]
[middleware/multipart-params]
[middleware/keyword-params]
[middleware/parse-request-body]
[middleware/errors errors/handle]
[middleware/cookies]]}
["/ws/notifications" {:middleware [(:middleware session)]
:handler ws
:allowed-methods #{:get}}]
["/_doc" {:get (doc/handler rpc)}]
["/api" {:middleware [[middleware/cors]
(:middleware session)]}
["/health" {:handler (:health-check debug)}]
["/_doc" {:handler (doc/handler rpc)
:allowed-methods #{:get}}]
["/feedback" {:handler feedback
:allowed-methods #{:post}}]
["/feedback" {:middleware [(:middleware session)]
:post feedback}]
["/auth/oauth/:provider" {:post (:handler oauth)}]
["/auth/oauth/:provider/callback" {:get (:callback-handler oauth)}]
["/auth/oauth/:provider" {:handler (:handler oauth)
:allowed-methods #{:post}}]
["/auth/oauth/:provider/callback" {:handler (:callback-handler oauth)
:allowed-methods #{:get}}]
["/audit/events" {:middleware [(:middleware session)]
:post (:audit-http-handler cfg)}]
["/audit/events" {:handler (:audit-handler cfg)
:allowed-methods #{:post}}]
["/rpc" {:middleware [(:middleware session)]}
["/query/:type" {:get (:query-handler rpc)
:post (:query-handler rpc)}]
["/mutation/:type" {:post (:mutation-handler rpc)}]]]]))
["/rpc"
["/query/:type" {:handler (:query-handler rpc)}]
["/mutation/:type" {:handler (:mutation-handler rpc)
:allowed-methods #{:post}}]]]]]))

View File

@@ -14,8 +14,12 @@
[app.metrics :as mtx]
[app.storage :as sto]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.response :as yrs]))
(def ^:private cache-max-age
(dt/duration {:hours 24}))
@@ -32,66 +36,83 @@
res))
(defn- get-file-media-object
[{:keys [pool] :as storage} id]
(let [id (coerce-id id)
mobj (db/exec-one! pool ["select * from file_media_object where id=?" id])]
(when-not mobj
(ex/raise :type :not-found
:hint "object does not found"))
mobj))
[{:keys [pool executor] :as storage} id]
(px/with-dispatch executor
(let [id (coerce-id id)
mobj (db/exec-one! pool ["select * from file_media_object where id=?" id])]
(when-not mobj
(ex/raise :type :not-found
:hint "object does not found"))
mobj)))
(defn- serve-object
"Helper function that returns the appropriate response depending on
the storage object backend type."
[{:keys [storage] :as cfg} obj]
(let [mdata (meta obj)
backend (sto/resolve-backend storage (:backend obj))]
(case (:type backend)
:db
{:status 200
:headers {"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}
:body (sto/get-object-bytes storage obj)}
(p/let [body (sto/get-object-bytes storage obj)]
(yrs/response :status 200
:body body
:headers {"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}))
:s3
(let [url (sto/get-object-url storage obj {:max-age signature-max-age})]
{:status 307
:headers {"location" (str url)
"x-host" (:host url)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}
:body ""})
(p/let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
(yrs/response :status 307
:headers {"location" (str url)
"x-host" (cond-> host port (str ":" port))
"cache-control" (str "max-age=" (inst-ms cache-max-age))}))
:fs
(let [purl (u/uri (:assets-path cfg))
purl (u/join purl (sto/object->relative-path obj))]
{:status 204
:headers {"x-accel-redirect" (:path purl)
"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}
:body ""}))))
(defn- generic-handler
[{:keys [storage] :as cfg} _request id]
(let [obj (sto/get-object storage id)]
(if obj
(serve-object cfg obj)
{:status 404 :body ""})))
(p/let [purl (u/uri (:assets-path cfg))
purl (u/join purl (sto/object->relative-path obj))]
(yrs/response :status 204
:headers {"x-accel-redirect" (:path purl)
"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))})))))
(defn objects-handler
[cfg request]
(let [id (get-in request [:path-params :id])]
(generic-handler cfg request (coerce-id id))))
"Handler that servers storage objects by id."
[{:keys [storage executor] :as cfg} request respond raise]
(-> (px/with-dispatch executor
(p/let [id (get-in request [:path-params :id])
id (coerce-id id)
obj (sto/get-object storage id)]
(if obj
(serve-object cfg obj)
(yrs/response 404))))
(p/bind p/wrap)
(p/then' respond)
(p/catch raise)))
(defn- generic-handler
"A generic handler helper/common code for file-media based handlers."
[{:keys [storage] :as cfg} request kf]
(p/let [id (get-in request [:path-params :id])
mobj (get-file-media-object storage id)
obj (sto/get-object storage (kf mobj))]
(if obj
(serve-object cfg obj)
(yrs/response 404))))
(defn file-objects-handler
[{:keys [storage] :as cfg} request]
(let [id (get-in request [:path-params :id])
mobj (get-file-media-object storage id)]
(generic-handler cfg request (:media-id mobj))))
"Handler that serves storage objects by file media id."
[cfg request respond raise]
(-> (generic-handler cfg request :media-id)
(p/then respond)
(p/catch raise)))
(defn file-thumbnails-handler
[{:keys [storage] :as cfg} request]
(let [id (get-in request [:path-params :id])
mobj (get-file-media-object storage id)]
(generic-handler cfg request (or (:thumbnail-id mobj) (:media-id mobj)))))
"Handler that serves storage objects by thumbnail-id and quick
fallback to file-media-id if no thumbnail is available."
[cfg request respond raise]
(-> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))
(p/then respond)
(p/catch raise)))
;; --- Initialization
@@ -101,10 +122,16 @@
(s/def ::signature-max-age ::dt/duration)
(defmethod ig/pre-init-spec ::handlers [_]
(s/keys :req-un [::storage ::mtx/metrics ::assets-path ::cache-max-age ::signature-max-age]))
(s/keys :req-un [::storage
::wrk/executor
::mtx/metrics
::assets-path
::cache-max-age
::signature-max-age]))
(defmethod ig/init-key ::handlers
[_ cfg]
{:objects-handler #(objects-handler cfg %)
:file-objects-handler #(file-objects-handler cfg %)
:file-thumbnails-handler #(file-thumbnails-handler cfg %)})
{:objects-handler (partial objects-handler cfg)
:file-objects-handler (partial file-objects-handler cfg)
:file-thumbnails-handler (partial file-thumbnails-handler cfg)})

View File

@@ -11,30 +11,41 @@
[app.common.logging :as l]
[app.db :as db]
[app.db.sql :as sql]
[app.util.http :as http]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[jsonista.core :as j]))
[jsonista.core :as j]
[promesa.exec :as px]
[yetti.response :as yrs]))
(declare parse-json)
(declare handle-request)
(declare parse-notification)
(declare process-report)
(s/def ::http-client fn?)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool]))
(s/keys :req-un [::db/pool ::http-client]))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [request]
(let [body (parse-json (slurp (:body request)))
[_ {:keys [executor] :as cfg}]
(fn [request respond _]
(let [data (slurp (:body request))]
(px/run! executor #(handle-request cfg data))
(respond (yrs/response 200)))))
(defn handle-request
[{:keys [http-client] :as cfg} data]
(try
(let [body (parse-json data)
mtype (get body "Type")]
(cond
(= mtype "SubscriptionConfirmation")
(let [surl (get body "SubscribeURL")
stopic (get body "TopicArn")]
(l/info :action "subscription received" :topic stopic :url surl)
(http/send! {:uri surl :method :post :timeout 10000}))
(http-client {:uri surl :method :post :timeout 10000} {:sync? true}))
(= mtype "Notification")
(when-let [message (parse-json (get body "Message"))]
@@ -43,8 +54,11 @@
:else
(l/warn :hint "unexpected data received"
:report (pr-str body)))
{:status 200 :body ""})))
:report (pr-str body))))
(catch Throwable cause
(l/error :hint "unexpected exception on awsns"
:cause cause))))
(defn- parse-bounce
[data]
@@ -173,14 +187,14 @@
(defn- process-report
[cfg {:keys [type profile-id] :as report}]
(l/trace :action "procesing report" :report (pr-str report))
(l/trace :action "processing report" :report (pr-str report))
(cond
;; In this case we receive a bounce/complaint notification without
;; confirmed identity, we just emit a warning but do nothing about
;; it because this is not a normal case. All notifications should
;; come with profile identity.
(nil? profile-id)
(l/warn :msg "a notification without identity recevied from AWS"
(l/warn :msg "a notification without identity received from AWS"
:report (pr-str report))
(= "bounce" type)

View File

@@ -0,0 +1,30 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.http.client
"Http client abstraction layer."
(:require
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[java-http-clj.core :as http]))
(defmethod ig/pre-init-spec :app.http/client [_]
(s/keys :req-un [::wrk/executor]))
(defmethod ig/init-key :app.http/client
[_ {:keys [executor] :as cfg}]
(let [client (http/build-client {:executor executor
:connect-timeout 30000 ;; 10s
:follow-redirects :always})]
(with-meta
(fn send
([req] (send req {}))
([req {:keys [response-type sync?] :or {response-type :string sync? false}}]
(if sync?
(http/send req {:client client :as response-type})
(http/send-async req {:client client :as response-type}))))
{::client client})))

View File

@@ -0,0 +1,259 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.http.debug
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.rpc.mutations.files :as m.files]
[app.rpc.queries.profile :as profile]
[app.util.blob :as blob]
[app.util.template :as tmpl]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.core :as fs]
[emoji.core :as emj]
[fipp.edn :as fpp]
[integrant.core :as ig]
[markdown.core :as md]
[markdown.transformers :as mdt]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
;; (selmer.parser/cache-off!)
(defn authorized?
[pool {:keys [profile-id]}]
(or (= "devenv" (cf/get :host))
(let [profile (ex/ignoring (profile/retrieve-profile-data pool profile-id))
admins (or (cf/get :admins) #{})]
(contains? admins (:email profile)))))
(defn index
[{:keys [pool]} request]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(yrs/response :status 200
:headers {"content-type" "text/html"}
:body (-> (io/resource "templates/debug.tmpl")
(tmpl/render {}))))
(def sql:retrieve-range-of-changes
"select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn")
(def sql:retrieve-single-change
"select revn, changes, data from file_change where file_id=? and revn = ?")
(defn prepare-response
[{:keys [params] :as request} body filename]
(when-not body
(ex/raise :type :not-found
:code :enpty-data
:hint "empty response"))
(cond-> (yrs/response :status 200
:body body
:headers {"content-type" "application/transit+json"})
(contains? params :download)
(update :headers assoc "content-disposition" (str "attachment; filename=" filename))))
(defn- retrieve-file-data
[{:keys [pool]} {:keys [params] :as request}]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(let [file-id (some-> (get-in request [:params :file-id]) uuid/uuid)
revn (some-> (get-in request [:params :revn]) d/parse-integer)
filename (str file-id)]
(when-not file-id
(ex/raise :type :validation
:code :missing-arguments))
(let [data (if (integer? revn)
(some-> (db/exec-one! pool [sql:retrieve-single-change file-id revn]) :data)
(some-> (db/get-by-id pool :file file-id) :data))]
(if (contains? params :download)
(-> (prepare-response request data filename)
(update :headers assoc "content-type" "application/octet-stream"))
(prepare-response request (some-> data blob/decode) filename)))))
(defn- upload-file-data
[{:keys [pool]} {:keys [profile-id params] :as request}]
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)
data (some-> params :file :path fs/slurp-bytes blob/decode)]
(if (and data project-id)
(let [fname (str "imported-file-" (dt/now))
file-id (try
(uuid/uuid (-> params :file :filename))
(catch Exception _ (uuid/next)))
file (db/exec-one! pool (sql/select :file {:id file-id}))]
(if file
(db/update! pool :file
{:data (blob/encode data)}
{:id file-id})
(m.files/create-file pool {:id file-id
:name fname
:project-id project-id
:profile-id profile-id
:data data}))
(yrs/response 200 "OK"))
(yrs/response 500 "ERROR"))))
(defn file-data
[cfg request]
(case (yrq/method request)
:get (retrieve-file-data cfg request)
:post (upload-file-data cfg request)
(ex/raise :type :http
:code :method-not-found)))
(defn retrieve-file-changes
[{:keys [pool]} request]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(let [file-id (some-> (get-in request [:params :id]) uuid/uuid)
revn (or (get-in request [:params :revn]) "latest")
filename (str file-id)]
(when (or (not file-id) (not revn))
(ex/raise :type :validation
:code :invalid-arguments
:hint "missing arguments"))
(cond
(d/num-string? revn)
(let [item (db/exec-one! pool [sql:retrieve-single-change file-id (d/parse-integer revn)])]
(prepare-response request (some-> item :changes blob/decode vec) filename))
(str/includes? revn ":")
(let [[start end] (->> (str/split revn #":")
(map str/trim)
(map d/parse-integer))
items (db/exec! pool [sql:retrieve-range-of-changes file-id start end])]
(prepare-response request
(some->> items
(map :changes)
(map blob/decode)
(mapcat identity)
(vec))
filename))
:else
(ex/raise :type :validation :code :invalid-arguments))))
(defn retrieve-error
[{:keys [pool]} request]
(letfn [(parse-id [request]
(let [id (get-in request [:path-params :id])
id (us/uuid-conformer id)]
(when (uuid? id)
id)))
(retrieve-report [id]
(ex/ignoring
(some-> (db/get-by-id pool :server-error-report id) :content db/decode-transit-pgobject)))
(render-template [report]
(let [context (dissoc report
:trace :cause :params :data :spec-problems
:spec-explain :spec-value :error :explain :hint)
params {:context (with-out-str
(fpp/pprint context {:width 200}))
:hint (:hint report)
:spec-explain (:spec-explain report)
:spec-problems (:spec-problems report)
:spec-value (:spec-value report)
:data (:data report)
:trace (or (:trace report)
(some-> report :error :trace))
:params (:params report)}]
(-> (io/resource "templates/error-report.tmpl")
(tmpl/render params))))]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(let [result (some-> (parse-id request)
(retrieve-report)
(render-template))]
(if result
(yrs/response :status 200
:body result
:headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"})
(yrs/response 404 "not found")))))
(def sql:error-reports
"select id, created_at from server_error_report order by created_at desc limit 100")
(defn retrieve-error-list
[{:keys [pool]} request]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(let [items (db/exec! pool [sql:error-reports])
items (map #(update % :created-at dt/format-instant :rfc1123) items)]
(yrs/response :status 200
:body (-> (io/resource "templates/error-list.tmpl")
(tmpl/render {:items items}))
:headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"})))
(defn health-check
"Mainly a task that performs a health check."
[{:keys [pool]} _]
(db/with-atomic [conn pool]
(db/exec-one! conn ["select count(*) as count from server_prop;"])
(yrs/response 200 "OK")))
(defn changelog
[_ _]
(letfn [(transform-emoji [text state]
[(emj/emojify text) state])
(md->html [text]
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
(if-let [clog (io/resource "changelog.md")]
(yrs/response :status 200
:headers {"content-type" "text/html; charset=utf-8"}
:body (-> clog slurp md->html))
(yrs/response :status 404 :body "NOT FOUND"))))
(defn- wrap-async
[{:keys [executor] :as cfg} f]
(fn [request respond raise]
(-> (px/submit! executor #(f cfg request))
(p/then respond)
(p/catch raise))))
(defmethod ig/pre-init-spec ::handlers [_]
(s/keys :req-un [::db/pool ::wrk/executor]))
(defmethod ig/init-key ::handlers
[_ cfg]
{:index (wrap-async cfg index)
:health-check (wrap-async cfg health-check)
:retrieve-file-changes (wrap-async cfg retrieve-file-changes)
:retrieve-error (wrap-async cfg retrieve-error)
:retrieve-error-list (wrap-async cfg retrieve-error-list)
:file-data (wrap-async cfg file-data)
:changelog (wrap-async cfg changelog)})

View File

@@ -13,7 +13,8 @@
[app.util.template :as tmpl]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[pretty-spec.core :as ps]))
[pretty-spec.core :as ps]
[yetti.response :as yrs]))
(defn get-spec-str
[k]
@@ -46,8 +47,8 @@
[rpc]
(let [context (prepare-context rpc)]
(if (contains? cf/flags :backend-api-doc)
(fn [_]
{:status 200
:body (-> (io/resource "api-doc.tmpl")
(tmpl/render context))})
(constantly {:status 404 :body ""}))))
(fn [_ respond _]
(respond (yrs/response 200 (-> (io/resource "api-doc.tmpl")
(tmpl/render context)))))
(fn [_ respond _]
(respond (yrs/response 404))))))

View File

@@ -7,57 +7,34 @@
(ns app.http.errors
"A errors handling for the http server."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.uuid :as uuid]
[clojure.pprint]
[cuerdas.core :as str]))
[app.common.spec :as us]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[yetti.request :as yrq]
[yetti.response :as yrs]))
(def ^:dynamic *context* {})
(defn- parse-client-ip
[{:keys [headers] :as request}]
(or (some-> (get headers "x-forwarded-for") (str/split ",") first)
(get headers "x-real-ip")
(get request :remote-addr)))
[request]
(or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first)
(yrq/get-header request "x-real-ip")
(yrq/remote-addr request)))
(defn- simple-prune
([s] (simple-prune s (* 1024 1024)))
([s max-length]
(if (> (count s) max-length)
(str (subs s 0 max-length) " [...]")
s)))
(defn- stringify-data
[data]
(binding [clojure.pprint/*print-right-margin* 200]
(let [result (with-out-str (clojure.pprint/pprint data))]
(simple-prune result (* 1024 1024)))))
(defn get-error-context
[request error]
(let [data (ex-data error)]
(d/without-nils
(merge
{:id (str (uuid/next))
:path (str (:uri request))
:method (name (:request-method request))
:hint (or (:hint data) (ex-message error))
:params (stringify-data (:params request))
:data (stringify-data (dissoc data :explain))
:ip-addr (parse-client-ip request)
:explain (str/prune (:explain data) (* 1024 1024) "[...]")}
(when-let [id (:profile-id request)]
{:profile-id id})
(let [headers (:headers request)]
{:user-agent (get headers "user-agent")
:frontend-version (get headers "x-frontend-version" "unknown")})
(when (map? data)
{:error-type (:type data)
:error-code (:code data)})))))
(defn get-context
[request]
(merge
*context*
{:path (:path request)
:method (:method request)
:params (:params request)
:ip-addr (parse-client-ip request)
:profile-id (:profile-id request)}
(let [headers (:headers request)]
{:user-agent (get headers "user-agent")
:frontend-version (get headers "x-frontend-version" "unknown")})))
(defmulti handle-exception
(fn [err & _rest]
@@ -67,102 +44,117 @@
(defmethod handle-exception :authentication
[err _]
{:status 401 :body (ex-data err)})
(yrs/response 401 (ex-data err)))
(defmethod handle-exception :restriction
[err _]
{:status 400 :body (ex-data err)})
(yrs/response 400 (ex-data err)))
(defmethod handle-exception :validation
[err req]
(let [header (get-in req [:headers "accept"])
edata (ex-data err)]
(if (and (= :spec-validation (:code edata))
(str/starts-with? header "text/html"))
{:status 400
:headers {"content-type" "text/html"}
:body (str "<pre style='font-size:16px'>"
(:explain edata)
"</pre>\n")}
{:status 400
:body (dissoc edata :data)})))
[err _]
(let [{:keys [code] :as data} (ex-data err)]
(cond
(= code :spec-validation)
(let [explain (us/pretty-explain data)]
(yrs/response :status 400
:body (-> data
(dissoc ::s/problems ::s/value)
(cond-> explain (assoc :explain explain)))))
(= code :request-body-too-large)
(yrs/response :status 413 :body data)
:else
(yrs/response :status 400 :body data))))
(defmethod handle-exception :assertion
[error request]
(let [edata (ex-data error)
cdata (get-error-context request error)]
(l/update-thread-context! cdata)
(l/error :hint "internal error: assertion"
:error-id (str (:id cdata))
explain (us/pretty-explain edata)]
(l/error ::l/raw (ex-message error)
::l/context (get-context request)
:cause error)
{:status 500
:body {:type :server-error
:code :assertion
:data (dissoc edata :data)}}))
(yrs/response :status 500
:body {:type :server-error
:code :assertion
:data (-> edata
(dissoc ::s/problems ::s/value ::s/spec)
(cond-> explain (assoc :explain explain)))})))
(defmethod handle-exception :not-found
[err _]
{:status 404 :body (ex-data err)})
(yrs/response 404 (ex-data err)))
(defmethod handle-exception org.postgresql.util.PSQLException
[error request]
(let [state (.getSQLState ^java.sql.SQLException error)]
(l/error ::l/raw (ex-message error)
::l/context (get-context request)
:cause error)
(cond
(= state "57014")
(yrs/response 504 {:type :server-error
:code :statement-timeout
:hint (ex-message error)})
(= state "25P03")
(yrs/response 504 {:type :server-error
:code :idle-in-transaction-timeout
:hint (ex-message error)})
:else
(yrs/response 500 {:type :server-error
:code :unexpected
:hint (ex-message error)
:state state}))))
(defmethod handle-exception :default
[error request]
(let [edata (ex-data error)]
;; NOTE: this is a special case for the idle-in-transaction error;
;; when it happens, the connection is automatically closed and
;; next-jdbc combines the two errors in a single ex-info. We only
;; need the :handling error, because the :rollback error will be
;; always "connection closed".
(if (and (ex/exception? (:rollback edata))
(ex/exception? (:handling edata)))
(handle-exception (:handling edata) request)
(let [cdata (get-error-context request error)]
(l/update-thread-context! cdata)
(l/error :hint "internal error"
:error-message (ex-message error)
:error-id (str (:id cdata))
:cause error)
{:status 500
:body {:type :server-error
:code :unexpected
:hint (ex-message error)
:data edata}}))))
(defmethod handle-exception org.postgresql.util.PSQLException
[error request]
(let [cdata (get-error-context request error)
state (.getSQLState ^java.sql.SQLException error)]
(l/update-thread-context! cdata)
(l/error :hint "psql exception"
:error-message (ex-message error)
:error-id (str (:id cdata))
:sql-state state
:cause error)
(cond
(= state "57014")
{:status 504
:body {:type :server-timeout
:code :statement-timeout
:hint (ex-message error)}}
;; This means that exception is not a controlled exception.
(nil? edata)
(do
(l/error ::l/raw (ex-message error)
::l/context (get-context request)
:cause error)
(yrs/response 500 {:type :server-error
:code :unexpected
:hint (ex-message error)}))
(= state "25P03")
{:status 504
:body {:type :server-timeout
:code :idle-in-transaction-timeout
:hint (ex-message error)}}
;; This is a special case for the idle-in-transaction error;
;; when it happens, the connection is automatically closed and
;; next-jdbc combines the two errors in a single ex-info. We
;; only need the :handling error, because the :rollback error
;; will be always "connection closed".
(and (ex/exception? (:rollback edata))
(ex/exception? (:handling edata)))
(handle-exception (:handling edata) request)
:else
{:status 500
:body {:type :server-error
:code :psql-exception
:hint (ex-message error)
:state state}})))
(do
(l/error ::l/raw (ex-message error)
::l/context (get-context request)
:cause error)
(yrs/response 500 {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata})))))
(defn handle
[error req]
(if (or (instance? java.util.concurrent.CompletionException error)
(instance? java.util.concurrent.ExecutionException error))
(handle-exception (.getCause ^Throwable error) req)
(handle-exception error req)))
[cause request]
(cond
(or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause))
(handle-exception (.getCause ^Throwable cause) request)
(ex/wrapped? cause)
(let [context (meta cause)
cause (deref cause)]
(binding [*context* context]
(handle-exception cause request)))
:else
(handle-exception cause request)))

View File

@@ -14,48 +14,57 @@
[app.db :as db]
[app.emails :as eml]
[app.rpc.queries.profile :as profile]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
(declare send-feedback)
(declare ^:private send-feedback)
(declare ^:private handler)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool]))
(s/keys :req-un [::db/pool ::wrk/executor]))
(defmethod ig/init-key ::handler
[_ {:keys [pool] :as scfg}]
(let [ftoken (cf/get :feedback-token ::no-token)
enabled (contains? cf/flags :user-feedback)]
(fn [{:keys [profile-id] :as request}]
(let [token (get-in request [:headers "x-feedback-token"])
params (d/merge (:params request)
(:body-params request))]
[_ {:keys [executor] :as cfg}]
(let [enabled? (contains? cf/flags :user-feedback)]
(if enabled?
(fn [request respond raise]
(-> (px/submit! executor #(handler cfg request))
(p/then' respond)
(p/catch raise)))
(fn [_ _ raise]
(raise (ex/error :type :validation
:code :feedback-disabled
:hint "feedback module is disabled"))))))
(when-not enabled
(ex/raise :type :validation
:code :feedback-disabled
:hint "feedback module is disabled"))
(defn- handler
[{:keys [pool] :as cfg} {:keys [profile-id] :as request}]
(let [ftoken (cf/get :feedback-token ::no-token)
token (yrq/get-header request "x-feedback-token")
params (d/merge (:params request)
(:body-params request))]
(cond
(uuid? profile-id)
(let [profile (profile/retrieve-profile-data pool profile-id)
params (assoc params :from (:email profile))]
(send-feedback pool profile params))
(cond
(uuid? profile-id)
(let [profile (profile/retrieve-profile-data pool profile-id)
params (assoc params :from (:email profile))]
(when-not (:is-muted profile)
(send-feedback pool profile params)))
(= token ftoken)
(send-feedback cfg nil params))
(= token ftoken)
(send-feedback scfg nil params))
{:status 204 :body ""}))))
(yrs/response 204)))
(s/def ::content ::us/string)
(s/def ::from ::us/email)
(s/def ::subject ::us/string)
(s/def ::feedback
(s/keys :req-un [::from ::subject ::content]))
(defn send-feedback
(defn- send-feedback
[pool profile params]
(let [params (us/conform ::feedback params)
destination (cf/get :feedback-destination)]

View File

@@ -6,200 +6,189 @@
(ns app.http.middleware
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.transit :as t]
[app.config :as cf]
[app.metrics :as mtx]
[app.util.json :as json]
[buddy.core.codecs :as bc]
[buddy.core.hash :as bh]
[clojure.java.io :as io]
[ring.middleware.cookies :refer [wrap-cookies]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
[ring.middleware.params :refer [wrap-params]]))
[cuerdas.core :as str]
[yetti.adapter :as yt]
[yetti.middleware :as ymw]
[yetti.request :as yrq]
[yetti.response :as yrs])
(:import
com.fasterxml.jackson.core.io.JsonEOFException
io.undertow.server.RequestTooBigException
java.io.OutputStream))
(defn wrap-server-timing
(def server-timing
{:name ::server-timing
:compile (constantly ymw/wrap-server-timing)})
(def params
{:name ::params
:compile (constantly ymw/wrap-params)})
(defn wrap-parse-request
[handler]
(let [seconds-from #(float (/ (- (System/nanoTime) %) 1000000000))]
(fn [request]
(let [start (System/nanoTime)
response (handler request)]
(update response :headers
(fn [headers]
(assoc headers "Server-Timing" (str "total;dur=" (seconds-from start)))))))))
(letfn [(process-request [request]
(let [header (yrq/get-header request "content-type")]
(cond
(str/starts-with? header "application/transit+json")
(with-open [is (-> request yrq/body yrq/body-stream)]
(let [params (t/read! (t/reader is))]
(-> request
(assoc :body-params params)
(update :params merge params))))
(defn wrap-parse-request-body
(str/starts-with? header "application/json")
(with-open [is (-> request yrq/body yrq/body-stream)]
(let [params (json/read is)]
(-> request
(assoc :body-params params)
(update :params merge params))))
:else
request)))
(handle-error [raise cause]
(cond
(instance? RequestTooBigException cause)
(raise (ex/error :type :validation
:code :request-body-too-large
:hint (ex-message cause)))
(instance? JsonEOFException cause)
(raise (ex/error :type :validation
:code :malformed-json
:hint (ex-message cause)))
:else
(raise cause)))]
(fn [request respond raise]
(when-let [request (try
(process-request request)
(catch RuntimeException cause
(handle-error raise (or (.getCause cause) cause)))
(catch Throwable cause
(handle-error raise cause)))]
(handler request respond raise)))))
(def parse-request
{:name ::parse-request
:compile (constantly wrap-parse-request)})
(defn buffered-output-stream
"Returns a buffered output stream that ignores flush calls. This is
needed because transit-java calls flush very aggresivelly on each
object write."
[^java.io.OutputStream os ^long chunk-size]
(proxy [java.io.BufferedOutputStream] [os (int chunk-size)]
;; Explicitly do not forward flush
(flush [])
(close []
(proxy-super flush)
(proxy-super close))))
(def ^:const buffer-size (:xnio/buffer-size yt/defaults))
(defn wrap-format-response
[handler]
(letfn [(parse-transit [body]
(let [reader (t/reader body)]
(t/read! reader)))
(letfn [(transit-streamable-body [data opts]
(reify yrs/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream]
(try
(with-open [bos (buffered-output-stream output-stream buffer-size)]
(let [tw (t/writer bos opts)]
(t/write! tw data)))
(parse-json [body]
(let [reader (io/reader body)]
(json/read reader)))
(catch java.io.IOException _cause
;; Do nothing, EOF means client closes connection abruptly
nil)
(catch Throwable cause
(l/warn :hint "unexpected error on encoding response"
:cause cause))
(finally
(.close ^OutputStream output-stream))))))
(parse [type body]
(try
(case type
:json (parse-json body)
:transit (parse-transit body))
(catch Exception e
(let [data {:type :parse
:hint "unable to parse request body"
:message (ex-message e)}]
{:status 400
:headers {"content-type" "application/transit+json"}
:body (t/encode-str data {:type :json-verbose})}))))]
(format-response [response request]
(let [body (yrs/body response)]
(if (coll? body)
(let [qs (yrq/query request)
opts (if (or (contains? cf/flags :transit-readable-response)
(str/includes? qs "transit_verbose"))
{:type :json-verbose}
{:type :json})]
(-> response
(update :headers assoc "content-type" "application/transit+json")
(assoc :body (transit-streamable-body body opts))))
response)))
(fn [{:keys [headers body] :as request}]
(let [ctype (get headers "content-type")]
(handler
(case ctype
"application/transit+json"
(let [params (parse :transit body)]
(-> request
(assoc :body-params params)
(update :params merge params)))
(process-response [response request]
(cond-> response
(map? response) (format-response request)))]
"application/json"
(let [params (parse :json body)]
(-> request
(assoc :body-params params)
(update :params merge params)))
(fn [request respond raise]
(handler request
(fn [response]
(let [response (process-response response request)]
(respond response)))
raise))))
request))))))
(def parse-request-body
{:name ::parse-request-body
:compile (constantly wrap-parse-request-body)})
(defn- impl-format-response-body
[response _request]
(let [body (:body response)
opts {:type :json}]
(cond
(coll? body)
(-> response
(update :headers assoc "content-type" "application/transit+json")
(assoc :body (t/encode body opts)))
(nil? body)
(assoc response :status 204 :body "")
:else
response)))
(defn- wrap-format-response-body
[handler]
(fn [request]
(let [response (handler request)]
(cond-> response
(map? response) (impl-format-response-body request)))))
(def format-response-body
{:name ::format-response-body
:compile (constantly wrap-format-response-body)})
(def format-response
{:name ::format-response
:compile (constantly wrap-format-response)})
(defn wrap-errors
[handler on-error]
(fn [request]
(try
(handler request)
(catch Throwable e
(on-error e request)))))
(fn [request respond _]
(handler request respond (fn [cause]
(-> cause (on-error request) respond)))))
(def errors
{:name ::errors
:compile (constantly wrap-errors)})
(def metrics
{:name ::metrics
:wrap (fn [handler]
(mtx/wrap-counter handler {:id "http__requests_counter"
:help "Absolute http requests counter."}))})
(def cookies
{:name ::cookies
:compile (constantly wrap-cookies)})
(def params
{:name ::params
:compile (constantly wrap-params)})
(def multipart-params
{:name ::multipart-params
:compile (constantly wrap-multipart-params)})
(def keyword-params
{:name ::keyword-params
:compile (constantly wrap-keyword-params)})
(def server-timing
{:name ::server-timing
:compile (constantly wrap-server-timing)})
(defn wrap-etag
[handler]
(letfn [(generate-etag [{:keys [body] :as response}]
(str "W/\"" (-> body bh/blake2b-128 bc/bytes->hex) "\""))
(get-match [{:keys [headers] :as request}]
(get headers "if-none-match"))]
(fn [request]
(let [response (handler request)]
(if (= :get (:request-method request))
(let [etag (generate-etag response)
match (get-match request)
response (update response :headers #(assoc % "ETag" etag))]
(cond-> response
(and (string? match)
(= :get (:request-method request))
(= etag match))
(-> response
(assoc :body "")
(assoc :status 304))))
response)))))
(def etag
{:name ::etag
:compile (constantly wrap-etag)})
(defn activity-logger
[handler]
(let [logger "penpot.profile-activity"]
(fn [{:keys [headers] :as request}]
(let [ip-addr (get headers "x-forwarded-for")
profile-id (:profile-id request)
qstring (:query-string request)]
(l/info ::l/async true
::l/logger logger
:ip-addr ip-addr
:profile-id profile-id
:uri (str (:uri request) (when qstring (str "?" qstring)))
:method (name (:request-method request)))
(handler request)))))
(defn- wrap-cors
(defn wrap-cors
[handler]
(if-not (contains? cf/flags :cors)
handler
(letfn [(add-cors-headers [response request]
(-> response
(update
:headers
(fn [headers]
(-> headers
(assoc "access-control-allow-origin" (get-in request [:headers "origin"]))
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))))))]
(fn [request]
(if (= (:request-method request) :options)
(-> {:status 200 :body ""}
(add-cors-headers request))
(let [response (handler request)]
(add-cors-headers response request)))))))
(letfn [(add-headers [headers request]
(let [origin (yrq/get-header request "origin")]
(-> headers
(assoc "access-control-allow-origin" origin)
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))))
(update-response [response request]
(update response :headers add-headers request))]
(fn [request respond raise]
(if (= (yrq/method request) :options)
(-> (yrs/response 200)
(update-response request)
(respond))
(handler request
(fn [response]
(respond (update-response response request)))
raise))))))
(def cors
{:name ::cors
:compile (constantly wrap-cors)})
(defn compile-restrict-methods
[data _]
(when-let [allowed (:allowed-methods data)]
(fn [handler]
(fn [request respond raise]
(let [method (yrq/method request)]
(if (contains? allowed method)
(handler request respond raise)
(respond (yrs/response 405))))))))
(def restrict-methods
{:name ::restrict-methods
:compile compile-restrict-methods})

View File

@@ -15,13 +15,15 @@
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.queries.profile :as profile]
[app.util.http :as http]
[app.util.json :as json]
[app.util.time :as dt]
[clojure.data.json :as json]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]))
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.response :as yrs]))
(defn- build-redirect-uri
[{:keys [provider] :as cfg}]
@@ -40,27 +42,6 @@
(assoc :query query)
(str))))
(defn retrieve-access-token
[{:keys [provider] :as cfg} code]
(try
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
:code code
:grant_type "authorization_code"
:redirect_uri (build-redirect-uri cfg)}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"}
:uri (:token-uri provider)
:body (u/map->query-string params)}
res (http/send! req)]
(when (= 200 (:status res))
(let [data (json/read-str (:body res))]
{:token (get data "access_token")
:type (get data "token_type")})))
(catch Exception e
(l/warn :hint "unexpected error on retrieve-access-token" :cause e)
nil)))
(defn- qualify-props
[provider props]
(reduce-kv (fn [result k v]
@@ -68,31 +49,99 @@
{}
props))
(defn- retrieve-user-info
[{:keys [provider] :as cfg} tdata]
(try
(let [req {:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}
res (http/send! req)]
(defn retrieve-access-token
[{:keys [provider http-client] :as cfg} code]
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
:code code
:grant_type "authorization_code"
:redirect_uri (build-redirect-uri cfg)}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"
"accept" "application/json"}
:uri (:token-uri provider)
:body (u/map->query-string params)}]
(p/then
(http-client req)
(fn [{:keys [status body] :as res}]
(if (= status 200)
(let [data (json/read body)]
{:token (get data :access_token)
:type (get data :token_type)})
(ex/raise :type :internal
:code :unable-to-retrieve-token
:http-status status
:http-body body))))))
(when (= 200 (:status res))
(let [info (json/read-str (:body res) :key-fn keyword)]
{:backend (:name provider)
:email (:email info)
:fullname (:name info)
:props (->> (dissoc info :name :email)
(qualify-props provider))})))
(catch Exception e
(l/warn :hint "unexpected exception on retrieve-user-info" :cause e)
nil)))
(defn- retrieve-user-info
[{:keys [provider http-client] :as cfg} tdata]
(letfn [(retrieve []
(http-client {:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}))
(retrieve-emails []
(if (some? (:emails-uri provider))
(http-client {:uri (:emails-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get})
(p/resolved {:status 200})))
(validate-response [[retrieve-res emails-res]]
(when-not (s/int-in-range? 200 300 (:status retrieve-res))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status retrieve-res)
:http-body (:body retrieve-res)))
(when-not (s/int-in-range? 200 300 (:status emails-res))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status emails-res)
:http-body (:body emails-res)))
[retrieve-res emails-res])
(get-email [info]
(let [attr-kw (cf/get :oidc-email-attr :email)]
(get info attr-kw)))
(get-name [info]
(let [attr-kw (cf/get :oidc-name-attr :name)]
(get info attr-kw)))
(process-response [[retrieve-res emails-res]]
(let [info (json/read (:body retrieve-res))
email (if (some? (:extract-email-callback provider))
((:extract-email-callback provider) emails-res)
(get-email info))]
{:backend (:name provider)
:email email
:fullname (or (get-name info) email)
:props (->> (dissoc info :name :email)
(qualify-props provider))}))
(validate-info [info]
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
:info (pr-str info))
(ex/raise :type :internal
:code :incomplete-user-info
:hint "inconmplete user info"
:info info))
info)]
(-> (p/all [(retrieve) (retrieve-emails)])
(p/then' validate-response)
(p/then' process-response)
(p/then' validate-info))))
(s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string)
(s/def ::fullname ::us/not-empty-string)
(s/def ::props (s/map-of ::us/keyword any?))
(s/def ::info
(s/keys :req-un [::backend
::email
@@ -100,73 +149,66 @@
::props]))
(defn retrieve-info
[{:keys [tokens provider] :as cfg} request]
(let [state (get-in request [:params :state])
state (tokens :verify {:token state :iss :oauth})
info (some->> (get-in request [:params :code])
(retrieve-access-token cfg)
(retrieve-user-info cfg))]
[{:keys [tokens provider] :as cfg} {:keys [params] :as request}]
(letfn [(validate-oidc [info]
;; If the provider is OIDC, we can proceed to check
;; roles if they are defined.
(when (and (= "oidc" (:name provider))
(seq (:roles provider)))
(let [provider-roles (into #{} (:roles provider))
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
roles (get info attr)]
(cond
(string? roles) (into #{} (str/words roles))
(vector? roles) (into #{} roles)
:else #{}))]
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
:info (pr-str info))
;; check if profile has a configured set of roles
(when-not (set/subset? provider-roles profile-roles)
(ex/raise :type :internal
:code :unable-to-auth
:hint "not enough permissions"))))
info)
(post-process [state info]
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state))
;; If state token comes with props, merge them. The state token
;; props can contain pm_ and utm_ prefixed query params.
(map? (:props state))
(update :props merge (:props state))))]
(when-let [error (get params :error)]
(ex/raise :type :internal
:code :unable-to-auth
:hint "no user info"))
:code :error-on-retrieving-code
:error-id error
:error-desc (get params :error_description)))
;; If the provider is OIDC, we can proceed to check
;; roles if they are defined.
(when (and (= "oidc" (:name provider))
(seq (:roles provider)))
(let [provider-roles (into #{} (:roles provider))
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
roles (get info attr)]
(cond
(string? roles) (into #{} (str/words roles))
(vector? roles) (into #{} roles)
:else #{}))]
;; check if profile has a configured set of roles
(when-not (set/subset? provider-roles profile-roles)
(ex/raise :type :internal
:code :unable-to-auth
:hint "not enought permissions"))))
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state))
;; If state token comes with props, merge them. The state token
;; props can contain pm_ and utm_ prefixed query params.
(map? (:props state))
(update :props merge (:props state)))))
(let [state (get params :state)
code (get params :code)
state (tokens :verify {:token state :iss :oauth})]
(-> (p/resolved code)
(p/then #(retrieve-access-token cfg %))
(p/then #(retrieve-user-info cfg %))
(p/then' validate-oidc)
(p/then' (partial post-process state))))))
;; --- HTTP HANDLERS
(defn extract-utm-props
"Extracts additional data from user params."
[params]
(reduce-kv (fn [params k v]
(let [sk (name k)]
(cond-> params
(str/starts-with? sk "utm_")
(assoc (->> sk str/kebab (keyword "penpot")) v))))
{}
params))
(defn- retrieve-profile
[{:keys [pool] :as cfg} info]
(with-open [conn (db/open pool)]
(some->> (:email info)
(profile/retrieve-profile-data-by-email conn)
(profile/populate-additional-data conn)
(profile/decode-profile-row))))
[{:keys [pool executor] :as cfg} info]
(px/with-dispatch executor
(with-open [conn (db/open pool)]
(some->> (:email info)
(profile/retrieve-profile-data-by-email conn)
(profile/populate-additional-data conn)
(profile/decode-profile-row)))))
(defn- redirect-response
[uri]
{:status 302
:headers {"location" (str uri)}
:body ""})
(yrs/response :status 302 :headers {"location" (str uri)}))
(defn- generate-error-redirect
[cfg error]
@@ -199,6 +241,7 @@
(->> (redirect-response uri)
(sxf request)))
(let [info (assoc info
:iss :prepared-register
:is-active true
@@ -213,28 +256,33 @@
(redirect-response uri))))
(defn- auth-handler
[{:keys [tokens] :as cfg} {:keys [params] :as request}]
(let [invitation (:invitation-token params)
props (extract-utm-props params)
state (tokens :generate
{:iss :oauth
:invitation-token invitation
:props props
:exp (dt/in-future "15m")})
uri (build-auth-uri cfg state)]
{:status 200
:body {:redirect-uri uri}}))
[{:keys [tokens] :as cfg} {:keys [params] :as request} respond raise]
(try
(let [props (audit/extract-utm-params params)
state (tokens :generate
{:iss :oauth
:invitation-token (:invitation-token params)
:props props
:exp (dt/in-future "15m")})
uri (build-auth-uri cfg state)]
(respond (yrs/response 200 {:redirect-uri uri})))
(catch Throwable cause
(raise cause))))
(defn- callback-handler
[cfg request]
(try
(let [info (retrieve-info cfg request)
profile (retrieve-profile cfg info)]
(generate-redirect cfg request info profile))
(catch Exception e
(l/warn :hint "error on oauth process"
:cause e)
(generate-error-redirect cfg e))))
[cfg request respond _]
(letfn [(process-request []
(p/let [info (retrieve-info cfg request)
profile (retrieve-profile cfg info)]
(generate-redirect cfg request info profile)))
(handle-error [cause]
(l/error :hint "error on oauth process" :cause cause)
(respond (generate-error-redirect cfg cause)))]
(-> (process-request)
(p/then respond)
(p/catch handle-error))))
;; --- INIT
@@ -250,15 +298,19 @@
(defn wrap-handler
[cfg handler]
(fn [request]
(fn [request respond raise]
(let [provider (get-in request [:path-params :provider])
provider (get-in @cfg [:providers provider])]
(when-not provider
(ex/raise :type :not-found
:context {:provider provider}
:hint "provider not configured"))
(-> (assoc @cfg :provider provider)
(handler request)))))
(if provider
(handler (assoc @cfg :provider provider)
request
respond
raise)
(raise
(ex/error
:type :not-found
:provider provider
:hint "provider not configured"))))))
(defmethod ig/init-key ::handler
[_ cfg]
@@ -267,15 +319,30 @@
:callback-handler (wrap-handler cfg callback-handler)}))
(defn- discover-oidc-config
[{:keys [base-uri] :as opts}]
[{:keys [http-client]} {:keys [base-uri] :as opts}]
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
response (http/send! {:method :get :uri (str discovery-uri)})]
(when (= 200 (:status response))
(let [data (json/read-str (:body response))]
(assoc opts
:token-uri (get data "token_endpoint")
:auth-uri (get data "authorization_endpoint")
:user-uri (get data "userinfo_endpoint"))))))
response (ex/try (http-client {:method :get :uri (str discovery-uri)} {:sync? true}))]
(cond
(ex/exception? response)
(do
(l/warn :hint "unable to discover oidc configuration"
:discover-uri (str discovery-uri)
:cause response)
nil)
(= 200 (:status response))
(let [data (json/read (:body response))]
{:token-uri (get data :token_endpoint)
:auth-uri (get data :authorization_endpoint)
:user-uri (get data :userinfo_endpoint)})
:else
(do
(l/warn :hint "unable to discover OIDC configuration"
:uri (str discovery-uri)
:response-status-code (:status response))
nil))))
(defn- obfuscate-string
[s]
@@ -296,20 +363,27 @@
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)
:name "oidc"}]
(if (and (string? (:base-uri opts))
(string? (:client-id opts))
(string? (:client-secret opts)))
(if (and (string? (:token-uri opts))
(string? (:user-uri opts))
(string? (:auth-uri opts)))
(do
(l/info :action "initialize" :provider "oidc" :method "static"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "oidc"] opts))
(let [opts (discover-oidc-config opts)]
(l/info :action "initialize" :provider "oidc" :method "discover"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "oidc"] opts)))
(do
(l/debug :hint "initialize oidc provider" :name "generic-oidc"
:opts (update opts :client-secret obfuscate-string))
(if (and (string? (:token-uri opts))
(string? (:user-uri opts))
(string? (:auth-uri opts)))
(do
(l/debug :hint "initialized with user provided configuration")
(assoc-in cfg [:providers "oidc"] opts))
(do
(l/debug :hint "trying to discover oidc provider configuration using BASE_URI")
(if-let [opts' (discover-oidc-config cfg opts)]
(do
(l/debug :hint "discovered opts" :additional-opts opts')
(assoc-in cfg [:providers "oidc"] (merge opts opts')))
cfg))))
cfg)))
(defn- initialize-google-provider
@@ -329,15 +403,25 @@
(assoc-in cfg [:providers "google"] opts))
cfg)))
(defn extract-github-email
[response]
(let [emails (json/read (:body response))
primary-email (->> emails
(filter #(:primary %))
first)]
(:email primary-email)))
(defn- initialize-github-provider
[cfg]
(let [opts {:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret)
:scopes #{"read:user" "user:email"}
:auth-uri "https://github.com/login/oauth/authorize"
:token-uri "https://github.com/login/oauth/access_token"
:user-uri "https://api.github.com/user"
:name "github"}]
(let [opts {:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret)
:scopes #{"read:user" "user:email"}
:auth-uri "https://github.com/login/oauth/authorize"
:token-uri "https://github.com/login/oauth/access_token"
:emails-uri "https://api.github.com/user/emails"
:extract-email-callback extract-github-email
:user-uri "https://api.github.com/user"
:name "github"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
@@ -346,17 +430,16 @@
(assoc-in cfg [:providers "github"] opts))
cfg)))
(defn- initialize-gitlab-provider
[cfg]
(let [base (cf/get :gitlab-base-uri "https://gitlab.com")
opts {:base-uri base
:client-id (cf/get :gitlab-client-id)
:client-secret (cf/get :gitlab-client-secret)
:scopes #{"read_user"}
:scopes #{"openid" "profile" "email"}
:auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token")
:user-uri (str base "/api/v4/user")
:user-uri (str base "/oauth/userinfo")
:name "gitlab"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))

View File

@@ -11,105 +11,209 @@
[app.common.logging :as l]
[app.config :as cfg]
[app.db :as db]
[app.db.sql :as sql]
[app.metrics :as mtx]
[app.util.async :as aa]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]))
;; A default cookie name for storing the session. We don't allow
;; configure it.
(def cookie-name "auth-token")
;; A default cookie name for storing the session. We don't allow to configure it.
(def token-cookie-name "auth-token")
;; A cookie that we can use to check from other sites of the same domain if a user
;; is registered. Is not intended for on premise installations, although nothing
;; prevents using it if some one wants to.
(def authenticated-cookie-name "authenticated")
(defprotocol ISessionStore
(read-session [store key])
(write-session [store key data])
(delete-session [store key]))
(defn- make-database-store
[{:keys [pool tokens executor]}]
(reify ISessionStore
(read-session [_ token]
(px/with-dispatch executor
(db/exec-one! pool (sql/select :http-session {:id token}))))
(write-session [_ _ data]
(px/with-dispatch executor
(let [profile-id (:profile-id data)
user-agent (:user-agent data)
token (tokens :generate {:iss "authentication"
:iat (dt/now)
:uid profile-id})
now (dt/now)
params {:user-agent user-agent
:profile-id profile-id
:created-at now
:updated-at now
:id token}]
(db/insert! pool :http-session params)
token)))
(delete-session [_ token]
(px/with-dispatch executor
(db/delete! pool :http-session {:id token})
nil))))
(defn make-inmemory-store
[{:keys [tokens]}]
(let [cache (atom {})]
(reify ISessionStore
(read-session [_ token]
(p/do (get @cache token)))
(write-session [_ _ data]
(p/do
(let [profile-id (:profile-id data)
user-agent (:user-agent data)
token (tokens :generate {:iss "authentication"
:iat (dt/now)
:uid profile-id})
params {:user-agent user-agent
:profile-id profile-id
:id token}]
(swap! cache assoc token params)
token)))
(delete-session [_ token]
(p/do
(swap! cache dissoc token)
nil)))))
(s/def ::tokens fn?)
(defmethod ig/pre-init-spec ::store [_]
(s/keys :req-un [::db/pool ::wrk/executor ::tokens]))
(defmethod ig/init-key ::store
[_ {:keys [pool] :as cfg}]
(if (db/read-only? pool)
(make-inmemory-store cfg)
(make-database-store cfg)))
(defmethod ig/halt-key! ::store
[_ _])
;; --- IMPL
(defn- create-session
[{:keys [conn tokens] :as cfg} {:keys [profile-id headers] :as request}]
(let [token (tokens :generate {:iss "authentication"
:iat (dt/now)
:uid profile-id})
params {:user-agent (get headers "user-agent")
:profile-id profile-id
:id token}]
(db/insert! conn :http-session params)))
(defn- create-session!
[store request profile-id]
(let [params {:user-agent (yrq/get-header request "user-agent")
:profile-id profile-id}]
(write-session store nil params)))
(defn- delete-session
[{:keys [conn] :as cfg} {:keys [cookies] :as request}]
(when-let [token (get-in cookies [cookie-name :value])]
(db/delete! conn :http-session {:id token}))
nil)
(defn- delete-session!
[store {:keys [cookies] :as request}]
(when-let [token (get-in cookies [token-cookie-name :value])]
(delete-session store token)))
(defn- retrieve-session
[{:keys [conn] :as cfg} id]
(when id
(db/exec-one! conn ["select id, profile_id from http_session where id = ?" id])))
(defn- retrieve-from-request
[cfg {:keys [cookies] :as request}]
(->> (get-in cookies [cookie-name :value])
(retrieve-session cfg)))
[store request]
(when-let [cookie (yrq/get-cookie request token-cookie-name)]
(-> (read-session store (:value cookie))
(p/then (fn [session]
(when session
{:session-id (:id session)
:profile-id (:profile-id session)}))))))
(defn- add-cookies
[response {:keys [id] :as session}]
[response token]
(let [cors? (contains? cfg/flags :cors)
secure? (contains? cfg/flags :secure-session-cookies)]
(assoc response :cookies {cookie-name {:path "/"
:http-only true
:value id
:same-site (if cors? :none :strict)
:secure secure?}})))
secure? (contains? cfg/flags :secure-session-cookies)
authenticated-cookie-domain (cfg/get :authenticated-cookie-domain)]
(update response :cookies
(fn [cookies]
(cond-> cookies
:always
(assoc token-cookie-name {:path "/"
:http-only true
:value token
:same-site (if cors? :none :lax)
:secure secure?})
(some? authenticated-cookie-domain)
(assoc authenticated-cookie-name {:domain authenticated-cookie-domain
:path "/"
:value true
:same-site :strict
:secure secure?}))))))
(defn- clear-cookies
[response]
(assoc response :cookies {cookie-name {:value "" :max-age -1}}))
(let [authenticated-cookie-domain (cfg/get :authenticated-cookie-domain)]
(assoc response :cookies
{token-cookie-name {:path "/"
:value ""
:max-age -1}
authenticated-cookie-name {:domain authenticated-cookie-domain
:path "/"
:value ""
:max-age -1}})))
(defn- make-middleware
[{:keys [::events-ch store] :as cfg}]
{:name :session-middleware
:wrap (fn [handler]
(fn [request respond raise]
(try
(-> (retrieve-session store request)
(p/then' #(merge request %))
(p/finally (fn [request cause]
(if cause
(raise cause)
(do
(when-let [session-id (:session-id request)]
(a/offer! events-ch session-id))
(handler request respond raise))))))
(catch Throwable cause
(raise cause)))))})
(defn- middleware
[cfg handler]
(fn [request]
(if-let [{:keys [id profile-id] :as session} (retrieve-from-request cfg request)]
(do
(a/>!! (::events-ch cfg) id)
(l/update-thread-context! {:profile-id profile-id})
(handler (assoc request :profile-id profile-id)))
(handler request))))
;; --- STATE INIT: SESSION
(defmethod ig/pre-init-spec ::session [_]
(s/keys :req-un [::db/pool]))
(s/def ::store #(satisfies? ISessionStore %))
(defmethod ig/prep-key ::session
(defmethod ig/pre-init-spec :app.http/session [_]
(s/keys :req-un [::store]))
(defmethod ig/prep-key :app.http/session
[_ cfg]
(d/merge {:buffer-size 64}
(d/merge {:buffer-size 128}
(d/without-nils cfg)))
(defmethod ig/init-key ::session
[_ {:keys [pool] :as cfg}]
(let [events (a/chan (a/dropping-buffer (:buffer-size cfg)))
cfg (-> cfg
(assoc :conn pool)
(assoc ::events-ch events))]
(defmethod ig/init-key :app.http/session
[_ {:keys [store] :as cfg}]
(let [events-ch (a/chan (a/dropping-buffer (:buffer-size cfg)))
cfg (assoc cfg ::events-ch events-ch)]
(-> cfg
(assoc :middleware #(middleware cfg %))
(assoc :middleware (make-middleware cfg))
(assoc :create (fn [profile-id]
(fn [request response]
(let [request (assoc request :profile-id profile-id)
session (create-session cfg request)]
(add-cookies response session)))))
(p/let [token (create-session! store request profile-id)]
(add-cookies response token)))))
(assoc :delete (fn [request response]
(delete-session cfg request)
(-> response
(assoc :status 204)
(assoc :body "")
(clear-cookies)))))))
(p/do
(delete-session! store request)
(-> response
(assoc :status 204)
(assoc :body nil)
(clear-cookies))))))))
(defmethod ig/halt-key! ::session
(defmethod ig/halt-key! :app.http/session
[_ data]
(a/close! (::events-ch data)))
;; --- STATE INIT: SESSION UPDATER
(declare update-sessions)
@@ -120,8 +224,7 @@
(defmethod ig/pre-init-spec ::updater [_]
(s/keys :req-un [::db/pool ::wrk/executor ::mtx/metrics ::session]
:opt-un [::max-batch-age
::max-batch-size]))
:opt-un [::max-batch-age ::max-batch-size]))
(defmethod ig/prep-key ::updater
[_ cfg]
@@ -136,16 +239,11 @@
:max-batch-size (str (:max-batch-size cfg)))
(let [input (aa/batch (::events-ch session)
{:max-batch-size (:max-batch-size cfg)
:max-batch-age (inst-ms (:max-batch-age cfg))})
mcnt (mtx/create
{:name "http_session_update_total"
:help "A counter of session update batch events."
:registry (:registry metrics)
:type :counter})]
:max-batch-age (inst-ms (:max-batch-age cfg))})]
(a/go-loop []
(when-let [[reason batch] (a/<! input)]
(let [result (a/<! (update-sessions cfg batch))]
(mcnt :inc)
(mtx/run! metrics {:id :session-update-total :inc 1})
(cond
(ex/exception? result)
(l/error :task "updater"
@@ -154,9 +252,10 @@
(= :size reason)
(l/debug :task "updater"
:action "update sessions"
:hint "update sessions"
:reason (name reason)
:count result))
(recur))))))
(defn- update-sessions
@@ -178,22 +277,25 @@
(defmethod ig/prep-key ::gc-task
[_ cfg]
(merge {:max-age (dt/duration {:days 2})}
(merge {:max-age (dt/duration {:days 15})}
(d/without-nils cfg)))
(defmethod ig/init-key ::gc-task
[_ {:keys [pool max-age] :as cfg}]
(l/debug :hint "initializing session gc task" :max-age max-age)
(fn [_]
(db/with-atomic [conn pool]
(let [interval (db/interval max-age)
result (db/exec-one! conn [sql:delete-expired interval])
result (db/exec-one! conn [sql:delete-expired interval interval])
result (:next.jdbc/update-count result)]
(l/debug :task "gc"
:action "clean http sessions"
:count result)
:hint "clean http sessions"
:deleted result)
result))))
(def ^:private
sql:delete-expired
"delete from http_session
where updated_at < now() - ?::interval")
where updated_at < now() - ?::interval
or (updated_at is null and
created_at < now() - ?::interval)")

View File

@@ -0,0 +1,223 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.http.websocket
"A penpot notification service for file cooperative edition."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.db :as db]
[app.metrics :as mtx]
[app.util.websocket :as ws]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[yetti.websocket :as yws]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WEBSOCKET HANDLER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmulti handle-message
(fn [_ message]
(:type message)))
(defmethod handle-message :connect
[wsp _]
(l/trace :fn "handle-message" :event :connect)
(let [msgbus-fn (:msgbus @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
xform (remove #(= (:session-id %) session-id))
channel (a/chan (a/dropping-buffer 16) xform)]
(swap! wsp assoc ::profile-subs-channel channel)
(a/pipe channel output-ch false)
(msgbus-fn :cmd :sub :topic profile-id :chan channel)))
(defmethod handle-message :disconnect
[wsp _]
(l/trace :fn "handle-message" :event :disconnect)
(a/go
(let [msgbus-fn (:msgbus @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
profile-ch (::profile-subs-channel @wsp)
subs (::subscriptions @wsp)]
;; Close the main profile subscription
(a/close! profile-ch)
(a/<! (msgbus-fn :cmd :purge :chans [profile-ch]))
;; Close all other active subscrption on this websocket context.
(doseq [{:keys [channel topic]} (map second subs)]
(a/close! channel)
(a/<! (msgbus-fn :cmd :pub :topic topic
:message {:type :disconnect
:profile-id profile-id
:session-id session-id}))
(a/<! (msgbus-fn :cmd :purge :chans [channel]))))))
(defmethod handle-message :subscribe-team
[wsp {:keys [team-id] :as params}]
(l/trace :fn "handle-message" :event :subscribe-team :team-id team-id)
(let [msgbus-fn (:msgbus @wsp)
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
subs (get-in @wsp [::subscriptions team-id])
xform (comp
(remove #(= (:session-id %) session-id))
(map #(assoc % :subs-id team-id)))]
(a/go
(when (not= (:team-id subs) team-id)
;; if it exists we just need to close that
(when-let [channel (:channel subs)]
(a/close! channel)
(a/<! (msgbus-fn :cmd :purge :chans [channel])))
(let [channel (a/chan (a/dropping-buffer 64) xform)]
;; Message forwarding
(a/pipe channel output-ch false)
(let [state {:team-id team-id :channel channel :topic team-id}]
(swap! wsp update ::subscriptions assoc team-id state))
(a/<! (msgbus-fn :cmd :sub :topic team-id :chan channel)))))))
(defmethod handle-message :subscribe-file
[wsp {:keys [subs-id file-id] :as params}]
(l/trace :fn "handle-message" :event :subscribe-file :subs-id subs-id :file-id file-id)
(let [msgbus-fn (:msgbus @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
xform (comp
(remove #(= (:session-id %) session-id))
(map #(assoc % :subs-id subs-id)))
channel (a/chan (a/dropping-buffer 64) xform)]
;; Message forwarding
(a/go-loop []
(when-let [{:keys [type] :as message} (a/<! channel)]
(when (or (= :join-file type)
(= :leave-file type)
(= :disconnect type))
(let [message {:type :presence
:file-id file-id
:session-id session-id
:profile-id profile-id}]
(a/<! (msgbus-fn :cmd :pub
:topic file-id
:message message))))
(a/>! output-ch message)
(recur)))
(let [state {:file-id file-id :channel channel :topic file-id}]
(swap! wsp update ::subscriptions assoc subs-id state))
(a/go
;; Subscribe to file topic
(a/<! (msgbus-fn :cmd :sub :topic file-id :chan channel))
;; Notifify the rest of participants of the new connection.
(let [message {:type :join-file
:file-id file-id
:session-id session-id
:profile-id profile-id}]
(a/<! (msgbus-fn :cmd :pub
:topic file-id
:message message))))))
(defmethod handle-message :unsubscribe-file
[wsp {:keys [subs-id] :as params}]
(l/trace :fn "handle-message" :event :unsubscribe-file :subs-id subs-id)
(let [msgbus-fn (:msgbus @wsp)
session-id (::session-id @wsp)
profile-id (::profile-id @wsp)]
(a/go
(when-let [{:keys [file-id channel]} (get-in @wsp [::subscriptions subs-id])]
(let [message {:type :leave-file
:file-id file-id
:session-id session-id
:profile-id profile-id}]
(a/close! channel)
(a/<! (msgbus-fn :cmd :pub :topic file-id :message message))
(a/<! (msgbus-fn :cmd :purge :chans [channel])))))))
(defmethod handle-message :keepalive
[_ _]
(l/trace :fn "handle-message" :event :keepalive)
(a/go :nothing))
(defmethod handle-message :pointer-update
[wsp {:keys [subs-id] :as message}]
(a/go
;; Only allow receive pointer updates when active subscription
(when-let [{:keys [topic]} (get-in @wsp [::subscriptions subs-id])]
(let [msgbus-fn (:msgbus @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
message (-> message
(dissoc :subs-id)
(assoc :profile-id profile-id)
(assoc :session-id session-id))]
(a/<! (msgbus-fn :cmd :pub
:topic topic
:message message))))))
(defmethod handle-message :default
[_ message]
(a/go
(l/log :level :warn
:msg "received unexpected message"
:message message)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP HANDLER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::msgbus fn?)
(s/def ::session-id ::us/uuid)
(s/def ::handler-params
(s/keys :req-un [::session-id]))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::msgbus ::db/pool ::mtx/metrics]))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [profile-id params] :as req} respond raise]
(let [{:keys [session-id]} (us/conform ::handler-params params)
cfg (-> cfg
(assoc ::profile-id profile-id)
(assoc ::session-id session-id))]
(l/trace :hint "http request to websocket" :profile-id profile-id :session-id session-id)
(cond
(not profile-id)
(raise (ex/error :type :authentication
:hint "Authentication required."))
(not (yws/upgrade-request? req))
(raise (ex/error :type :validation
:code :websocket-request-expected
:hint "this endpoint only accepts websocket connections"))
:else
(->> (ws/handler handle-message cfg)
(yws/upgrade req)
(respond))))))

View File

@@ -16,7 +16,6 @@
[app.config :as cf]
[app.db :as db]
[app.util.async :as aa]
[app.util.http :as http]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.core.async :as a]
@@ -24,13 +23,30 @@
[cuerdas.core :as str]
[integrant.core :as ig]
[lambdaisland.uri :as u]
[promesa.exec :as px]))
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
(defn parse-client-ip
[{:keys [headers] :as request}]
(or (some-> (get headers "x-forwarded-for") (str/split ",") first)
(get headers "x-real-ip")
(get request :remote-addr)))
[request]
(or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first)
(yrq/get-header request "x-real-ip")
(yrq/remote-addr request)))
(defn extract-utm-params
"Extracts additional data from params and namespace them under
`penpot` ns."
[params]
(letfn [(process-param [params k v]
(let [sk (d/name k)]
(cond-> params
(str/starts-with? sk "utm_")
(assoc (->> sk str/kebab (keyword "penpot")) v)
(str/starts-with? sk "mtm_")
(assoc (->> sk str/kebab (keyword "penpot")) v))))]
(reduce-kv process-param {} params)))
(defn profile->props
[profile]
@@ -41,33 +57,26 @@
(defn clean-props
[{:keys [profile-id] :as event}]
(letfn [(clean-common [props]
(-> props
(dissoc :session-id)
(dissoc :password)
(dissoc :old-password)
(dissoc :token)))
(let [invalid-keys #{:session-id
:password
:old-password
:token}
xform (comp
(remove (fn [kv]
(qualified-keyword? (first kv))))
(remove (fn [kv]
(contains? invalid-keys (first kv))))
(remove (fn [[k v]]
(and (= k :profile-id)
(= v profile-id))))
(filter (fn [[_ v]]
(or (string? v)
(keyword? v)
(uuid? v)
(boolean? v)
(number? v)))))]
(clean-profile-id [props]
(cond-> props
(= profile-id (:profile-id props))
(dissoc :profile-id)))
(clean-complex-data [props]
(reduce-kv (fn [props k v]
(cond-> props
(or (string? v)
(uuid? v)
(boolean? v)
(number? v))
(assoc k v)
(keyword? v)
(assoc k (name v))))
{}
props))]
(update event :props #(-> % clean-common clean-profile-id clean-complex-data))))
(update event :props #(into {} xform %))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP Handler
@@ -82,52 +91,61 @@
(s/def ::timestamp dt/instant?)
(s/def ::context (s/map-of ::us/keyword any?))
(s/def ::event
(s/def ::frontend-event
(s/keys :req-un [::type ::name ::props ::timestamp ::profile-id]
:opt-un [::context]))
(s/def ::events (s/every ::event))
(s/def ::frontend-events (s/every ::frontend-event))
(defmethod ig/init-key ::http-handler
[_ {:keys [executor] :as cfg}]
(fn [{:keys [params profile-id] :as request}]
(when (contains? cf/flags :audit-log)
(let [events (->> (:events params)
(remove #(not= profile-id (:profile-id %)))
(us/conform ::events))
ip-addr (parse-client-ip request)
cfg (-> cfg
(assoc :source "frontend")
(assoc :events events)
(assoc :ip-addr ip-addr))]
(px/run! executor #(persist-http-events cfg))))
{:status 204 :body ""}))
[_ {:keys [executor pool] :as cfg}]
(if (or (db/read-only? pool) (not (contains? cf/flags :audit-log)))
(do
(l/warn :hint "audit log http handler disabled or db is read-only")
(fn [_ respond _]
(respond (yrs/response 204))))
(letfn [(handler [{:keys [profile-id] :as request}]
(let [events (->> (:events (:params request))
(remove #(not= profile-id (:profile-id %)))
(us/conform ::frontend-events))
ip-addr (parse-client-ip request)
cfg (-> cfg
(assoc :source "frontend")
(assoc :events events)
(assoc :ip-addr ip-addr))]
(persist-http-events cfg)))
(handle-error [cause]
(let [xdata (ex-data cause)]
(if (= :spec-validation (:code xdata))
(l/error ::l/raw (str "spec validation on persist-events:\n" (us/pretty-explain xdata)))
(l/error :hint "error on persist-events" :cause cause))))]
(fn [request respond _]
;; Fire and forget, log error in case of errro
(-> (px/submit! executor #(handler request))
(p/catch handle-error))
(respond (yrs/response 204))))))
(defn- persist-http-events
[{:keys [pool events ip-addr source] :as cfg}]
(try
(let [columns [:id :name :source :type :tracked-at :profile-id :ip-addr :props :context]
prepare-xf (map (fn [event]
[(uuid/next)
(:name event)
source
(:type event)
(:timestamp event)
(:profile-id event)
(db/inet ip-addr)
(db/tjson (:props event))
(db/tjson (d/without-nils (:context event)))]))
events (us/conform ::events events)]
(when (seq events)
(->> (into [] prepare-xf events)
(db/insert-multi! pool :audit-log columns))))
(catch Throwable e
(let [xdata (ex-data e)]
(if (= :spec-validation (:code xdata))
(l/error ::l/raw (str "spec validation on persist-events:\n"
(:explain xdata)))
(l/error :hint "error on persist-events"
:cause e))))))
(let [columns [:id :name :source :type :tracked-at :profile-id :ip-addr :props :context]
prepare-xf (map (fn [event]
[(uuid/next)
(:name event)
source
(:type event)
(:timestamp event)
(:profile-id event)
(db/inet ip-addr)
(db/tjson (:props event))
(db/tjson (d/without-nils (:context event)))]))]
(when (seq events)
(->> (into [] prepare-xf events)
(db/insert-multi! pool :audit-log columns)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Collector
@@ -142,36 +160,53 @@
(defmethod ig/pre-init-spec ::collector [_]
(s/keys :req-un [::db/pool ::wrk/executor]))
(def event-xform
(s/def ::ip-addr string?)
(s/def ::backend-event
(s/keys :req-un [::type ::name ::profile-id]
:opt-un [::ip-addr ::props]))
(def ^:private backend-event-xform
(comp
(filter :profile-id)
(filter #(us/valid? ::backend-event %))
(map clean-props)))
(defmethod ig/init-key ::collector
[_ cfg]
(when (contains? cf/flags :audit-log)
(l/info :msg "intializing audit log collector")
(let [input (a/chan 512 event-xform)
[_ {:keys [pool] :as cfg}]
(cond
(not (contains? cf/flags :audit-log))
(do
(l/info :hint "audit log collection disabled")
(constantly nil))
(db/read-only? pool)
(do
(l/warn :hint "audit log collection disabled, db is read-only")
(constantly nil))
:else
(let [input (a/chan 512 backend-event-xform)
buffer (aa/batch input {:max-batch-size 100
:max-batch-age (* 10 1000) ; 10s
:init []})]
(l/info :hint "audit log collector initialized")
(a/go-loop []
(when-let [[_type events] (a/<! buffer)]
(let [res (a/<! (persist-events cfg events))]
(when (ex/exception? res)
(l/error :hint "error on persiting events"
:cause res)))
(recur)))
(l/error :hint "error on persisting events" :cause res))
(recur))))
(fn [& {:keys [cmd] :as params}]
(let [params (-> params
(dissoc :cmd)
(assoc :tracked-at (dt/now)))]
(case cmd
:stop (a/close! input)
:submit (when-not (a/offer! input params)
(l/warn :msg "activity channel is full"))))))))
(case cmd
:stop
(a/close! input)
:submit
(let [params (-> params
(dissoc :cmd)
(assoc :tracked-at (dt/now)))]
(when-not (a/offer! input params)
(l/warn :hint "activity channel is full"))))))))
(defn- persist-events
[{:keys [pool executor] :as cfg} events]
@@ -189,22 +224,23 @@
(db/with-atomic [conn pool]
(db/insert-multi! conn :audit-log
[:id :name :type :profile-id :tracked-at :ip-addr :props :source]
(sequence (map event->row) events)))))))
(sequence (keep event->row) events)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Archive Task
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; This is a task responsible to send the accomulated events to an
;; This is a task responsible to send the accumulated events to an
;; external service for archival.
(declare archive-events)
(s/def ::http-client fn?)
(s/def ::uri ::us/string)
(s/def ::tokens fn?)
(defmethod ig/pre-init-spec ::archive-task [_]
(s/keys :req-un [::db/pool ::tokens]
(s/keys :req-un [::db/pool ::tokens ::http-client]
:opt-un [::uri]))
(defmethod ig/init-key ::archive-task
@@ -216,6 +252,7 @@
(:enabled props false))
uri (or uri (:uri props))
cfg (assoc cfg :uri uri)]
(when (and enabled (not uri))
(ex/raise :type :internal
:code :task-not-configured
@@ -231,11 +268,11 @@
"select * from audit_log
where archived_at is null
order by created_at asc
limit 1000
limit 256
for update skip locked;")
(defn archive-events
[{:keys [pool uri tokens] :as cfg}]
[{:keys [pool uri tokens http-client] :as cfg}]
(letfn [(decode-row [{:keys [props ip-addr context] :as row}]
(cond-> row
(db/pgobject? props)
@@ -271,12 +308,13 @@
:method :post
:headers headers
:body body}
resp (http/send! params)]
resp (http-client params {:sync? true})]
(if (= (:status resp) 204)
true
(do
(l/warn :hint "unable to archive events"
:resp-status (:status resp))
(l/error :hint "unable to archive events"
:resp-status (:status resp)
:resp-body (:body resp))
false))))
(mark-as-archived [conn rows]

View File

@@ -7,17 +7,13 @@
(ns app.loggers.database
"A specific logger impl that persists errors on the database."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.util.async :as aa]
[app.util.template :as tmpl]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]))
@@ -32,11 +28,10 @@
(defn- persist-on-database!
[{:keys [pool] :as cfg} {:keys [id] :as event}]
(db/with-atomic [conn pool]
(db/insert! conn :server-error-report
{:id id :content (db/tjson event)})))
(when-not (db/read-only? pool)
(db/insert! pool :server-error-report {:id id :content (db/tjson event)})))
(defn- parse-context
(defn- parse-event-data
[event]
(reduce-kv
(fn [acc k v]
@@ -46,35 +41,42 @@
(str/blank? v) acc
:else (assoc acc k v)))
{}
(:context event)))
event))
(defn parse-event
[event]
(-> (parse-context event)
(merge (dissoc event :context))
(-> (parse-event-data event)
(assoc :tenant (cf/get :tenant))
(assoc :host (cf/get :host))
(assoc :public-uri (cf/get :public-uri))
(assoc :version (:full cf/version))))
(assoc :version (:full cf/version))
(update :id #(or % (uuid/next)))))
(defn handle-event
[{:keys [executor] :as cfg} event]
(aa/with-thread executor
(try
(let [event (parse-event event)]
(let [event (parse-event event)
uri (cf/get :public-uri)]
(l/debug :hint "registering error on database" :id (:id event)
:uri (str uri "/dbg/error/" (:id event)))
(persist-on-database! cfg event))
(catch Exception e
(l/warn :hint "unexpected exception on database error logger"
:cause e)))))
(catch Exception cause
(l/warn :hint "unexpected exception on database error logger" :cause cause)))))
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]))
(defn error-event?
[event]
(= "error" (:logger/level event)))
(defmethod ig/init-key ::reporter
[_ {:keys [receiver] :as cfg}]
(l/info :msg "initializing database error persistence")
(let [output (a/chan (a/sliding-buffer 128)
(filter #(= (:level %) "error")))]
(let [output (a/chan (a/sliding-buffer 5) (filter error-event?))]
(receiver :sub output)
(a/go-loop []
(let [msg (a/<! output)]
@@ -88,39 +90,3 @@
(defmethod ig/halt-key! ::reporter
[_ output]
(a/close! output))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Http Handler
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool]))
(defmethod ig/init-key ::handler
[_ {:keys [pool] :as cfg}]
(letfn [(parse-id [request]
(let [id (get-in request [:path-params :id])
id (us/uuid-conformer id)]
(when (uuid? id)
id)))
(retrieve-report [id]
(ex/ignoring
(when-let [{:keys [content] :as row} (db/get-by-id pool :server-error-report id)]
(assoc row :content (db/decode-transit-pgobject content)))))
(render-template [{:keys [content] :as report}]
(some-> (io/resource "error-report.tmpl")
(tmpl/render content)))]
(fn [request]
(let [result (some-> (parse-id request)
(retrieve-report)
(render-template))]
(if result
{:status 200
:headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}
:body result}
{:status 404
:body "not found"})))))

View File

@@ -10,36 +10,34 @@
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cfg]
[app.util.async :as aa]
[app.util.http :as http]
[app.util.json :as json]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(declare handle-event)
(declare ^:private handle-event)
(declare ^:private start-rcv-loop)
(s/def ::uri ::us/string)
(s/def ::receiver fn?)
(s/def ::http-client fn?)
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req-un [::wrk/executor ::receiver]
(s/keys :req-un [ ::receiver ::http-client]
:opt-un [::uri]))
(defmethod ig/init-key ::reporter
[_ {:keys [receiver uri] :as cfg}]
(when uri
(l/info :msg "intializing loki reporter" :uri uri)
(let [input (a/chan (a/dropping-buffer 512))]
(l/info :msg "initializing loki reporter" :uri uri)
(let [input (a/chan (a/dropping-buffer 2048))]
(receiver :sub input)
(a/go-loop []
(let [msg (a/<! input)]
(if (nil? msg)
(l/info :msg "stoping error reporting loop")
(do
(a/<! (handle-event cfg msg))
(recur)))))
(doto (Thread. #(start-rcv-loop cfg input))
(.setDaemon true)
(.setName "penpot/loki-sender")
(.start))
input)))
(defmethod ig/halt-key! ::reporter
@@ -47,53 +45,49 @@
(when output
(a/close! output)))
(defn- start-rcv-loop
[cfg input]
(loop []
(let [msg (a/<!! input)]
(when-not (nil? msg)
(handle-event cfg msg)
(recur))))
(l/info :msg "stoping error reporting loop"))
(defn- prepare-payload
[event]
(let [labels {:host (cfg/get :host)
:tenant (cfg/get :tenant)
:version (:full cfg/version)
:logger (:logger event)
:level (:level event)}]
:logger (:logger/name event)
:level (:logger/level event)}]
{:streams
[{:stream labels
:values [[(str (* (inst-ms (:created-at event)) 1000000))
(str (:message event)
(when-let [error (:error event)]
(str "\n" (:trace error))))]]}]}))
(when-let [error (:trace event)]
(str "\n" error)))]]}]}))
(defn- send-log
[uri payload i]
(try
(let [response (http/send! {:uri uri
:timeout 6000
:method :post
:headers {"content-type" "application/json"}
:body (json/encode payload)})]
(cond
(= (:status response) 204)
true
(= (:status response) 400)
(do
(l/error :hint "error on sending log to loki (no retry)"
:rsp (pr-str response))
true)
:else
(do
(l/error :hint "error on sending log to loki" :try i
:rsp (pr-str response))
false)))
(catch Exception e
(l/error :hint "error on sending message to loki" :cause e :try i)
false)))
(defn- make-request
[{:keys [http-client uri] :as cfg} payload]
(http-client {:uri uri
:timeout 3000
:method :post
:headers {"content-type" "application/json"}
:body (json/write payload)}
{:sync? true}))
(defn- handle-event
[{:keys [executor uri]} event]
(aa/with-thread executor
(let [payload (prepare-payload event)]
(loop [i 1]
(when (and (not (send-log uri payload i)) (< i 20))
(Thread/sleep (* i 2000))
(recur (inc i)))))))
[cfg event]
(try
(let [payload (prepare-payload event)
response (make-request cfg payload)]
(when-not (= 204 (:status response))
(map? response)
(l/error :hint "error on sending log to loki (unexpected response)"
:response (pr-str response))))
(catch Throwable cause
(l/error :hint "error on sending log to loki (unexpected exception)"
:cause cause))))

View File

@@ -9,52 +9,47 @@
(:require
[app.common.logging :as l]
[app.config :as cf]
[app.db :as db]
[app.loggers.database :as ldb]
[app.util.async :as aa]
[app.util.http :as http]
[app.util.json :as json]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
[integrant.core :as ig]
[promesa.core :as p]))
(defonce enabled (atom true))
(defn- send-mattermost-notification!
[cfg {:keys [host id public-uri] :as event}]
(try
(let [uri (:uri cfg)
text (str "Exception on (host: " host ", url: " public-uri "/dbg/error-by-id/" id ")\n"
(when-let [pid (:profile-id event)]
(str "- profile-id: #uuid-" pid "\n")))
rsp (http/send! {:uri uri
:method :post
:headers {"content-type" "application/json"}
:body (json/encode-str {:text text})})]
(when (not= (:status rsp) 200)
(l/error :hint "error on sending data to mattermost"
:response (pr-str rsp))))
(catch Exception e
(l/error :hint "unexpected exception on error reporter"
:cause e))))
[{:keys [http-client] :as cfg} {:keys [host id public-uri] :as event}]
(let [uri (:uri cfg)
text (str "Exception on (host: " host ", url: " public-uri "/dbg/error/" id ")\n"
(when-let [pid (:profile-id event)]
(str "- profile-id: #uuid-" pid "\n")))]
(p/then
(http-client {:uri uri
:method :post
:headers {"content-type" "application/json"}
:body (json/write-str {:text text})})
(fn [{:keys [status] :as rsp}]
(when (not= status 200)
(l/warn :hint "error on sending data to mattermost"
:response (pr-str rsp)))))))
(defn handle-event
[{:keys [executor] :as cfg} event]
(aa/with-thread executor
(try
(let [event (ldb/parse-event event)]
(when @enabled
(send-mattermost-notification! cfg event)))
(catch Exception e
(l/warn :hint "unexpected exception on error reporter" :cause e)))))
[cfg event]
(let [ch (a/chan)]
(-> (p/let [event (ldb/parse-event event)]
(send-mattermost-notification! cfg event))
(p/finally (fn [_ cause]
(when cause
(l/warn :hint "unexpected exception on error reporter" :cause cause))
(a/close! ch))))
ch))
(s/def ::http-client fn?)
(s/def ::uri ::cf/error-report-webhook)
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]
(s/keys :req-un [::http-client ::receiver]
:opt-un [::uri]))
(defmethod ig/init-key ::reporter
@@ -62,7 +57,8 @@
(when uri
(l/info :msg "initializing mattermost error reporter" :uri uri)
(let [output (a/chan (a/sliding-buffer 128)
(filter #(= (:level %) "error")))]
(filter (fn [event]
(= (:logger/level event) "error"))))]
(receiver :sub output)
(a/go-loop []
(let [msg (a/<! output)]

View File

@@ -120,8 +120,6 @@
(.captureMessage ^IHub shub msg)
))
]
;; (clojure.pprint/pprint event)
(when @enabled
(.withScope ^IHub shub (reify ScopeCallback
(run [_ scope]

View File

@@ -7,6 +7,7 @@
(ns app.loggers.zmq
"A generic ZMQ listener."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.util.json :as json]
@@ -30,13 +31,17 @@
(defmethod ig/init-key ::receiver
[_ {:keys [endpoint] :as cfg}]
(l/info :msg "intializing ZMQ receiver" :bind endpoint)
(l/info :msg "initializing ZMQ receiver" :bind endpoint)
(let [buffer (a/chan 1)
output (a/chan 1 (comp (filter map?)
(map prepare)))
(keep prepare)))
mult (a/mult output)]
(when endpoint
(a/thread (start-rcv-loop {:out buffer :endpoint endpoint})))
(let [thread (Thread. #(start-rcv-loop {:out buffer :endpoint endpoint}))]
(.setDaemon thread false)
(.setName thread "penpot/zmq-logger-receiver")
(.start thread)))
(a/pipe buffer output)
(with-meta
(fn [cmd ch]
@@ -52,37 +57,54 @@
[_ f]
(a/close! (::buffer (meta f))))
(def ^:private json-mapper
(json/mapper
{:encode-key-fn str/camel
:decode-key-fn (comp keyword str/kebab)}))
(defn- start-rcv-loop
([] (start-rcv-loop nil))
([{:keys [out endpoint] :or {endpoint "tcp://localhost:5556"}}]
(let [out (or out (a/chan 1))
zctx (ZContext.)
zctx (ZContext. 1)
socket (.. zctx (createSocket SocketType/SUB))]
(.. socket (connect ^String endpoint))
(.. socket (subscribe ""))
(.. socket (setReceiveTimeOut 5000))
(loop []
(let [msg (.recv ^ZMQ$Socket socket)
msg (json/decode msg)
msg (ex/ignoring (json/read msg json-mapper))
msg (if (nil? msg) :empty msg)]
(if (a/>!! out msg)
(recur)
(do
(.close ^java.lang.AutoCloseable socket)
(.close ^java.lang.AutoCloseable zctx))))))))
(.destroy ^ZContext zctx))))))))
(s/def ::logger-name string?)
(s/def ::level string?)
(s/def ::thread string?)
(s/def ::time-millis integer?)
(s/def ::message string?)
(s/def ::context-map map?)
(s/def ::thrown map?)
(s/def ::log4j-event
(s/keys :req-un [::logger-name ::level ::thread ::time-millis ::message]
:opt-un [::context-map ::thrown]))
(defn- prepare
[event]
(merge
{:logger (:loggerName event)
:level (str/lower (:level event))
:thread (:thread event)
:created-at (dt/instant (:timeMillis event))
:message (:message event)}
(when-let [ctx (:contextMap event)]
{:context ctx})
(when-let [thrown (:thrown event)]
{:error
{:class (:name thrown)
:message (:message thrown)
:trace (:extendedStackTrace thrown)}})))
(if (s/valid? ::log4j-event event)
(merge {:message (:message event)
:created-at (dt/instant (:time-millis event))
:logger/name (:logger-name event)
:logger/level (str/lower (:level event))}
(when-let [trace (-> event :thrown :extended-stack-trace)]
{:trace trace})
(:context-map event))
(do
(l/warn :hint "invalid event" :event event)
nil)))

View File

@@ -9,49 +9,63 @@
[app.common.logging :as l]
[app.config :as cf]
[app.util.time :as dt]
[integrant.core :as ig]))
[integrant.core :as ig])
(:gen-class))
(def system-config
{:app.db/pool
{:uri (cf/get :database-uri)
:username (cf/get :database-username)
:password (cf/get :database-password)
:read-only (cf/get :database-readonly false)
:metrics (ig/ref :app.metrics/metrics)
:migrations (ig/ref :app.migrations/all)
:name :main
:min-pool-size 0
:max-pool-size 30}
:name :main
:min-size (cf/get :database-min-pool-size 0)
:max-size (cf/get :database-max-pool-size 30)}
:app.metrics/metrics
{:definitions
{:profile-register
{:name "actions_profile_register_count"
:help "A global counter of user registrations."
:type :counter}
;; Default thread pool for IO operations
[::default :app.worker/executor]
{:parallelism (cf/get :default-executor-parallelism 60)
:prefix :default}
:profile-activation
{:name "actions_profile_activation_count"
:help "A global counter of profile activations"
:type :counter}
;; Constrained thread pool. Should only be used from high resources
;; demanding operations.
[::blocking :app.worker/executor]
{:parallelism (cf/get :blocking-executor-parallelism 10)
:prefix :blocking}
:update-file-changes
{:name "rpc_update_file_changes_total"
:help "A total number of changes submitted to update-file."
:type :counter}
;; Dedicated thread pool for backround tasks execution.
[::worker :app.worker/executor]
{:parallelism (cf/get :worker-executor-parallelism 10)
:prefix :worker}
:update-file-bytes-processed
{:name "rpc_update_file_bytes_processed_total"
:help "A total number of bytes processed by update-file."
:type :counter}}}
:app.worker/scheduler
{:parallelism 1
:prefix :scheduler}
:app.migrations/all
{:main (ig/ref :app.migrations/migrations)}
:app.worker/executors
{:default (ig/ref [::default :app.worker/executor])
:worker (ig/ref [::worker :app.worker/executor])
:blocking (ig/ref [::blocking :app.worker/executor])}
:app.worker/executors-monitor
{:metrics (ig/ref :app.metrics/metrics)
:scheduler (ig/ref :app.worker/scheduler)
:executors (ig/ref :app.worker/executors)}
:app.migrations/migrations
{}
:app.metrics/metrics
{}
:app.migrations/all
{:main (ig/ref :app.migrations/migrations)}
:app.msgbus/msgbus
{:backend (cf/get :msgbus-backend :redis)
:executor (ig/ref [::default :app.worker/executor])
:redis-uri (cf/get :redis-uri)}
:app.tokens/tokens
@@ -60,18 +74,22 @@
:app.storage/gc-deleted-task
{:pool (ig/ref :app.db/pool)
:storage (ig/ref :app.storage/storage)
:executor (ig/ref [::worker :app.worker/executor])
:min-age (dt/duration {:hours 2})}
:app.storage/gc-touched-task
{:pool (ig/ref :app.db/pool)}
{:pool (ig/ref :app.db/pool)}
:app.storage/recheck-task
:app.http/client
{:executor (ig/ref [::default :app.worker/executor])}
:app.http/session
{:store (ig/ref :app.http.session/store)}
:app.http.session/store
{:pool (ig/ref :app.db/pool)
:storage (ig/ref :app.storage/storage)}
:app.http.session/session
{:pool (ig/ref :app.db/pool)
:tokens (ig/ref :app.tokens/tokens)}
:tokens (ig/ref :app.tokens/tokens)
:executor (ig/ref [::default :app.worker/executor])}
:app.http.session/gc-task
{:pool (ig/ref :app.db/pool)
@@ -80,108 +98,100 @@
:app.http.session/updater
{:pool (ig/ref :app.db/pool)
:metrics (ig/ref :app.metrics/metrics)
:executor (ig/ref :app.worker/executor)
:session (ig/ref :app.http.session/session)
:executor (ig/ref [::worker :app.worker/executor])
:session (ig/ref :app.http/session)
:max-batch-age (cf/get :http-session-updater-batch-max-age)
:max-batch-size (cf/get :http-session-updater-batch-max-size)}
:app.http.awsns/handler
{:tokens (ig/ref :app.tokens/tokens)
:pool (ig/ref :app.db/pool)}
{:tokens (ig/ref :app.tokens/tokens)
:pool (ig/ref :app.db/pool)
:http-client (ig/ref :app.http/client)
:executor (ig/ref [::worker :app.worker/executor])}
:app.http/server
{:port (cf/get :http-server-port)
:router (ig/ref :app.http/router)
:metrics (ig/ref :app.metrics/metrics)
:ws {"/ws/notifications" (ig/ref :app.notifications/handler)}}
{:port (cf/get :http-server-port)
:host (cf/get :http-server-host)
:router (ig/ref :app.http/router)
:metrics (ig/ref :app.metrics/metrics)
:executor (ig/ref [::default :app.worker/executor])
:io-threads (cf/get :http-server-io-threads)
:max-body-size (cf/get :http-server-max-body-size)
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
:app.http/router
{:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http.session/session)
:tokens (ig/ref :app.tokens/tokens)
:public-uri (cf/get :public-uri)
:metrics (ig/ref :app.metrics/metrics)
:oauth (ig/ref :app.http.oauth/handler)
:assets (ig/ref :app.http.assets/handlers)
:storage (ig/ref :app.storage/storage)
:sns-webhook (ig/ref :app.http.awsns/handler)
:feedback (ig/ref :app.http.feedback/handler)
:audit-http-handler (ig/ref :app.loggers.audit/http-handler)
:error-report-handler (ig/ref :app.loggers.database/handler)}
{:assets (ig/ref :app.http.assets/handlers)
:feedback (ig/ref :app.http.feedback/handler)
:session (ig/ref :app.http/session)
:awsns-handler (ig/ref :app.http.awsns/handler)
:oauth (ig/ref :app.http.oauth/handler)
:debug (ig/ref :app.http.debug/handlers)
:ws (ig/ref :app.http.websocket/handler)
:metrics (ig/ref :app.metrics/metrics)
:public-uri (cf/get :public-uri)
:storage (ig/ref :app.storage/storage)
:tokens (ig/ref :app.tokens/tokens)
:audit-handler (ig/ref :app.loggers.audit/http-handler)
:rpc (ig/ref :app.rpc/rpc)
:executor (ig/ref [::default :app.worker/executor])}
:app.http.debug/handlers
{:pool (ig/ref :app.db/pool)
:executor (ig/ref [::worker :app.worker/executor])}
:app.http.websocket/handler
{:pool (ig/ref :app.db/pool)
:metrics (ig/ref :app.metrics/metrics)
:msgbus (ig/ref :app.msgbus/msgbus)}
:app.http.assets/handlers
{:metrics (ig/ref :app.metrics/metrics)
:assets-path (cf/get :assets-path)
:storage (ig/ref :app.storage/storage)
:executor (ig/ref [::default :app.worker/executor])
:cache-max-age (dt/duration {:hours 24})
:signature-max-age (dt/duration {:hours 24 :minutes 5})}
:app.http.feedback/handler
{:pool (ig/ref :app.db/pool)}
{:pool (ig/ref :app.db/pool)
:executor (ig/ref [::default :app.worker/executor])}
:app.http.oauth/handler
{:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http.session/session)
:pool (ig/ref :app.db/pool)
:tokens (ig/ref :app.tokens/tokens)
:audit (ig/ref :app.loggers.audit/collector)
:public-uri (cf/get :public-uri)}
;; RLimit definition for password hashing
:app.rlimits/password
(cf/get :rlimits-password)
;; RLimit definition for image processing
:app.rlimits/image
(cf/get :rlimits-image)
;; RLimit definition for font processing
:app.rlimits/font
(cf/get :rlimits-font 2)
;; A collection of rlimits as hash-map.
:app.rlimits/all
{:password (ig/ref :app.rlimits/password)
:image (ig/ref :app.rlimits/image)
:font (ig/ref :app.rlimits/font)}
{:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http/session)
:pool (ig/ref :app.db/pool)
:tokens (ig/ref :app.tokens/tokens)
:audit (ig/ref :app.loggers.audit/collector)
:executor (ig/ref [::default :app.worker/executor])
:http-client (ig/ref :app.http/client)
:public-uri (cf/get :public-uri)}
:app.rpc/rpc
{:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http.session/session)
:tokens (ig/ref :app.tokens/tokens)
:metrics (ig/ref :app.metrics/metrics)
:storage (ig/ref :app.storage/storage)
:msgbus (ig/ref :app.msgbus/msgbus)
:rlimits (ig/ref :app.rlimits/all)
:public-uri (cf/get :public-uri)
:audit (ig/ref :app.loggers.audit/collector)}
:app.notifications/handler
{:msgbus (ig/ref :app.msgbus/msgbus)
:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http.session/session)
:metrics (ig/ref :app.metrics/metrics)
:executor (ig/ref :app.worker/executor)}
:app.worker/executor
{:min-threads 0
:max-threads 256
:idle-timeout 60000
:name :worker}
{:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http/session)
:tokens (ig/ref :app.tokens/tokens)
:metrics (ig/ref :app.metrics/metrics)
:storage (ig/ref :app.storage/storage)
:msgbus (ig/ref :app.msgbus/msgbus)
:public-uri (cf/get :public-uri)
:audit (ig/ref :app.loggers.audit/collector)
:http-client (ig/ref :app.http/client)
:executors (ig/ref :app.worker/executors)}
:app.worker/worker
{:executor (ig/ref :app.worker/executor)
{:executor (ig/ref [::worker :app.worker/executor])
:tasks (ig/ref :app.worker/registry)
:metrics (ig/ref :app.metrics/metrics)
:pool (ig/ref :app.db/pool)}
:app.worker/scheduler
{:executor (ig/ref :app.worker/executor)
:app.worker/cron
{:executor (ig/ref [::worker :app.worker/executor])
:scheduler (ig/ref :app.worker/scheduler)
:tasks (ig/ref :app.worker/registry)
:pool (ig/ref :app.db/pool)
:schedule
:entries
[{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :file-media-gc}
:task :file-gc}
{:cron #app/cron "0 0 * * * ?" ;; hourly
:task :file-xlog-gc}
@@ -195,42 +205,36 @@
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :session-gc}
{:cron #app/cron "0 0 * * * ?" ;; hourly
:task :storage-recheck}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :objects-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :tasks-gc}
{:cron #app/cron "0 30 */3,23 * * ?"
:task :telemetry}
(when (cf/get :fdata-storage-backed)
{:cron #app/cron "0 0 * * * ?" ;; hourly
:task :file-offload})
(when (contains? cf/flags :audit-log-archive)
{:cron #app/cron "0 */3 * * * ?" ;; every 3m
{:cron #app/cron "0 */5 * * * ?" ;; every 5m
:task :audit-log-archive})
(when (contains? cf/flags :audit-log-gc)
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :audit-log-gc})
(when (or (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
{:cron #app/cron "0 0 */6 * * ?" ;; every 6h
:task :telemetry})]}
:task :audit-log-gc})]}
:app.worker/registry
{:metrics (ig/ref :app.metrics/metrics)
:tasks
{:sendmail (ig/ref :app.emails/sendmail-handler)
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
:file-media-gc (ig/ref :app.tasks.file-media-gc/handler)
:file-gc (ig/ref :app.tasks.file-gc/handler)
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:storage-deleted-gc (ig/ref :app.storage/gc-deleted-task)
:storage-touched-gc (ig/ref :app.storage/gc-touched-task)
:storage-recheck (ig/ref :app.storage/recheck-task)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
:telemetry (ig/ref :app.tasks.telemetry/handler)
:session-gc (ig/ref :app.http.session/gc-task)
@@ -258,7 +262,7 @@
:storage (ig/ref :app.storage/storage)
:max-age cf/deletion-delay}
:app.tasks.file-media-gc/handler
:app.tasks.file-gc/handler
{:pool (ig/ref :app.db/pool)
:max-age cf/deletion-delay}
@@ -276,7 +280,8 @@
{:pool (ig/ref :app.db/pool)
:version (:full cf/version)
:uri (cf/get :telemetry-uri)
:sprops (ig/ref :app.setup/props)}
:sprops (ig/ref :app.setup/props)
:http-client (ig/ref :app.http/client)}
:app.srepl/server
{:port (cf/get :srepl-port)
@@ -294,72 +299,65 @@
:app.loggers.audit/http-handler
{:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}
:executor (ig/ref [::default :app.worker/executor])}
:app.loggers.audit/collector
{:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}
:executor (ig/ref [::worker :app.worker/executor])}
:app.loggers.audit/archive-task
{:uri (cf/get :audit-log-archive-uri)
:tokens (ig/ref :app.tokens/tokens)
:pool (ig/ref :app.db/pool)}
{:uri (cf/get :audit-log-archive-uri)
:tokens (ig/ref :app.tokens/tokens)
:pool (ig/ref :app.db/pool)
:http-client (ig/ref :app.http/client)}
:app.loggers.audit/gc-task
{:max-age (cf/get :audit-log-gc-max-age cf/deletion-delay)
:pool (ig/ref :app.db/pool)}
:app.loggers.loki/reporter
{:uri (cf/get :loggers-loki-uri)
:receiver (ig/ref :app.loggers.zmq/receiver)
:executor (ig/ref :app.worker/executor)}
{:uri (cf/get :loggers-loki-uri)
:receiver (ig/ref :app.loggers.zmq/receiver)
:http-client (ig/ref :app.http/client)}
:app.loggers.mattermost/reporter
{:uri (cf/get :error-report-webhook)
:receiver (ig/ref :app.loggers.zmq/receiver)
:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}
{:uri (cf/get :error-report-webhook)
:receiver (ig/ref :app.loggers.zmq/receiver)
:http-client (ig/ref :app.http/client)}
:app.loggers.database/reporter
{:receiver (ig/ref :app.loggers.zmq/receiver)
:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}
:app.loggers.database/handler
{:pool (ig/ref :app.db/pool)}
:app.loggers.sentry/reporter
{:dsn (cf/get :sentry-dsn)
:trace-sample-rate (cf/get :sentry-trace-sample-rate 1.0)
:attach-stack-trace (cf/get :sentry-attach-stack-trace false)
:debug (cf/get :sentry-debug false)
:receiver (ig/ref :app.loggers.zmq/receiver)
:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)}
:executor (ig/ref [::worker :app.worker/executor])}
:app.storage/storage
{:pool (ig/ref :app.db/pool)
:executor (ig/ref :app.worker/executor)
:executor (ig/ref [::default :app.worker/executor])
:backends {
:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
:assets-db (ig/ref [::assets :app.storage.db/backend])
:assets-fs (ig/ref [::assets :app.storage.fs/backend])
:tmp (ig/ref [::tmp :app.storage.fs/backend])
:fdata-s3 (ig/ref [::fdata :app.storage.s3/backend])
:backends
{:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
:assets-db (ig/ref [::assets :app.storage.db/backend])
:assets-fs (ig/ref [::assets :app.storage.fs/backend])
;; keep this for backward compatibility
:s3 (ig/ref [::assets :app.storage.s3/backend])
:fs (ig/ref [::assets :app.storage.fs/backend])}}
:tmp (ig/ref [::tmp :app.storage.fs/backend])
:fdata-s3 (ig/ref [::fdata :app.storage.s3/backend])
;; keep this for backward compatibility
:s3 (ig/ref [::assets :app.storage.s3/backend])
:fs (ig/ref [::assets :app.storage.fs/backend])}}
[::fdata :app.storage.s3/backend]
{:region (cf/get :storage-fdata-s3-region)
:bucket (cf/get :storage-fdata-s3-bucket)
:prefix (cf/get :storage-fdata-s3-prefix)}
{:region (cf/get :storage-fdata-s3-region)
:bucket (cf/get :storage-fdata-s3-bucket)
:endpoint (cf/get :storage-fdata-s3-endpoint)
:prefix (cf/get :storage-fdata-s3-prefix)
:executor (ig/ref [::default :app.worker/executor])}
[::assets :app.storage.s3/backend]
{:region (cf/get :storage-assets-s3-region)
:bucket (cf/get :storage-assets-s3-bucket)}
{:region (cf/get :storage-assets-s3-region)
:endpoint (cf/get :storage-assets-s3-endpoint)
:bucket (cf/get :storage-assets-s3-bucket)
:executor (ig/ref [::default :app.worker/executor])}
[::assets :app.storage.fs/backend]
{:directory (cf/get :storage-assets-fs-directory)}

View File

@@ -12,7 +12,6 @@
[app.common.media :as cm]
[app.common.spec :as us]
[app.config :as cf]
[app.rlimits :as rlm]
[app.util.svg :as svg]
[buddy.core.bytes :as bb]
[buddy.core.codecs :as bc]
@@ -29,28 +28,30 @@
org.im4java.core.IMOperation
org.im4java.core.Info))
(s/def ::image-content-type cm/valid-image-types)
(s/def ::font-content-type cm/valid-font-types)
(s/def :internal.http.upload/filename ::us/string)
(s/def :internal.http.upload/size ::us/integer)
(s/def :internal.http.upload/content-type ::us/string)
(s/def :internal.http.upload/tempfile any?)
(s/def ::path fs/path?)
(s/def ::filename string?)
(s/def ::size integer?)
(s/def ::headers (s/map-of string? string?))
(s/def ::mtype string?)
(s/def ::upload
(s/keys :req-un [:internal.http.upload/filename
:internal.http.upload/size
:internal.http.upload/tempfile
:internal.http.upload/content-type]))
(s/keys :req-un [::filename ::size ::path]
:opt-un [::mtype ::headers]))
(defn validate-media-type
([mtype] (validate-media-type mtype cm/valid-image-types))
([mtype allowed]
(when-not (contains? allowed mtype)
;; A subset of fields from the ::upload spec
(s/def ::input
(s/keys :req-un [::path]
:opt-un [::mtype]))
(defn validate-media-type!
([upload] (validate-media-type! upload cm/valid-image-types))
([upload allowed]
(when-not (contains? allowed (:mtype upload))
(ex/raise :type :validation
:code :media-type-not-allowed
:hint "Seems like you are uploading an invalid media object"))))
:hint "Seems like you are uploading an invalid media object"))
upload))
(defmulti process :cmd)
(defmulti process-error class)
@@ -66,39 +67,23 @@
(throw error))
(defn run
[{:keys [rlimits] :as cfg} {:keys [rlimit] :or {rlimit :image} :as params}]
(us/assert map? rlimits)
(let [rlimit (get rlimits rlimit)]
(when-not rlimit
(ex/raise :type :internal
:code :rlimit-not-configured
:hint ":image rlimit not configured"))
(try
(rlm/execute rlimit (process params))
(catch Throwable e
(process-error e)))))
[params]
(try
(process params)
(catch Throwable e
(process-error e))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Thumbnails Generation
;; IMAGE THUMBNAILS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::cmd keyword?)
(s/def ::path (s/or :path fs/path?
:string string?
:file fs/file?))
(s/def ::input
(s/keys :req-un [::path]
:opt-un [::cm/mtype]))
(s/def ::width integer?)
(s/def ::height integer?)
(s/def ::format #{:jpeg :webp :png})
(s/def ::quality #(< 0 % 101))
(s/def ::thumbnail-params
(s/keys :req-un [::cmd ::input ::format ::width ::height]))
(s/keys :req-un [::input ::format ::width ::height]))
;; Related info on how thumbnails generation
;; http://www.imagemagick.org/Usage/thumbnails/
@@ -185,7 +170,7 @@
(ex/raise :type :validation
:code :invalid-svg-file
:hint "uploaded svg does not provides dimensions"))
(assoc info :mtype mtype))
(merge input info))
(let [instance (Info. (str path))
mtype' (.getProperty instance "Mime type")]
@@ -198,9 +183,9 @@
;; For an animated GIF, getImageWidth/Height returns the delta size of one frame (if no frame given
;; it returns size of the last one), whereas getPageWidth/Height always return the full size of
;; any frame.
{:width (.getPageWidth instance)
:height (.getPageHeight instance)
:mtype mtype}))))
(assoc input
:width (.getPageWidth instance)
:height (.getPageHeight instance))))))
(defmethod process-error org.im4java.core.InfoException
[error]
@@ -210,11 +195,9 @@
:cause error))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Fonts Generation
;; FONTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def all-fotmats #{"font/woff2", "font/woff", "font/otf", "font/ttf"})
(defmethod process :generate-fonts
[{:keys [input] :as params}]
(letfn [(ttf->otf [data]
@@ -334,10 +317,11 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn configure-assets-storage
"Given storage map, returns a storage configured with the apropriate
backend for assets."
[storage conn]
(-> storage
(assoc :conn conn)
(assoc :backend (cf/get :assets-storage-backend :assets-fs))))
"Given storage map, returns a storage configured with the appropriate
backend for assets and optional connection attached."
([storage]
(assoc storage :backend (cf/get :assets-storage-backend :assets-fs)))
([storage conn]
(-> storage
(assoc :conn conn)
(assoc :backend (cf/get :assets-storage-backend :assets-fs)))))

View File

@@ -5,48 +5,130 @@
;; Copyright (c) UXBOX Labs SL
(ns app.metrics
(:refer-clojure :exclude [run!])
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[clojure.spec.alpha :as s]
[integrant.core :as ig])
(:import
io.prometheus.client.CollectorRegistry
io.prometheus.client.Counter
io.prometheus.client.Counter$Child
io.prometheus.client.Gauge
io.prometheus.client.Gauge$Child
io.prometheus.client.Summary
io.prometheus.client.Summary$Child
io.prometheus.client.Summary$Builder
io.prometheus.client.Histogram
io.prometheus.client.Histogram$Child
io.prometheus.client.exporter.common.TextFormat
io.prometheus.client.hotspot.DefaultExports
io.prometheus.client.jetty.JettyStatisticsCollector
org.eclipse.jetty.server.handler.StatisticsHandler
java.io.StringWriter))
(declare instrument-vars!)
(declare instrument)
(set! *warn-on-reflection* true)
(declare create-registry)
(declare create)
(declare handler)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Entry Point
;; METRICS SERVICE PROVIDER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- handler
[registry _request]
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
writer (StringWriter.)]
(TextFormat/write004 writer samples)
{:headers {"content-type" TextFormat/CONTENT_TYPE_004}
:body (.toString writer)}))
(def default-metrics
{:update-file-changes
{:name "rpc_update_file_changes_total"
:help "A total number of changes submitted to update-file."
:type :counter}
(s/def ::definitions
(s/map-of keyword? map?))
:update-file-bytes-processed
{:name "rpc_update_file_bytes_processed_total"
:help "A total number of bytes processed by update-file."
:type :counter}
(defmethod ig/pre-init-spec ::metrics [_]
(s/keys :opt-un [::definitions]))
:rpc-mutation-timing
{:name "rpc_mutation_timing"
:help "RPC mutation method call timming."
:labels ["name"]
:type :histogram}
:rpc-query-timing
{:name "rpc_query_timing"
:help "RPC query method call timing."
:labels ["name"]
:type :histogram}
:websocket-active-connections
{:name "websocket_active_connections"
:help "Active websocket connections gauge"
:type :gauge}
:websocket-messages-total
{:name "websocket_message_total"
:help "Counter of processed messages."
:labels ["op"]
:type :counter}
:websocket-session-timing
{:name "websocket_session_timing"
:help "Websocket session timing (seconds)."
:type :summary}
:session-update-total
{:name "http_session_update_total"
:help "A counter of session update batch events."
:type :counter}
:tasks-timing
{:name "penpot_tasks_timing"
:help "Background tasks timing (milliseconds)."
:labels ["name"]
:type :summary}
:rlimit-queued-submissions
{:name "penpot_rlimit_queued_submissions"
:help "Current number of queued submissions on RLIMIT."
:labels ["name"]
:type :gauge}
:rlimit-used-permits
{:name "penpot_rlimit_used_permits"
:help "Current number of used permits on RLIMIT."
:labels ["name"]
:type :gauge}
:rlimit-acquires-total
{:name "penpot_rlimit_acquires_total"
:help "Total number of acquire operations on RLIMIT."
:labels ["name"]
:type :counter}
:executors-active-threads
{:name "penpot_executors_active_threads"
:help "Current number of threads available in the executor service."
:labels ["name"]
:type :gauge}
:executors-completed-tasks
{:name "penpot_executors_completed_tasks_total"
:help "Aproximate number of completed tasks by the executor."
:labels ["name"]
:type :counter}
:executors-running-threads
{:name "penpot_executors_running_threads"
:help "Current number of threads with state RUNNING."
:labels ["name"]
:type :gauge}
:executors-queued-submissions
{:name "penpot_executors_queued_submissions"
:help "Current number of queued submissions."
:labels ["name"]
:type :gauge}})
(defmethod ig/init-key ::metrics
[_ {:keys [definitions] :as cfg}]
[_ _]
(l/info :action "initialize metrics")
(let [registry (create-registry)
definitions (reduce-kv (fn [res k v]
@@ -54,7 +136,7 @@
(create)
(assoc res k)))
{}
definitions)]
default-metrics)]
{:handler (partial handler registry)
:definitions definitions
:registry registry}))
@@ -64,24 +146,45 @@
(s/def ::metrics
(s/keys :req-un [::registry ::handler]))
(defn- handler
[registry _ respond _]
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
writer (StringWriter.)]
(TextFormat/write004 writer samples)
(respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004}
:body (.toString writer)})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def default-empty-labels (into-array String []))
(def default-quantiles
[[0.5 0.01]
[0.90 0.01]
[0.99 0.001]])
(def default-histogram-buckets
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
(defn run!
[{:keys [definitions]} {:keys [id] :as params}]
(when-let [mobj (get definitions id)]
((::fn mobj) params)
true))
(defn create-registry
[]
(let [registry (CollectorRegistry.)]
(DefaultExports/register registry)
registry))
(defmacro with-measure
[& {:keys [expr cb]}]
`(let [start# (System/nanoTime)
tdown# ~cb]
(try
~expr
(finally
(tdown# (/ (- (System/nanoTime) start#) 1000000))))))
(defn- is-array?
[o]
(let [oc (class o)]
(and (.isArray ^Class oc)
(= (.getComponentType oc) String))))
(defn make-counter
[{:keys [name help registry reg labels] :as props}]
@@ -94,12 +197,9 @@
instance (.register instance registry)]
{::instance instance
::fn (fn [{:keys [by labels] :or {by 1}}]
(if labels
(.. ^Counter instance
(labels (into-array String labels))
(inc by))
(.inc ^Counter instance by)))}))
::fn (fn [{:keys [inc labels] :or {inc 1 labels default-empty-labels}}]
(let [instance (.labels instance (if (is-array? labels) labels (into-array String labels)))]
(.inc ^Counter$Child instance (double inc))))}))
(defn make-gauge
[{:keys [name help registry reg labels] :as props}]
@@ -110,48 +210,33 @@
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
{::instance instance
::fn (fn [{:keys [cmd by labels] :or {by 1}}]
(if labels
(let [labels (into-array String [labels])]
(case cmd
:inc (.. ^Gauge instance (labels labels) (inc by))
:dec (.. ^Gauge instance (labels labels) (dec by))))
(case cmd
:inc (.inc ^Gauge instance by)
:dec (.dec ^Gauge instance by))))}))
(def default-quantiles
[[0.75 0.02]
[0.99 0.001]])
::fn (fn [{:keys [inc dec labels val] :or {labels default-empty-labels}}]
(let [instance (.labels ^Gauge instance (if (is-array? labels) labels (into-array String labels)))]
(cond (number? inc) (.inc ^Gauge$Child instance (double inc))
(number? dec) (.dec ^Gauge$Child instance (double dec))
(number? val) (.set ^Gauge$Child instance (double val)))))}))
(defn make-summary
[{:keys [name help registry reg labels max-age quantiles buckets]
:or {max-age 3600 buckets 6 quantiles default-quantiles} :as props}]
:or {max-age 3600 buckets 12 quantiles default-quantiles} :as props}]
(let [registry (or registry reg)
instance (doto (Summary/build)
builder (doto (Summary/build)
(.name name)
(.help help))
_ (when (seq quantiles)
(.maxAgeSeconds ^Summary instance max-age)
(.ageBuckets ^Summary instance buckets))
(.maxAgeSeconds ^Summary$Builder builder ^long max-age)
(.ageBuckets ^Summary$Builder builder buckets))
_ (doseq [[q e] quantiles]
(.quantile ^Summary instance q e))
(.quantile ^Summary$Builder builder q e))
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
(.labelNames ^Summary$Builder builder (into-array String labels)))
instance (.register ^Summary$Builder builder registry)]
{::instance instance
::fn (fn [{:keys [val labels]}]
(if labels
(.. ^Summary instance
(labels (into-array String labels))
(observe val))
(.observe ^Summary instance val)))}))
(def default-histogram-buckets
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
::fn (fn [{:keys [val labels] :or {labels default-empty-labels}}]
(let [instance (.labels ^Summary instance (if (is-array? labels) labels (into-array String labels)))]
(.observe ^Summary$Child instance val)))}))
(defn make-histogram
[{:keys [name help registry reg labels buckets]
@@ -166,12 +251,9 @@
instance (.register instance registry)]
{::instance instance
::fn (fn [{:keys [val labels]}]
(if labels
(.. ^Histogram instance
(labels (into-array String labels))
(observe val))
(.observe ^Histogram instance val)))}))
::fn (fn [{:keys [val labels] :or {labels default-empty-labels}}]
(let [instance (.labels ^Histogram instance (if (is-array? labels) labels (into-array String labels)))]
(.observe ^Histogram$Child instance val)))}))
(defn create
[{:keys [type] :as props}]
@@ -180,118 +262,3 @@
:gauge (make-gauge props)
:summary (make-summary props)
:histogram (make-histogram props)))
(defn wrap-counter
([rootf mobj]
(let [mdata (meta rootf)
origf (::original mdata rootf)]
(with-meta
(fn
([a]
((::fn mobj) nil)
(origf a))
([a b]
((::fn mobj) nil)
(origf a b))
([a b c]
((::fn mobj) nil)
(origf a b c))
([a b c d]
((::fn mobj) nil)
(origf a b c d))
([a b c d & more]
((::fn mobj) nil)
(apply origf a b c d more)))
(assoc mdata ::original origf))))
([rootf mobj labels]
(let [mdata (meta rootf)
origf (::original mdata rootf)]
(with-meta
(fn
([a]
((::fn mobj) {:labels labels})
(origf a))
([a b]
((::fn mobj) {:labels labels})
(origf a b))
([a b & more]
((::fn mobj) {:labels labels})
(apply origf a b more)))
(assoc mdata ::original origf)))))
(defn wrap-summary
([rootf mobj]
(let [mdata (meta rootf)
origf (::original mdata rootf)]
(with-meta
(fn
([a]
(with-measure
:expr (origf a)
:cb #((::fn mobj) {:val %})))
([a b]
(with-measure
:expr (origf a b)
:cb #((::fn mobj) {:val %})))
([a b & more]
(with-measure
:expr (apply origf a b more)
:cb #((::fn mobj) {:val %}))))
(assoc mdata ::original origf))))
([rootf mobj labels]
(let [mdata (meta rootf)
origf (::original mdata rootf)]
(with-meta
(fn
([a]
(with-measure
:expr (origf a)
:cb #((::fn mobj) {:val % :labels labels})))
([a b]
(with-measure
:expr (origf a b)
:cb #((::fn mobj) {:val % :labels labels})))
([a b & more]
(with-measure
:expr (apply origf a b more)
:cb #((::fn mobj) {:val % :labels labels}))))
(assoc mdata ::original origf)))))
(defn instrument-vars!
[vars {:keys [wrap] :as props}]
(let [obj (create props)]
(cond
(instance? Counter (::instance obj))
(doseq [var vars]
(alter-var-root var (or wrap wrap-counter) obj))
(instance? Summary (::instance obj))
(doseq [var vars]
(alter-var-root var (or wrap wrap-summary) obj))
:else
(ex/raise :type :not-implemented))))
(defn instrument
[f {:keys [wrap] :as props}]
(let [obj (create props)]
(cond
(instance? Counter (::instance obj))
((or wrap wrap-counter) f obj)
(instance? Summary (::instance obj))
((or wrap wrap-summary) f obj)
(instance? Histogram (::instance obj))
((or wrap wrap-summary) f obj)
:else
(ex/raise :type :not-implemented))))
(defn instrument-jetty!
[^CollectorRegistry registry ^StatisticsHandler handler]
(doto (JettyStatisticsCollector. handler)
(.register registry))
nil)

View File

@@ -202,6 +202,30 @@
{:name "0064-mod-audit-log-table"
:fn (mg/resource "app/migrations/sql/0064-mod-audit-log-table.sql")}
{:name "0065-add-trivial-spelling-fixes"
:fn (mg/resource "app/migrations/sql/0065-add-trivial-spelling-fixes.sql")}
{:name "0066-add-frame-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0066-add-frame-thumbnail-table.sql")}
{:name "0067-add-team-invitation-table"
:fn (mg/resource "app/migrations/sql/0067-add-team-invitation-table.sql")}
{:name "0068-mod-storage-object-table"
:fn (mg/resource "app/migrations/sql/0068-mod-storage-object-table.sql")}
{:name "0069-add-file-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0069-add-file-thumbnail-table.sql")}
{:name "0070-del-frame-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0070-del-frame-thumbnail-table.sql")}
{:name "0071-add-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0071-add-file-object-thumbnail-table.sql")}
{:name "0072-mod-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0072-mod-file-object-thumbnail-table.sql")}
])

View File

@@ -22,7 +22,7 @@ CREATE TABLE storage_data (
CREATE INDEX storage_data__id__idx ON storage_data(id);
-- Table used for store inflight upload ids, for later recheck and
-- delete possible staled files that exists on the phisical storage
-- delete possible staled files that exists on the physical storage
-- but does not exists in the 'storage_object' table.
CREATE TABLE storage_pending (

View File

@@ -1,4 +1,4 @@
-- Fix problem with content-type inconherence
-- Fix problem with content-type incoherence
UPDATE storage_object so
SET metadata = jsonb_set(metadata, '{~:content-type}', to_jsonb(fmo.mtype))

View File

@@ -0,0 +1,2 @@
ALTER INDEX file__modified_at__has_media_trimed__idx RENAME TO file__modified_at__has_media_trimmed__idx;
ALTER INDEX media_bject__file_id__idx RENAME TO media_object__file_id__idx;

View File

@@ -0,0 +1,13 @@
CREATE TABLE file_frame_thumbnail (
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
frame_id uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
data text NULL,
PRIMARY KEY(file_id, frame_id)
);
ALTER TABLE file_frame_thumbnail
ALTER COLUMN data SET STORAGE external;

View File

@@ -0,0 +1,14 @@
CREATE TABLE team_invitation (
team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
email_to text NOT NULL,
role text NOT NULL,
valid_until timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY(team_id, email_to)
);
ALTER TABLE team_invitation
ALTER COLUMN email_to SET STORAGE external,
ALTER COLUMN role SET STORAGE external;

View File

@@ -0,0 +1,3 @@
CREATE INDEX storage_object__hash_backend_bucket__idx
ON storage_object ((metadata->>'~:hash'), (metadata->>'~:bucket'), backend)
WHERE deleted_at IS NULL;

View File

@@ -0,0 +1,14 @@
CREATE TABLE file_thumbnail (
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
revn bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz NULL,
data text NULL,
props jsonb NULL,
PRIMARY KEY(file_id, revn)
);
ALTER TABLE file_thumbnail
ALTER COLUMN data SET STORAGE external,
ALTER COLUMN props SET STORAGE external;

View File

@@ -0,0 +1 @@
DROP TABLE file_frame_thumbnail;

View File

@@ -0,0 +1,11 @@
CREATE TABLE file_object_thumbnail (
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
object_id uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
data text NULL,
PRIMARY KEY(file_id, object_id)
);
ALTER TABLE file_object_thumbnail
ALTER COLUMN data SET STORAGE external;

View File

@@ -0,0 +1,4 @@
TRUNCATE TABLE file_object_thumbnail;
ALTER TABLE file_object_thumbnail
ALTER COLUMN object_id TYPE text;

View File

@@ -1,5 +1,5 @@
--- This is a second migration but it should be applied when manual
--- migration intervention is alteady executed.
--- migration intervention is already executed.
ALTER TABLE file_media_object ALTER COLUMN media_id SET NOT NULL;
DROP TABLE file_media_thumbnail;

View File

@@ -7,18 +7,20 @@
(ns app.msgbus
"The msgbus abstraction implemented using redis as underlying backend."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.transit :as t]
[app.config :as cfg]
[app.util.blob :as blob]
[app.util.async :as aa]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p])
(:import
java.time.Duration
io.lettuce.core.RedisClient
io.lettuce.core.RedisURI
io.lettuce.core.api.StatefulConnection
@@ -29,7 +31,12 @@
io.lettuce.core.codec.StringCodec
io.lettuce.core.pubsub.RedisPubSubListener
io.lettuce.core.pubsub.StatefulRedisPubSubConnection
io.lettuce.core.pubsub.api.async.RedisPubSubAsyncCommands))
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
io.lettuce.core.resource.ClientResources
io.lettuce.core.resource.DefaultClientResources
java.time.Duration))
(set! *warn-on-reflection* true)
(def ^:private prefix (cfg/get :tenant))
@@ -37,266 +44,254 @@
[topic]
(str prefix "." topic))
(def xform-prefix (map prefix-topic))
(def xform-topics (map (fn [m] (update m :topics #(into #{} xform-prefix %)))))
(def xform-topic (map (fn [m] (update m :topic prefix-topic))))
(def ^:private xform-prefix-topic
(map (fn [obj] (update obj :topic prefix-topic))))
(s/def ::redis-uri ::us/string)
(s/def ::buffer-size ::us/integer)
(defmulti init-backend :backend)
(defmulti stop-backend :backend)
(defmulti init-pub-loop :backend)
(defmulti init-sub-loop :backend)
(defmethod ig/pre-init-spec ::msgbus [_]
(s/keys :opt-un [::buffer-size ::redis-uri]))
(declare ^:private redis-connect)
(declare ^:private redis-disconnect)
(declare ^:private start-io-loop)
(declare ^:private subscribe)
(declare ^:private purge)
(declare ^:private redis-pub)
(declare ^:private redis-sub)
(declare ^:private redis-unsub)
(defmethod ig/prep-key ::msgbus
[_ cfg]
(merge {:buffer-size 128} cfg))
(merge {:buffer-size 128
:timeout (dt/duration {:seconds 30})}
(d/without-nils cfg)))
(s/def ::timeout ::dt/duration)
(s/def ::redis-uri ::us/string)
(s/def ::buffer-size ::us/integer)
(defmethod ig/pre-init-spec ::msgbus [_]
(s/keys :req-un [::buffer-size ::redis-uri ::timeout ::wrk/executor]))
(defmethod ig/init-key ::msgbus
[_ {:keys [backend buffer-size] :as cfg}]
(l/debug :action "initialize msgbus"
:backend (name backend))
(let [cfg (init-backend cfg)
[_ {:keys [buffer-size redis-uri] :as cfg}]
(l/info :hint "initialize msgbus"
:buffer-size buffer-size
:redis-uri redis-uri)
(let [cmd-ch (a/chan buffer-size)
rcv-ch (a/chan (a/dropping-buffer buffer-size))
pub-ch (a/chan (a/dropping-buffer buffer-size) xform-prefix-topic)
state (agent {} :error-handler #(l/error :cause % :hint "unexpected error on agent" ::l/async false))
cfg (-> (redis-connect cfg)
(assoc ::cmd-ch cmd-ch)
(assoc ::rcv-ch rcv-ch)
(assoc ::pub-ch pub-ch)
(assoc ::state state))]
;; Channel used for receive publications from the application.
pub-ch (-> (a/dropping-buffer buffer-size)
(a/chan xform-topic))
;; Channel used for receive subscription requests.
sub-ch (a/chan 1 xform-topics)
cfg (-> cfg
(assoc ::pub-ch pub-ch)
(assoc ::sub-ch sub-ch))]
(init-pub-loop cfg)
(init-sub-loop cfg)
(start-io-loop cfg)
(with-meta
(fn run
([command] (run command nil))
([command params]
(a/go
(case command
:pub (a/>! pub-ch params)
:sub (a/>! sub-ch params)))))
(fn [& {:keys [cmd] :as params}]
(a/go
(case cmd
:pub (a/>! pub-ch params)
:sub (a/<! (subscribe cfg params))
:purge (a/<! (purge cfg params))
(l/error :hint "unexpeced error on msgbus command processing" :params params))))
cfg)))
(defmethod ig/halt-key! ::msgbus
[_ f]
(let [mdata (meta f)]
(stop-backend mdata)
(a/close! (::pub-ch mdata))
(a/close! (::sub-ch mdata))))
(redis-disconnect mdata)
(a/close! (::cmd-ch mdata))
(a/close! (::rcv-ch mdata))))
;; --- IN-MEMORY BACKEND IMPL
;; --- IMPL
(defmethod init-backend :memory [cfg] cfg)
(defmethod stop-backend :memory [_])
(defmethod init-pub-loop :memory [_])
(defn- redis-connect
[{:keys [redis-uri timeout] :as cfg}]
(let [codec (RedisCodec/of StringCodec/UTF8 ByteArrayCodec/INSTANCE)
(defmethod init-sub-loop :memory
[{:keys [::sub-ch ::pub-ch]}]
(a/go-loop [state {}]
(let [[val port] (a/alts! [pub-ch sub-ch])]
(cond
(and (= port sub-ch) (some? val))
(let [{:keys [topics chan]} val]
(recur (reduce #(update %1 %2 (fnil conj #{}) chan) state topics)))
resources (.. (DefaultClientResources/builder)
(ioThreadPoolSize 4)
(computationThreadPoolSize 4)
(build))
(and (= port pub-ch) (some? val))
(let [topic (:topic val)
message (:message val)
state (loop [state state
chans (get state topic)]
(if-let [c (first chans)]
(if (a/>! c message)
(recur state (rest chans))
(recur (update state topic disj c)
(rest chans)))
state))]
(recur state))
uri (RedisURI/create redis-uri)
rclient (RedisClient/create ^ClientResources resources ^RedisURI uri)
:else
(->> (vals state)
(mapcat identity)
(run! a/close!))))))
pconn (.connect ^RedisClient rclient ^RedisCodec codec)
sconn (.connectPubSub ^RedisClient rclient ^RedisCodec codec)]
;; Add a unique listener to connection
;; --- REDIS BACKEND IMPL
(declare impl-redis-open?)
(declare impl-redis-pub)
(declare impl-redis-sub)
(declare impl-redis-unsub)
(defmethod init-backend :redis
[{:keys [redis-uri] :as cfg}]
(let [codec (RedisCodec/of StringCodec/UTF8 ByteArrayCodec/INSTANCE)
uri (RedisURI/create redis-uri)
rclient (RedisClient/create ^RedisURI uri)
pub-conn (.connect ^RedisClient rclient ^RedisCodec codec)
sub-conn (.connectPubSub ^RedisClient rclient ^RedisCodec codec)]
(.setTimeout ^StatefulRedisConnection pub-conn ^Duration (dt/duration {:seconds 10}))
(.setTimeout ^StatefulRedisPubSubConnection sub-conn ^Duration (dt/duration {:seconds 10}))
(.setTimeout ^StatefulRedisConnection pconn ^Duration timeout)
(.setTimeout ^StatefulRedisPubSubConnection sconn ^Duration timeout)
(-> cfg
(assoc ::pub-conn pub-conn)
(assoc ::sub-conn sub-conn))))
(assoc ::resources resources)
(assoc ::pconn pconn)
(assoc ::sconn sconn))))
(defmethod stop-backend :redis
[{:keys [::pub-conn ::sub-conn] :as cfg}]
(.close ^StatefulRedisConnection pub-conn)
(.close ^StatefulRedisPubSubConnection sub-conn))
(defn- redis-disconnect
[{:keys [::pconn ::sconn ::resources] :as cfg}]
(.. ^StatefulConnection pconn close)
(.. ^StatefulConnection sconn close)
(.shutdown ^ClientResources resources))
(defmethod init-pub-loop :redis
[{:keys [::pub-conn ::pub-ch]}]
(let [rac (.async ^StatefulRedisConnection pub-conn)]
(a/go-loop []
(when-let [val (a/<! pub-ch)]
(let [result (a/<! (impl-redis-pub rac val))]
(when (and (impl-redis-open? pub-conn)
(ex/exception? result))
(l/error :cause result
:hint "unexpected error on publish message to redis")))
(recur)))))
(defn- conj-subscription
"A low level function that is responsible to create on-demand
subscriptions on redis. It reuses the same subscription if it is
already established. Intended to be executed in agent."
[nsubs cfg topic chan]
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
(when (= 1 (count nsubs))
(l/trace :hint "open subscription" :topic topic ::l/async false)
(redis-sub cfg topic))
nsubs))
(defmethod init-sub-loop :redis
[{:keys [::sub-conn ::sub-ch buffer-size]}]
(let [rcv-ch (a/chan (a/dropping-buffer buffer-size))
chans (agent {} :error-handler #(l/error :cause % :hint "unexpected error on agent"))
rac (.async ^StatefulRedisPubSubConnection sub-conn)]
(defn- disj-subscription
"A low level function responsible on removing subscriptions. The
subscription is trully removed from redis once no single local
subscription is look for it. Intended to be executed in agent."
[nsubs cfg topic chan]
(let [nsubs (disj nsubs chan)]
(when (empty? nsubs)
(l/trace :hint "close subscription" :topic topic ::l/async false)
(redis-unsub cfg topic))
nsubs))
;; Add a unique listener to connection
(.addListener sub-conn
(reify RedisPubSubListener
(message [it pattern topic message])
(message [it topic message]
;; There are no back pressure, so we use a slidding
;; buffer for cases when the pubsub broker sends
;; more messages that we can process.
(let [val {:topic topic :message (blob/decode message)}]
(when-not (a/offer! rcv-ch val)
(l/warn :msg "dropping message on subscription loop"))))
(psubscribed [it pattern count])
(punsubscribed [it pattern count])
(subscribed [it topic count])
(unsubscribed [it topic count])))
(defn- subscribe-to-topics
"Function responsible to attach local subscription to the
state. Intended to be used in agent."
[state cfg topics chan done-ch]
(l/trace :hint "subscribe-to-topics" :topics topics ::l/async false)
(aa/with-closing done-ch
(let [state (update state :chans assoc chan topics)]
(reduce (fn [state topic]
(update-in state [:topics topic] conj-subscription cfg topic chan))
state
topics))))
(letfn [(subscribe-to-single-topic [nsubs topic chan]
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
(when (= 1 (count nsubs))
(let [result (a/<!! (impl-redis-sub rac topic))]
(l/trace :action "open subscription"
:topic topic)
(when (ex/exception? result)
(l/error :cause result
:hint "unexpected exception on subscribing"
:topic topic))))
nsubs))
(defn- unsubscribe-single-channel
"Auxiliar function responsible on removing a single local
subscription from the state."
[state cfg chan]
(let [topics (get-in state [:chans chan])
state (update state :chans dissoc chan)]
(reduce (fn [state topic]
(update-in state [:topics topic] disj-subscription cfg topic chan))
state
topics)))
(subscribe-to-topics [state topics chan]
(let [state (update state :chans assoc chan topics)]
(reduce (fn [state topic]
(update-in state [:topics topic] subscribe-to-single-topic topic chan))
state
topics)))
(defn- unsubscribe-channels
"Function responsible from detach from state a seq of channels,
useful when client disconnects or in-bulk unsubscribe
operations. Intended to be executed in agent."
[state cfg channels done-ch]
(l/trace :hint "unsubscribe-channels" :chans (count channels) ::l/async false)
(aa/with-closing done-ch
(reduce #(unsubscribe-single-channel %1 cfg %2) state channels)))
(unsubscribe-from-single-topic [nsubs topic chan]
(let [nsubs (disj nsubs chan)]
(when (empty? nsubs)
(let [result (a/<!! (impl-redis-unsub rac topic))]
(l/trace :action "close subscription"
:topic topic)
(when (and (impl-redis-open? sub-conn)
(ex/exception? result))
(l/error :cause result
:hint "unexpected exception on unsubscribing"
:topic topic))))
nsubs))
(defn- subscribe
[{:keys [::state executor] :as cfg} {:keys [topic topics chan]}]
(let [done-ch (a/chan)
topics (into [] (map prefix-topic) (if topic [topic] topics))]
(l/trace :hint "subscribe" :topics topics)
(send-via executor state subscribe-to-topics cfg topics chan done-ch)
done-ch))
(unsubscribe-channels [state pending]
(reduce (fn [state ch]
(let [topics (get-in state [:chans ch])
state (update state :chans dissoc ch)]
(reduce (fn [state topic]
(update-in state [:topics topic] unsubscribe-from-single-topic topic ch))
state
topics)))
state
pending))]
(defn- purge
[{:keys [::state executor] :as cfg} {:keys [chans]}]
(l/trace :hint "purge" :chans (count chans))
(let [done-ch (a/chan)]
(send-via executor state unsubscribe-channels cfg chans done-ch)
done-ch))
;; Asynchronous subscription loop;
(a/go-loop []
(if-let [{:keys [topics chan]} (a/<! sub-ch)]
(do
(send-off chans subscribe-to-topics topics chan)
(recur))
(a/close! rcv-ch)))
(defn- create-listener
[rcv-ch]
(reify RedisPubSubListener
(message [_ _pattern _topic _message])
(message [_ topic message]
;; There are no back pressure, so we use a slidding
;; buffer for cases when the pubsub broker sends
;; more messages that we can process.
(let [val {:topic topic :message (t/decode message)}]
(when-not (a/offer! rcv-ch val)
(l/warn :msg "dropping message on subscription loop"))))
(psubscribed [_ _pattern _count])
(punsubscribed [_ _pattern _count])
(subscribed [_ _topic _count])
(unsubscribed [_ _topic _count])))
;; Asyncrhonous message processing loop;x
(a/go-loop []
(if-let [{:keys [topic message]} (a/<! rcv-ch)]
;; This means we receive data from redis and we need to
;; forward it to the underlying subscriptions.
(let [pending (loop [chans (seq (get-in @chans [:topics topic]))
pending #{}]
(if-let [ch (first chans)]
(if (a/>! ch message)
(recur (rest chans) pending)
(recur (rest chans) (conj pending ch)))
pending))]
(some->> (seq pending)
(send-off chans unsubscribe-channels))
(defn start-io-loop
[{:keys [::sconn ::rcv-ch ::pub-ch ::state executor] :as cfg}]
(recur))
;; Add a single listener to the pubsub connection
(.addListener ^StatefulRedisPubSubConnection sconn
^RedisPubSubListener (create-listener rcv-ch))
;; Stop condition; close all underlying subscriptions and
;; exit. The close operation is performed asynchronously.
(send-off chans (fn [state]
(->> (vals state)
(mapcat identity)
(filter some?)
(run! a/close!)))))))))
(letfn [(send-to-topic [topic message]
(a/go-loop [chans (seq (get-in @state [:topics topic]))
closed #{}]
(if-let [ch (first chans)]
(if (a/>! ch message)
(recur (rest chans) closed)
(recur (rest chans) (conj closed ch)))
(seq closed))))
(process-incoming [{:keys [topic message]}]
(a/go
(when-let [closed (a/<! (send-to-topic topic message))]
(send-via executor state unsubscribe-channels cfg closed nil))))
]
(defn- impl-redis-open?
[^StatefulConnection conn]
(.isOpen conn))
(a/go-loop []
(let [[val port] (a/alts! [pub-ch rcv-ch])]
(cond
(nil? val)
(do
(l/trace :hint "stoping io-loop, nil received")
(send-via executor state (fn [state]
(->> (vals state)
(mapcat identity)
(filter some?)
(run! a/close!))
nil)))
(defn- impl-redis-pub
[^RedisAsyncCommands rac {:keys [topic message]}]
(let [message (blob/encode message)
res (a/chan 1)]
(-> (.publish rac ^String topic ^bytes message)
(p/finally (fn [_ e]
(when e (a/>!! res e))
(= port rcv-ch)
(do
(a/<! (process-incoming val))
(recur))
(= port pub-ch)
(let [result (a/<! (redis-pub cfg val))]
(when (ex/exception? result)
(l/error :hint "unexpected error on publishing" :message val
:cause result))
(recur)))))))
(defn- redis-pub
"Publish a message to the redis server. Asynchronous operation,
intended to be used in core.async go blocks."
[{:keys [::pconn] :as cfg} {:keys [topic message]}]
(let [message (t/encode message)
res (a/chan 1)
pcomm (.async ^StatefulRedisConnection pconn)]
(-> (.publish ^RedisAsyncCommands pcomm ^String topic ^bytes message)
(p/finally (fn [_ cause]
(when (and cause (.isOpen ^StatefulConnection pconn))
(a/offer! res cause))
(a/close! res))))
res))
(defn impl-redis-sub
[^RedisPubSubAsyncCommands rac topic]
(let [res (a/chan 1)]
(-> (.subscribe rac (into-array String [topic]))
(p/finally (fn [_ e]
(when e (a/>!! res e))
(a/close! res))))
res))
(defn redis-sub
"Create redis subscription. Blocking operation, intended to be used
inside an agent."
[{:keys [::sconn] :as cfg} topic]
(let [topic (into-array String [topic])
scomm (.sync ^StatefulRedisPubSubConnection sconn)]
(.subscribe ^RedisPubSubCommands scomm topic)))
(defn impl-redis-unsub
[rac topic]
(let [res (a/chan 1)]
(-> (.unsubscribe rac (into-array String [topic]))
(p/finally (fn [_ e]
(when e (a/>!! res e))
(a/close! res))))
res))
(defn redis-unsub
"Removes redis subscription. Blocking operation, intended to be used
inside an agent."
[{:keys [::sconn] :as cfg} topic]
(let [topic (into-array String [topic])
scomm (.sync ^StatefulRedisPubSubConnection sconn)]
(.unsubscribe ^RedisPubSubCommands scomm topic)))

View File

@@ -1,281 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.notifications
"A websocket based notifications mechanism."
(:require
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.transit :as t]
[app.db :as db]
[app.metrics :as mtx]
[app.util.async :as aa]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[ring.adapter.jetty9 :as jetty]
[ring.middleware.cookies :refer [wrap-cookies]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.params :refer [wrap-params]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Http Handler
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare retrieve-file)
(declare websocket)
(declare handler)
(s/def ::session map?)
(s/def ::msgbus fn?)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::msgbus ::db/pool ::session ::mtx/metrics ::wrk/executor]))
(defmethod ig/init-key ::handler
[_ {:keys [session metrics] :as cfg}]
(let [wrap-session (:middleware session)
mtx-active-connections
(mtx/create
{:name "websocket_active_connections"
:registry (:registry metrics)
:type :gauge
:help "Active websocket connections."})
mtx-messages
(mtx/create
{:name "websocket_message_total"
:registry (:registry metrics)
:labels ["op"]
:type :counter
:help "Counter of processed messages."})
mtx-sessions
(mtx/create
{:name "websocket_session_timing"
:registry (:registry metrics)
:quantiles []
:help "Websocket session timing (seconds)."
:type :summary})
cfg (assoc cfg
:mtx-active-connections mtx-active-connections
:mtx-messages mtx-messages
:mtx-sessions mtx-sessions
)]
(-> #(handler cfg %)
(wrap-session)
(wrap-keyword-params)
(wrap-cookies)
(wrap-params))))
(s/def ::file-id ::us/uuid)
(s/def ::session-id ::us/uuid)
(s/def ::websocket-handler-params
(s/keys :req-un [::file-id ::session-id]))
(defn- handler
[{:keys [pool] :as cfg} {:keys [profile-id params] :as req}]
(let [params (us/conform ::websocket-handler-params params)
file (retrieve-file pool (:file-id params))
cfg (merge cfg params
{:profile-id profile-id
:team-id (:team-id file)})]
(cond
(not profile-id)
{:error {:code 403 :message "Authentication required"}}
(not file)
{:error {:code 404 :message "File does not exists"}}
:else
(websocket cfg))))
(def ^:private
sql:retrieve-file
"select f.id as id,
p.team_id as team_id
from file as f
join project as p on (p.id = f.project_id)
where f.id = ?")
(defn- retrieve-file
[conn id]
(db/exec-one! conn [sql:retrieve-file id]))
;; --- WEBSOCKET INIT
(declare handle-connect)
(defn- ws-send
[conn data]
(try
(when (jetty/connected? conn)
(jetty/send! conn data)
true)
(catch java.lang.NullPointerException _e
false)))
(defn websocket
[{:keys [file-id team-id msgbus executor] :as cfg}]
(let [rcv-ch (a/chan 32)
out-ch (a/chan 32)
mtx-aconn (:mtx-active-connections cfg)
mtx-messages (:mtx-messages cfg)
mtx-sessions (:mtx-sessions cfg)
created-at (dt/now)
ws-send (mtx/wrap-counter ws-send mtx-messages ["send"])]
(letfn [(on-connect [conn]
((::mtx/fn mtx-aconn) {:cmd :inc :by 1})
;; A subscription channel should use a lossy buffer
;; because we can't penalize normal clients when one
;; slow client is connected to the room.
(let [sub-ch (a/chan (a/dropping-buffer 128))
cfg (assoc cfg
:conn conn
:rcv-ch rcv-ch
:out-ch out-ch
:sub-ch sub-ch)]
(l/trace :event "connect" :session (:session-id cfg))
;; Forward all messages from out-ch to the websocket
;; connection
(a/go-loop []
(let [val (a/<! out-ch)]
(when (some? val)
(when (a/<! (aa/thread-call executor #(ws-send conn (t/encode-str val))))
(recur)))))
(a/go
;; Subscribe to corresponding topics
(a/<! (msgbus :sub {:topics [file-id team-id] :chan sub-ch}))
(a/<! (handle-connect cfg))
;; when connection is closed
((::mtx/fn mtx-aconn) {:cmd :dec :by 1})
((::mtx/fn mtx-sessions) {:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)})
;; close subscription
(a/close! sub-ch))))
(on-error [_conn _e]
(l/trace :event "error" :session (:session-id cfg))
(a/close! out-ch)
(a/close! rcv-ch))
(on-close [_conn _status _reason]
(l/trace :event "close" :session (:session-id cfg))
(a/close! out-ch)
(a/close! rcv-ch))
(on-message [_ws message]
(let [message (t/decode-str message)]
(when-not (a/offer! rcv-ch message)
(l/warn :msg "drop messages"))))]
{:on-connect on-connect
:on-error on-error
:on-close on-close
:on-text (mtx/wrap-counter on-message mtx-messages ["recv"])
:on-bytes (constantly nil)})))
;; --- CONNECTION INIT
(declare send-presence)
(declare handle-message)
(declare start-loop!)
(defn- handle-connect
[cfg]
(a/go
(a/<! (handle-message cfg {:type :connect}))
(a/<! (start-loop! cfg))
(a/<! (handle-message cfg {:type :disconnect}))))
(defn- start-loop!
[{:keys [rcv-ch out-ch sub-ch session-id] :as cfg}]
(a/go-loop []
(let [timeout (a/timeout 30000)
[val port] (a/alts! [rcv-ch sub-ch timeout])]
(cond
;; Process message coming from connected client
(and (= port rcv-ch) (some? val))
(do
(a/<! (handle-message cfg val))
(recur))
;; Process message coming from pubsub.
(and (= port sub-ch) (some? val))
(do
(when-not (= (:session-id val) session-id)
;; If we receive a connect message of other user, we need
;; to send an update presence to all participants.
(when (= :connect (:type val))
(a/<! (send-presence cfg :presence)))
;; Then, just forward the message
(a/>! out-ch val))
(recur))
;; When timeout channel is signaled, we need to send a ping
;; message to the output channel. TODO: we need to make this
;; more smart.
(= port timeout)
(do
(a/>! out-ch {:type :ping})
(recur))))))
(defn send-presence
([cfg] (send-presence cfg :presence))
([{:keys [msgbus session-id profile-id file-id]} type]
(a/go
(a/<! (msgbus :pub {:topic file-id
:message {:type type
:session-id session-id
:profile-id profile-id}})))))
;; --- INCOMING MSG PROCESSING
(defmulti handle-message
(fn [_ message] (:type message)))
(defmethod handle-message :connect
[cfg _]
(send-presence cfg :connect))
(defmethod handle-message :disconnect
[cfg _]
(send-presence cfg :disconnect))
(defmethod handle-message :keepalive
[_ _]
(a/go :nothing))
(defmethod handle-message :pointer-update
[{:keys [profile-id file-id session-id msgbus] :as cfg} message]
(let [message (assoc message
:profile-id profile-id
:session-id session-id)]
(msgbus :pub {:topic file-id
:message message})))
(defmethod handle-message :default
[_ws message]
(a/go
(l/log :level :warn
:msg "received unexpected message"
:message message)))

View File

@@ -1,45 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.rlimits
"Resource usage limits (in other words: semaphores)."
(:require
[app.common.spec :as us]
[clojure.spec.alpha :as s]
[integrant.core :as ig])
(:import
java.util.concurrent.Semaphore))
(s/def ::rlimit #(instance? Semaphore %))
(s/def ::rlimits (s/map-of ::us/keyword ::rlimit))
(derive ::password ::instance)
(derive ::image ::instance)
(derive ::font ::instance)
(defmethod ig/pre-init-spec ::instance [_]
(s/spec int?))
(defmethod ig/init-key ::instance
[_ permits]
(Semaphore. (int permits)))
(defn acquire!
[sem]
(.acquire ^Semaphore sem))
(defn release!
[sem]
(.release ^Semaphore sem))
(defmacro execute
[rlinst & body]
`(try
(acquire! ~rlinst)
~@body
(finally
(release! ~rlinst))))

View File

@@ -13,140 +13,177 @@
[app.db :as db]
[app.loggers.audit :as audit]
[app.metrics :as mtx]
[app.rlimits :as rlm]
[app.util.retry :as retry]
[app.rpc.retry :as retry]
[app.rpc.rlimit :as rlimit]
[app.util.async :as async]
[app.util.services :as sv]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]))
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.response :as yrs]))
(defn- default-handler
[_]
(ex/raise :type :not-found))
(p/rejected (ex/error :type :not-found)))
(defn- run-hook
[hook-fn response]
(ex/ignoring (hook-fn))
(defn- handle-response-transformation
[response request mdata]
(if-let [transform-fn (:transform-response mdata)]
(p/do (transform-fn request response))
(p/resolved response)))
(defn- handle-before-comple-hook
[response mdata]
(when-let [hook-fn (:before-complete mdata)]
(ex/ignoring (hook-fn)))
response)
(defn- rpc-query-handler
[methods {:keys [profile-id] :as request}]
(let [type (keyword (get-in request [:path-params :type]))
"Ring handler that dispatches query requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id params] :as request} respond raise]
(letfn [(handle-response [result]
(let [mdata (meta result)]
(-> (yrs/response 200 result)
(handle-response-transformation request mdata))))]
data (merge (:params request)
(:body-params request)
(:uploads request)
{::request request})
(let [type (keyword (:type params))
data (into {::request request} params)
data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
method (get methods type default-handler)]
data (if profile-id
(assoc data :profile-id profile-id)
(dissoc data :profile-id))
result ((get methods type default-handler) data)
mdata (meta result)]
(cond->> {:status 200 :body result}
(fn? (:transform-response mdata))
((:transform-response mdata) request))))
(-> (method data)
(p/then handle-response)
(p/then respond)
(p/catch (fn [cause]
(let [context {:profile-id profile-id}]
(raise (ex/wrap-with-context cause context)))))))))
(defn- rpc-mutation-handler
[methods {:keys [profile-id] :as request}]
(let [type (keyword (get-in request [:path-params :type]))
data (merge (:params request)
(:body-params request)
(:uploads request)
{::request request})
"Ring handler that dispatches mutation requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id params] :as request} respond raise]
(letfn [(handle-response [result]
(let [mdata (meta result)]
(p/-> (yrs/response 200 result)
(handle-response-transformation request mdata)
(handle-before-comple-hook mdata))))]
data (if profile-id
(assoc data :profile-id profile-id)
(dissoc data :profile-id))
(let [type (keyword (:type params))
data (into {::request request} params)
data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
result ((get methods type default-handler) data)
mdata (meta result)]
(cond->> {:status 200 :body result}
(fn? (:transform-response mdata))
((:transform-response mdata) request)
method (get methods type default-handler)]
(-> (method data)
(p/then handle-response)
(p/then respond)
(p/catch (fn [cause]
(let [context {:profile-id profile-id}]
(raise (ex/wrap-with-context cause context)))))))))
(fn? (:before-complete mdata))
(run-hook (:before-complete mdata)))))
(defn- wrap-metrics
"Wrap service method with metrics measurement."
[{:keys [metrics ::metrics-id]} f mdata]
(let [labels (into-array String [(::sv/name mdata)])]
(fn [cfg params]
(let [start (System/nanoTime)]
(p/finally
(f cfg params)
(fn [_ _]
(mtx/run! metrics
{:id metrics-id
:val (/ (- (System/nanoTime) start) 1000000)
:labels labels})))))))
(defn- wrap-with-metrics
[cfg f mdata]
(mtx/wrap-summary f (::mobj cfg) [(::sv/name mdata)]))
(defn- wrap-dispatch
"Wraps service method into async flow, with the ability to dispatching
it to a preconfigured executor service."
[{:keys [executors] :as cfg} f mdata]
(let [dname (::async/dispatch mdata :default)]
(if (= :none dname)
(with-meta
(fn [cfg params]
(p/do (f cfg params)))
mdata)
;; Wrap the rpc handler with a semaphore if it is specified in the
;; metadata asocciated with the handler.
(defn- wrap-with-rlimits
[cfg f mdata]
(if-let [key (:rlimit mdata)]
(let [rlinst (get-in cfg [:rlimits key])]
(when-not rlinst
(ex/raise :type :internal
:code :rlimit-not-configured
:hint (str/fmt "%s rlimit not configured" key)))
(l/trace :action "add rlimit"
:handler (::sv/name mdata))
(fn [cfg params]
(rlm/execute rlinst (f cfg params))))
(let [executor (get executors dname)]
(when-not executor
(ex/raise :type :internal
:code :executor-not-configured
:hint (format "executor %s not configured" dname)))
(with-meta
(fn [cfg params]
(-> (px/submit! executor #(f cfg params))
(p/bind p/wrap)))
mdata)))))
(defn- wrap-audit
[{:keys [audit] :as cfg} f mdata]
(if audit
(with-meta
(fn [cfg {:keys [::request] :as params}]
(p/finally (f cfg params)
(fn [result _]
(when result
(let [resultm (meta result)
profile-id (or (:profile-id params)
(:profile-id result)
(::audit/profile-id resultm))
props (d/merge params (::audit/props resultm))]
(audit :cmd :submit
:type (or (::audit/type resultm)
(::type cfg))
:name (or (::audit/name resultm)
(::sv/name mdata))
:profile-id profile-id
:ip-addr (some-> request audit/parse-client-ip)
:props (dissoc props ::request)))))))
mdata)
f))
(defn- wrap-impl
[{:keys [audit] :as cfg} f mdata]
(let [f (wrap-with-rlimits cfg f mdata)
f (retry/wrap-retry cfg f mdata)
f (wrap-with-metrics cfg f mdata)
spec (or (::sv/spec mdata) (s/spec any?))
auth? (:auth mdata true)]
(defn- wrap
[cfg f mdata]
(let [f (as-> f $
(wrap-dispatch cfg $ mdata)
(rlimit/wrap-rlimit cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
(wrap-audit cfg $ mdata)
(wrap-metrics cfg $ mdata)
)
spec (or (::sv/spec mdata) (s/spec any?))
auth? (:auth mdata true)]
(l/trace :action "register" :name (::sv/name mdata))
(with-meta
(fn [params]
(fn [{:keys [::request] :as params}]
;; Raise authentication error when rpc method requires auth but
;; no profile-id is found in the request.
(when (and auth? (not (uuid? (:profile-id params))))
(ex/raise :type :authentication
:code :authentication-required
:hint "authentication required for this endpoint"))
(p/do!
(if (and auth? (not (uuid? (:profile-id params))))
(ex/raise :type :authentication
:code :authentication-required
:hint "authentication required for this endpoint")
(let [params (us/conform spec (dissoc params ::request))]
(f cfg (assoc params ::request request))))))
(let [params' (dissoc params ::request)
params' (us/conform spec params')
result (f cfg params')]
;; When audit log is enabled (default false).
(when (fn? audit)
(let [resultm (meta result)
request (::request params)
profile-id (or (:profile-id params')
(:profile-id result)
(::audit/profile-id resultm))
props (d/merge params' (::audit/props resultm))]
(audit :cmd :submit
:type (or (::audit/type resultm)
(::type cfg))
:name (or (::audit/name resultm)
(::sv/name mdata))
:profile-id profile-id
:ip-addr (audit/parse-client-ip request)
:props props)))
result))
mdata)))
(defn- process-method
[cfg vfn]
(let [mdata (meta vfn)]
[(keyword (::sv/name mdata))
(wrap-impl cfg (deref vfn) mdata)]))
(wrap cfg (deref vfn) mdata)]))
(defn- resolve-query-methods
[cfg]
(let [mobj (mtx/create
{:name "rpc_query_timing"
:labels ["name"]
:registry (get-in cfg [:metrics :registry])
:type :histogram
:help "Timing of query services."})
cfg (assoc cfg ::mobj mobj ::type "query")]
(let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)]
(->> (sv/scan-ns 'app.rpc.queries.projects
'app.rpc.queries.files
'app.rpc.queries.teams
@@ -159,13 +196,7 @@
(defn- resolve-mutation-methods
[cfg]
(let [mobj (mtx/create
{:name "rpc_mutation_timing"
:labels ["name"]
:registry (get-in cfg [:metrics :registry])
:type :histogram
:help "Timing of mutation services."})
cfg (assoc cfg ::mobj mobj ::type "mutation")]
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
(->> (sv/scan-ns 'app.rpc.mutations.demo
'app.rpc.mutations.media
'app.rpc.mutations.profile
@@ -185,15 +216,16 @@
(s/def ::session map?)
(s/def ::tokens fn?)
(s/def ::audit (s/nilable fn?))
(s/def ::executors (s/map-of keyword? ::wrk/executor))
(defmethod ig/pre-init-spec ::rpc [_]
(s/keys :req-un [::storage ::session ::tokens ::audit
::mtx/metrics ::rlm/rlimits ::db/pool]))
::executors ::mtx/metrics ::db/pool]))
(defmethod ig/init-key ::rpc
[_ cfg]
(let [mq (resolve-query-methods cfg)
mm (resolve-mutation-methods cfg)]
{:methods {:query mq :mutation mm}
:query-handler #(rpc-query-handler mq %)
:mutation-handler #(rpc-mutation-handler mm %)}))
:query-handler (partial rpc-query-handler mq)
:mutation-handler (partial rpc-mutation-handler mm)}))

View File

@@ -0,0 +1,16 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.rpc.helpers
"General purpose RPC helpers."
(:require [app.common.data.macros :as dm]))
(defn http-cache
[{:keys [max-age]}]
(fn [_ response]
(let [exp (if (integer? max-age) max-age (inst-ms max-age))
val (dm/fmt "max-age=%" (int (/ exp 1000.0)))]
(update response :headers assoc "cache-control" val))))

View File

@@ -7,14 +7,13 @@
(ns app.rpc.mutations.comments
(:require
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.db :as db]
[app.rpc.queries.comments :as comments]
[app.rpc.queries.files :as files]
[app.rpc.retry :as retry]
[app.util.blob :as blob]
#_:clj-kondo/ignore
[app.util.retry :as retry]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -28,15 +27,14 @@
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::position ::us/point)
(s/def ::position ::gpt/point)
(s/def ::content ::us/string)
(s/def ::create-comment-thread
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]))
(sv/defmethod ::create-comment-thread
{::retry/enabled true
::retry/max-retries 3
{::retry/max-retries 3
::retry/matches retry/conflict-db-insert?}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
@@ -176,7 +174,7 @@
:content content})]
;; NOTE: this is done in SQL instead of using db/update!
;; helper bacause currently the helper does not allow pass raw
;; helper because currently the helper does not allow pass raw
;; function call parameters to the underlying prepared
;; statement; in a future when we fix/improve it, this can be
;; changed to use the helper.

View File

@@ -36,7 +36,8 @@
:is-active true
:deleted-at (dt/in-future cf/deletion-delay)
:password password
:props {:onboarding-viewed true}}]
:props {}
}]
(when-not (contains? cf/flags :demo-users)
(ex/raise :type :validation

View File

@@ -11,21 +11,26 @@
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.metrics :as mtx]
[app.rpc.permissions :as perms]
[app.rpc.queries.files :as files]
[app.rpc.queries.projects :as proj]
[app.rpc.rlimit :as rlimit]
[app.storage.impl :as simpl]
[app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
[clojure.spec.alpha :as s]
[promesa.core :as p]))
(declare create-file)
;; --- Helpers & Specs
(s/def ::frame-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::profile-id ::us/uuid)
@@ -53,8 +58,9 @@
(db/insert! conn :file-profile-rel))))
(defn create-file
[conn {:keys [id name project-id is-shared data deleted-at]
[conn {:keys [id name project-id is-shared data deleted-at revn]
:or {is-shared false
revn 0
deleted-at nil}
:as params}]
(let [id (or id (:id data) (uuid/next))
@@ -63,6 +69,7 @@
{:id id
:project-id project-id
:name name
:revn revn
:is-shared is-shared
:data (blob/encode data)
:deleted-at deleted-at})]
@@ -122,7 +129,6 @@
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)
(mark-file-deleted conn params)))
(defn mark-file-deleted
@@ -181,7 +187,7 @@
:library-file-id library-id}))
;; --- Mutation: Update syncrhonization status of a link
;; --- Mutation: Update synchronization status of a link
(declare update-sync)
@@ -269,6 +275,7 @@
(contains? o :changes-with-metadata)))))
(sv/defmethod ::update-file
{::rlimit/permits (cf/get :rlimit-file-update)}
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(db/xact-lock! conn id)
@@ -280,16 +287,19 @@
(defn- take-snapshot?
"Defines the rule when file `data` snapshot should be saved."
[{:keys [revn modified-at] :as file}]
;; The snapshot will be saved every 20 changes or if the last
;; modification is older than 3 hour.
(or (zero? (mod revn 20))
(> (inst-ms (dt/diff modified-at (dt/now)))
(inst-ms (dt/duration {:hours 3})))))
(let [freq (or (cf/get :file-change-snapshot-every) 20)
timeout (or (cf/get :file-change-snapshot-timeout)
(dt/duration {:hours 1}))]
(or (= 1 freq)
(zero? (mod revn freq))
(> (inst-ms (dt/diff modified-at (dt/now)))
(inst-ms timeout)))))
(defn- delete-from-storage
[{:keys [storage] :as cfg} file]
(when-let [backend (simpl/resolve-backend storage (:data-backend file))]
(simpl/del-object backend file)))
(p/do
(when-let [backend (simpl/resolve-backend storage (:data-backend file))]
(simpl/del-object backend file))))
(defn- update-file
[{:keys [conn metrics] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}]
@@ -302,22 +312,21 @@
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
(let [mtx1 (get-in metrics [:definitions :update-file-changes])
mtx2 (get-in metrics [:definitions :update-file-bytes-processed])
changes (if changes-with-metadata
(let [changes (if changes-with-metadata
(mapcat :changes changes-with-metadata)
changes)
changes (vec changes)
;; Trace the number of changes processed
_ ((::mtx/fn mtx1) {:by (count changes)})
_ (mtx/run! metrics {:id :update-file-changes :inc (count changes)})
ts (dt/now)
file (-> (files/retrieve-data cfg file)
file (-> file
(update :revn inc)
(update :data (fn [data]
;; Trace the length of bytes of processed data
((::mtx/fn mtx2) {:by (alength data)})
(mtx/run! metrics {:id :update-file-bytes-processed :inc (alength data)})
(-> data
(blob/decode)
(assoc :id (:id file))
@@ -347,7 +356,7 @@
;; We need to delete the data from external storage backend
(when-not (nil? (:data-backend file))
(delete-from-storage cfg file))
@(delete-from-storage cfg file))
(db/update! conn :project
{:modified-at ts}
@@ -379,38 +388,41 @@
(assoc :changes []))))))))
(defn- send-notifications
[{:keys [msgbus conn] :as cfg} {:keys [file changes session-id] :as params}]
(let [lchanges (filter library-change? changes)]
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}]
(let [lchanges (filter library-change? changes)
msgbus-fn (:msgbus cfg)]
;; Asynchronously publish message to the msgbus
(msgbus :pub {:topic (:id file)
:message
{:type :file-change
:profile-id (:profile-id params)
:file-id (:id file)
:session-id (:session-id params)
:revn (:revn file)
:changes changes}})
(msgbus-fn :cmd :pub
:topic (:id file)
:message {:type :file-change
:profile-id (:profile-id params)
:file-id (:id file)
:session-id (:session-id params)
:revn (:revn file)
:changes changes})
(when (and (:is-shared file) (seq lchanges))
(let [team-id (retrieve-team-id conn (:project-id file))]
;; Asynchronously publish message to the msgbus
(msgbus :pub {:topic team-id
:message
{:type :library-change
:profile-id (:profile-id params)
:file-id (:id file)
:session-id session-id
:revn (:revn file)
:modified-at (dt/now)
:changes lchanges}})))))
(msgbus-fn :cmd :pub
:topic team-id
:message {:type :library-change
:profile-id (:profile-id params)
:file-id (:id file)
:session-id session-id
:revn (:revn file)
:modified-at (dt/now)
:changes lchanges})))))
(defn- retrieve-team-id
[conn project-id]
(:team-id (db/get-by-id conn :project project-id {:columns [:team-id]})))
;; TEMPORARY FILE CREATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TEMPORARY FILES (behaves differently)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::create-temp-file ::create-file)
@@ -420,6 +432,23 @@
(proj/check-edition-permissions! conn profile-id project-id)
(create-file conn (assoc params :deleted-at (dt/in-future {:days 1})))))
(s/def ::update-temp-file
(s/keys :req-un [::changes ::revn ::session-id ::id]))
(sv/defmethod ::update-temp-file
[{:keys [pool] :as cfg} {:keys [profile-id session-id id revn changes] :as params}]
(db/with-atomic [conn pool]
(db/insert! conn :file-change
{:id (uuid/next)
:session-id session-id
:profile-id profile-id
:created-at (dt/now)
:file-id id
:revn revn
:data nil
:changes (blob/encode changes)})
nil))
(s/def ::persist-temp-file
(s/keys :req-un [::id ::profile-id]))
@@ -427,6 +456,68 @@
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)
(db/update! conn :file
{:deleted-at nil}
{:id id})))
(let [file (db/get-by-id conn :file id)
revs (db/query conn :file-change
{:file-id id}
{:order-by [[:revn :asc]]})
revn (count revs)]
(when (nil? (:deleted-at file))
(ex/raise :type :validation
:code :cant-persist-already-persisted-file))
(loop [revs (seq revs)
data (blob/decode (:data file))]
(if-let [rev (first revs)]
(recur (rest revs)
(->> rev :changes blob/decode (cp/process-changes data)))
(db/update! conn :file
{:deleted-at nil
:revn revn
:data (blob/encode data)}
{:id id})))
nil)))
;; --- Mutation: upsert object thumbnail
(def sql:upsert-object-thumbnail
"insert into file_object_thumbnail(file_id, object_id, data)
values (?, ?, ?)
on conflict(file_id, object_id) do
update set data = ?;")
(s/def ::data (s/nilable ::us/string))
(s/def ::object-id ::us/string)
(s/def ::upsert-file-object-thumbnail
(s/keys :req-un [::profile-id ::file-id ::object-id ::data]))
(sv/defmethod ::upsert-file-object-thumbnail
[{:keys [pool] :as cfg} {:keys [profile-id file-id object-id data]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(if data
(db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data])
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id}))
nil))
;; --- Mutation: upsert file thumbnail
(def sql:upsert-file-thumbnail
"insert into file_thumbnail (file_id, revn, data, props)
values (?, ?, ?, ?::jsonb)
on conflict(file_id, revn) do
update set data = ?, props=?, updated_at=now();")
(s/def ::revn ::us/integer)
(s/def ::props map?)
(s/def ::upsert-file-thumbnail
(s/keys :req-un [::profile-id ::file-id ::revn ::data ::props]))
(sv/defmethod ::upsert-file-thumbnail
[{:keys [pool] :as cfg} {:keys [profile-id file-id revn data props]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(let [props (db/tjson (or props {}))]
(db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn data props data props])
nil)))

View File

@@ -6,16 +6,21 @@
(ns app.rpc.mutations.fonts
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.media :as media]
[app.rpc.queries.teams :as teams]
[app.rpc.rlimit :as rlimit]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
[clojure.spec.alpha :as s]
[promesa.core :as p]
[promesa.exec :as px]))
(declare create-font-variant)
@@ -29,7 +34,6 @@
(s/def ::weight valid-weight)
(s/def ::style valid-style)
(s/def ::font-id ::us/uuid)
(s/def ::content-type ::media/font-content-type)
(s/def ::data (s/map-of ::us/string any?))
(s/def ::create-font-variant
@@ -37,52 +41,76 @@
::font-id ::font-family ::font-weight ::font-style]))
(sv/defmethod ::create-font-variant
{::rlimit/permits (cf/get :rlimit-font)}
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)]
(teams/check-edition-permissions! conn profile-id team-id)
(create-font-variant cfg params))))
(let [cfg (update cfg :storage media/configure-assets-storage)]
(teams/check-edition-permissions! pool profile-id team-id)
(create-font-variant cfg params)))
(defn create-font-variant
[{:keys [conn storage] :as cfg} {:keys [data] :as params}]
(let [data (media/run cfg {:cmd :generate-fonts :input data :rlimit :font})
storage (media/configure-assets-storage storage conn)
[{:keys [storage pool executors] :as cfg} {:keys [data] :as params}]
(letfn [(generate-fonts [data]
(px/with-dispatch (:blocking executors)
(media/run {:cmd :generate-fonts :input data})))
;; Function responsible of calculating cryptographyc hash of
;; the provided data. Even though it uses the hight
;; performance BLAKE2b algorithm, we prefer to schedule it
;; to be executed on the blocking executor.
(calculate-hash [data]
(px/with-dispatch (:blocking executors)
(sto/calculate-hash data)))
otf (when-let [fdata (get data "font/otf")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/otf"}))
(validate-data [data]
(when (and (not (contains? data "font/otf"))
(not (contains? data "font/ttf"))
(not (contains? data "font/woff"))
(not (contains? data "font/woff2")))
(ex/raise :type :validation
:code :invalid-font-upload))
data)
ttf (when-let [fdata (get data "font/ttf")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/ttf"}))
(persist-font-object [data mtype]
(when-let [fdata (get data mtype)]
(p/let [hash (calculate-hash fdata)
content (-> (sto/content fdata)
(sto/wrap-with-hash hash))]
(sto/put-object! storage {::sto/content content
::sto/touched-at (dt/now)
::sto/deduplicate? true
:content-type mtype
:bucket "team-font-variant"}))))
woff1 (when-let [fdata (get data "font/woff")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/woff"}))
(persist-fonts [data]
(p/let [otf (persist-font-object data "font/otf")
ttf (persist-font-object data "font/ttf")
woff1 (persist-font-object data "font/woff")
woff2 (persist-font-object data "font/woff2")]
woff2 (when-let [fdata (get data "font/woff2")]
(sto/put-object storage {:content (sto/content fdata)
:content-type "font/woff2"}))]
(d/without-nils
{:otf otf
:ttf ttf
:woff1 woff1
:woff2 woff2})))
(when (and (nil? otf)
(nil? ttf)
(nil? woff1)
(nil? woff2))
(ex/raise :type :validation
:code :invalid-font-upload))
(insert-into-db [{:keys [woff1 woff2 otf ttf]}]
(db/insert! pool :team-font-variant
{:id (uuid/next)
:team-id (:team-id params)
:font-id (:font-id params)
:font-family (:font-family params)
:font-weight (:font-weight params)
:font-style (:font-style params)
:woff1-file-id (:id woff1)
:woff2-file-id (:id woff2)
:otf-file-id (:id otf)
:ttf-file-id (:id ttf)}))
]
(db/insert! conn :team-font-variant
{:id (uuid/next)
:team-id (:team-id params)
:font-id (:font-id params)
:font-family (:font-family params)
:font-weight (:font-weight params)
:font-style (:font-style params)
:woff1-file-id (:id woff1)
:woff2-file-id (:id woff2)
:otf-file-id (:id otf)
:ttf-file-id (:id ttf)})))
(-> (generate-fonts data)
(p/then validate-data)
(p/then persist-fonts (:default executors))
(p/then insert-into-db (:default executors)))))
;; --- UPDATE FONT FAMILY

View File

@@ -56,7 +56,7 @@
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::login-with-ldap {:auth false :rlimit :password}
(sv/defmethod ::login-with-ldap {:auth false}
[{:keys [pool session tokens] :as cfg} params]
(db/with-atomic [conn pool]
(let [info (authenticate params)

View File

@@ -62,7 +62,7 @@
(= :image (:type form)))
(update-in [:metadata :id] #(get index % %))))
;; A function responsible to analize all file data and
;; A function responsible to analyze all file data and
;; replace the old :component-file reference with the new
;; ones, using the provided file-index
(relink-shapes [data]
@@ -294,7 +294,7 @@
;; move all files to the project
(db/exec-one! conn [sql:move-files project-id fids])
;; delete posible broken relations on moved files
;; delete possible broken relations on moved files
(db/exec-one! conn [sql:delete-broken-relations pids])
nil)))
@@ -329,7 +329,7 @@
{:team-id team-id}
{:id project-id})
;; delete posible broken relations on moved files
;; delete possible broken relations on moved files
(db/exec-one! conn [sql:delete-broken-relations pids])
nil)))

View File

@@ -6,19 +6,22 @@
(ns app.rpc.mutations.media
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.media :as cm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.media :as media]
[app.rpc.queries.teams :as teams]
[app.rpc.rlimit :as rlimit]
[app.storage :as sto]
[app.util.http :as http]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[datoteka.core :as fs]))
[promesa.core :as p]
[promesa.exec :as px]))
(def thumbnail-options
{:width 100
@@ -37,9 +40,7 @@
(declare create-file-media-object)
(declare select-file)
(s/def ::content-type ::media/image-content-type)
(s/def ::content (s/and ::media/upload (s/keys :req-un [::content-type])))
(s/def ::content ::media/upload)
(s/def ::is-local ::us/boolean)
(s/def ::upload-file-media-object
@@ -47,12 +48,12 @@
:opt-un [::id]))
(sv/defmethod ::upload-file-media-object
{::rlimit/permits (cf/get :rlimit-image)}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(let [file (select-file conn file-id)]
(teams/check-edition-permissions! conn profile-id (:team-id file))
(-> (assoc cfg :conn conn)
(create-file-media-object params)))))
(let [file (select-file pool file-id)
cfg (update cfg :storage media/configure-assets-storage)]
(teams/check-edition-permissions! pool profile-id (:team-id file))
(create-file-media-object cfg params)))
(defn- big-enough-for-thumbnail?
"Checks if the provided image info is big enough for
@@ -65,89 +66,165 @@
[info]
(= (:mtype info) "image/svg+xml"))
(defn- fetch-url
[url]
(try
(http/get! url {:as :byte-array})
(catch Exception e
(ex/raise :type :validation
:code :unable-to-access-to-url
:cause e))))
;; NOTE: we use the `on conflict do update` instead of `do nothing`
;; because postgresql does not returns anything if no update is
;; performed, the `do update` does the trick.
(defn- download-media
[{:keys [storage] :as cfg} url]
(let [result (fetch-url url)
data (:body result)
mtype (get (:headers result) "content-type")
format (cm/mtype->format mtype)]
(when (nil? format)
(ex/raise :type :validation
:code :media-type-not-allowed
:hint "Seems like the url points to an invalid media object."))
(-> (assoc storage :backend :tmp)
(sto/put-object {:content (sto/content data)
:content-type mtype
:expired-at (dt/in-future {:minutes 30})}))))
(def sql:create-file-media-object
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
on conflict (id) do update set created_at=file_media_object.created_at
returning *")
;; NOTE: the following function executes without a transaction, this
;; means that if something fails in the middle of this function, it
;; will probably leave leaked/unreferenced objects in the database and
;; probably in the storage layer. For handle possible object leakage,
;; we create all media objects marked as touched, this ensures that if
;; something fails, all leaked (already created storage objects) will
;; be eventually marked as deleted by the touched-gc task.
;;
;; The touched-gc task, performs periodic analisis of all touched
;; storage objects and check references of it. This is the reason why
;; `reference` metadata exists: it indicates the name of the table
;; witch holds the reference to storage object (it some kind of
;; inverse, soft referential integrity).
(defn create-file-media-object
[{:keys [conn storage] :as cfg} {:keys [id file-id is-local name content] :as params}]
(media/validate-media-type (:content-type content))
(let [storage (media/configure-assets-storage storage conn)
source-path (fs/path (:tempfile content))
source-mtype (:content-type content)
source-info (media/run cfg {:cmd :info :input {:path source-path :mtype source-mtype}})
[{:keys [storage pool executors] :as cfg} {:keys [id file-id is-local name content] :as params}]
(media/validate-media-type! content)
thumb (when (and (not (svg-image? source-info))
(big-enough-for-thumbnail? source-info))
(media/run cfg (assoc thumbnail-options
:cmd :generic-thumbnail
:input {:mtype (:mtype source-info)
:path source-path})))
(letfn [;; Function responsible to retrieve the file information, as
;; it is synchronous operation it should be wrapped into
;; with-dispatch macro.
(get-info [content]
(px/with-dispatch (:blocking executors)
(media/run {:cmd :info :input content})))
image (if (= (:mtype source-info) "image/svg+xml")
(let [data (slurp source-path)]
(sto/put-object storage {:content (sto/content data)
:content-type (:mtype source-info)}))
(sto/put-object storage {:content (sto/content source-path)
:content-type (:mtype source-info)}))
;; Function responsible of calculating cryptographyc hash of
;; the provided data. Even though it uses the hight
;; performance BLAKE2b algorithm, we prefer to schedule it
;; to be executed on the blocking executor.
(calculate-hash [data]
(px/with-dispatch (:blocking executors)
(sto/calculate-hash data)))
thumb (when thumb
(sto/put-object storage {:content (sto/content (:data thumb) (:size thumb))
:content-type (:mtype thumb)}))]
(db/insert! conn :file-media-object
{:id (or id (uuid/next))
:file-id file-id
:is-local is-local
:name name
:media-id (:id image)
:thumbnail-id (:id thumb)
:width (:width source-info)
:height (:height source-info)
:mtype source-mtype})))
;; Function responsible of generating thumnail. As it is synchronous
;; opetation, it should be wrapped into with-dispatch macro
(generate-thumbnail [info]
(px/with-dispatch (:blocking executors)
(media/run (assoc thumbnail-options
:cmd :generic-thumbnail
:input info))))
(create-thumbnail [info]
(when (and (not (svg-image? info))
(big-enough-for-thumbnail? info))
(p/let [thumb (generate-thumbnail info)
hash (calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
(sto/put-object! storage
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype thumb)
:bucket "file-media-object"}))))
(create-image [info]
(p/let [data (cond-> (:path info) (= (:mtype info) "image/svg+xml") slurp)
hash (calculate-hash data)
content (-> (sto/content data)
(sto/wrap-with-hash hash))]
(sto/put-object! storage
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype info)
:bucket "file-media-object"})))
(insert-into-database [info image thumb]
(px/with-dispatch (:default executors)
(db/exec-one! pool [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
(:id image)
(:id thumb)
(:width info)
(:height info)
(:mtype info)])))]
(p/let [info (get-info content)
thumb (create-thumbnail info)
image (create-image info)]
(insert-into-database info image thumb))))
;; --- Create File Media Object (from URL)
(declare ^:private create-file-media-object-from-url)
(s/def ::create-file-media-object-from-url
(s/keys :req-un [::profile-id ::file-id ::is-local ::url]
:opt-un [::id ::name]))
(sv/defmethod ::create-file-media-object-from-url
[{:keys [pool storage] :as cfg} {:keys [profile-id file-id url name] :as params}]
(db/with-atomic [conn pool]
(let [file (select-file conn file-id)]
(teams/check-edition-permissions! conn profile-id (:team-id file))
(let [mobj (download-media cfg url)
content {:filename "tempfile"
:size (:size mobj)
:tempfile (sto/get-object-path storage mobj)
:content-type (:content-type (meta mobj))}
params' (merge params {:content content
:name (or name (:filename content))})]
(-> (assoc cfg :conn conn)
(create-file-media-object params'))))))
{::rlimit/permits (cf/get :rlimit-image)}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(let [file (select-file pool file-id)
cfg (update cfg :storage media/configure-assets-storage)]
(teams/check-edition-permissions! pool profile-id (:team-id file))
(create-file-media-object-from-url cfg params)))
(def max-download-file-size
(* 1024 1024 100)) ; 100MiB
(defn- create-file-media-object-from-url
[{:keys [storage http-client] :as cfg} {:keys [url name] :as params}]
(letfn [(parse-and-validate-size [headers]
(let [size (some-> (get headers "content-length") d/parse-integer)
mtype (get headers "content-type")
format (cm/mtype->format mtype)]
(when-not size
(ex/raise :type :validation
:code :unknown-size
:hint "Seems like the url points to resource with unknown size"))
(when (> size max-download-file-size)
(ex/raise :type :validation
:code :file-too-large
:hint "Seems like the url points to resource with size greater than 100MiB"))
(when (nil? format)
(ex/raise :type :validation
:code :media-type-not-allowed
:hint "Seems like the url points to an invalid media object"))
{:size size
:mtype mtype
:format format}))
(get-upload-object [sobj]
(p/let [path (sto/get-object-path storage sobj)
mdata (meta sobj)]
{:filename "tempfile"
:size (:size sobj)
:path path
:mtype (:content-type mdata)}))
(download-media [uri]
(p/let [{:keys [body headers]} (http-client {:method :get :uri uri} {:response-type :input-stream})
{:keys [size mtype]} (parse-and-validate-size headers)]
(-> (assoc storage :backend :tmp)
(sto/put-object! {::sto/content (sto/content body size)
::sto/expired-at (dt/in-future {:minutes 30})
:content-type mtype
:bucket "file-media-object"})
(p/then get-upload-object))))]
(p/let [content (download-media url)]
(->> (merge params {:content content :name (or name (:filename content))})
(create-file-media-object cfg)))))
;; --- Clone File Media object (Upload and create from url)
@@ -161,7 +238,6 @@
(db/with-atomic [conn pool]
(let [file (select-file conn file-id)]
(teams/check-edition-permissions! conn profile-id (:team-id file))
(-> (assoc cfg :conn conn)
(clone-file-media-object params)))))
@@ -179,7 +255,6 @@
:height (:height mobj)
:mtype (:mtype mobj)})))
;; --- HELPERS
(def ^:private

View File

@@ -6,30 +6,32 @@
(ns app.rpc.mutations.profile
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.http.oauth :refer [extract-utm-props]]
[app.loggers.audit :as audit]
[app.media :as media]
[app.metrics :as mtx]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.rpc.rlimit :as rlimit]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[promesa.core :as p]
[promesa.exec :as px]))
;; --- Helpers & Specs
(s/def ::email ::us/email)
(s/def ::fullname ::us/not-empty-string)
(s/def ::lang (s/nilable ::us/not-empty-string))
(s/def ::lang ::us/string)
(s/def ::path ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::password ::us/not-empty-string)
@@ -37,7 +39,6 @@
(s/def ::theme ::us/string)
(s/def ::invitation-token ::us/not-empty-string)
(declare annotate-profile-register)
(declare check-profile-existence!)
(declare create-profile)
(declare create-profile-relations)
@@ -99,15 +100,22 @@
(sv/defmethod ::prepare-register-profile {:auth false}
[{:keys [pool tokens] :as cfg} params]
(when-not (contains? cf/flags :registration)
(ex/raise :type :restriction
:code :registration-disabled))
(if-not (contains? params :invitation-token)
(ex/raise :type :restriction
:code :registration-disabled)
(let [invitation (tokens :verify {:token (:invitation-token params) :iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction
:code :email-does-not-match-invitation
:hint "email should match the invitation")))))
(when-let [domains (cf/get :registration-domain-whitelist)]
(when-not (email-domain-in-whitelist? domains (:email params))
(ex/raise :type :validation
:code :email-domain-is-not-allowed)))
;; Don't allow proceed in preparing registration if the profile is
;; already reported as spamer.
;; already reported as spammer.
(when (eml/has-bounce-reports? pool (:email params))
(ex/raise :type :validation
:code :email-has-permanent-bounces
@@ -115,10 +123,19 @@
(check-profile-existence! pool params)
(let [params (assoc params
:backend "penpot"
:iss :prepared-register
:exp (dt/in-future "48h"))
(when (= (str/lower (:email params))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password"))
(let [params {:email (:email params)
:password (:password params)
:invitation-token (:invitation-token params)
:backend "penpot"
:iss :prepared-register
:exp (dt/in-future "48h")}
token (tokens :generate params)]
{:token token}))
@@ -128,59 +145,45 @@
(s/def ::register-profile
(s/keys :req-un [::token ::fullname]))
(sv/defmethod ::register-profile {:auth false :rlimit :password}
(sv/defmethod ::register-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(register-profile params))))
(defn- annotate-profile-register
"A helper for properly increase the profile-register metric once the
transaction is completed."
[metrics]
(fn []
(let [mobj (get-in metrics [:definitions :profile-register])]
((::mtx/fn mobj) {:by 1}))))
(defn register-profile
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]
[{:keys [conn tokens session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens :verify {:token token :iss :prepared-register})
params (merge params claims)]
(check-profile-existence! conn params)
(let [is-active (or (:is-active params)
(contains? cf/flags :insecure-register))
profile (->> (assoc params :is-active is-active)
(create-profile conn)
(create-profile-relations conn)
(decode-profile-row))]
(let [is-active (or (:is-active params)
(contains? cf/flags :insecure-register))
profile (->> (assoc params :is-active is-active)
(create-profile conn)
(create-profile-relations conn)
(decode-profile-row))
invitation (when-let [token (:invitation-token params)]
(tokens :verify {:token token :iss :team-invitation}))]
(cond
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged).
(some? (:invitation-token params))
(let [token (:invitation-token params)
claims (tokens :verify {:token token :iss :team-invitation})
claims (assoc claims
:member-id (:id profile)
:member-email (:email profile))
;; If invitation token comes in params, this is because the user comes from team-invitation process;
;; in this case, regenerate token and send back to the user a new invitation token (and mark current
;; session as logged). This happens only if the invitation email matches with the register email.
(and (some? invitation) (= (:email profile) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile))
token (tokens :generate claims)
resp {:invitation-token token}]
(with-meta resp
{:transform-response ((:create session) (:id profile))
:before-complete (annotate-profile-register metrics)
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
;; If auth backend is different from "penpot" means user is
;; registring using third party auth mechanism; in this case
;; registering using third party auth mechanism; in this case
;; we need to mark this session as logged.
(not= "penpot" (:auth-backend profile))
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
:before-complete (annotate-profile-register metrics)
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})
@@ -189,7 +192,6 @@
(true? is-active)
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
:before-complete (annotate-profile-register metrics)
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})
@@ -212,8 +214,7 @@
:extra-data ptoken})
(with-meta profile
{:before-complete (annotate-profile-register metrics)
::audit/props (audit/profile->props profile)
{::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
(defn create-profile
@@ -222,7 +223,7 @@
[conn params]
(let [id (or (:id params) (uuid/next))
props (-> (extract-utm-props params)
props (-> (audit/extract-utm-params params)
(merge (:props params))
(db/tjson))
@@ -281,8 +282,15 @@
(s/keys :req-un [::email ::password]
:opt-un [::scope ::invitation-token]))
(sv/defmethod ::login {:auth false :rlimit :password}
(sv/defmethod ::login
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
(when-not (contains? cf/flags :login)
(ex/raise :type :restriction
:code :login-disabled
:hint "login is disabled in this instance"))
(letfn [(check-password [profile password]
(when (= (:password profile) "!")
(ex/raise :type :validation
@@ -302,82 +310,106 @@
profile)]
(db/with-atomic [conn pool]
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
(validate-profile)
(profile/strip-private-attrs)
(profile/populate-additional-data conn)
(decode-profile-row))]
(if-let [token (:invitation-token params)]
;; If the request comes with an invitation token, this means
;; that user wants to accept it with different user. A very
;; strange case but still can happen. In this case, we
;; proceed in the same way as in register: regenerate the
;; invitation token and return it to the user for proper
;; invitation acceptation.
(let [claims (tokens :verify {:token token :iss :team-invitation})
claims (assoc claims
:member-id (:id profile)
:member-email (:email profile))
token (tokens :generate claims)]
(with-meta {:invitation-token token}
{:transform-response ((:create session) (:id profile))
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
(validate-profile)
(profile/strip-private-attrs)
(profile/populate-additional-data conn)
(decode-profile-row))
(with-meta profile
{:transform-response ((:create session) (:id profile))
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
invitation (when-let [token (:invitation-token params)]
(tokens :verify {:token token :iss :team-invitation}))
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
;; invitation because invitations matches exactly; and user can't loging with other email and
;; accept invitation with other email
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
{:invitation-token (:invitation-token params)}
profile)]
(with-meta response
{:transform-response ((:create session) (:id profile))
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
;; --- MUTATION: Logout
(s/def ::logout
(s/keys :req-un [::profile-id]))
(s/keys :opt-un [::profile-id]))
(sv/defmethod ::logout
(sv/defmethod ::logout {:auth false}
[{:keys [session] :as cfg} _]
(with-meta {}
{:transform-response (:delete session)}))
;; --- MUTATION: Update Profile (own)
(defn- update-profile
[conn {:keys [id fullname lang theme] :as params}]
(let [profile (db/update! conn :profile
{:fullname fullname
:lang lang
:theme theme}
{:id id})]
(-> profile
(profile/decode-profile-row)
(profile/strip-private-attrs))))
(s/def ::newsletter-subscribed ::us/boolean)
(s/def ::update-profile
(s/keys :req-un [::id ::fullname]
:opt-un [::lang ::theme]))
(s/keys :req-un [::fullname ::profile-id]
:opt-un [::lang ::theme ::newsletter-subscribed]))
(sv/defmethod ::update-profile
[{:keys [pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [profile-id fullname lang theme newsletter-subscribed] :as params}]
(db/with-atomic [conn pool]
(let [profile (update-profile conn params)]
(with-meta profile
;; NOTE: we need to retrieve the profile independently if we use
;; it or not for explicit locking and avoid concurrent updates of
;; the same row/object.
(let [profile (-> (db/get-by-id conn :profile profile-id {:for-update true})
(profile/decode-profile-row))
;; Update the profile map with direct params
profile (-> profile
(assoc :fullname fullname)
(assoc :lang lang)
(assoc :theme theme))
;; Update profile props if the indirect prop is coming in
;; the params map and update the profile props data
;; acordingly.
profile (cond-> profile
(some? newsletter-subscribed)
(update :props assoc :newsletter-subscribed newsletter-subscribed))]
(db/update! conn :profile
{:fullname fullname
:lang lang
:theme theme
:props (db/tjson (:props profile))}
{:id profile-id})
(with-meta (-> profile profile/strip-private-attrs d/without-nils)
{::audit/props (audit/profile->props profile)}))))
;; --- MUTATION: Update Password
(declare validate-password!)
(declare update-profile-password!)
(declare invalidate-profile-session!)
(s/def ::update-profile-password
(s/keys :req-un [::profile-id ::password ::old-password]))
(sv/defmethod ::update-profile-password {:rlimit :password}
(sv/defmethod ::update-profile-password
{::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool] :as cfg} {:keys [password] :as params}]
(db/with-atomic [conn pool]
(let [profile (validate-password! conn params)]
(let [profile (validate-password! conn params)
session-id (:app.rpc/session-id params)]
(when (= (str/lower (:email profile))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password"))
(update-profile-password! conn (assoc profile :password password))
(invalidate-profile-session! conn (:id profile) session-id)
nil)))
(defn- invalidate-profile-session!
"Removes all sessions except the current one."
[conn profile-id session-id]
(let [sql "delete from http_session where profile_id = ? and id != ?"]
(:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id]))))
(defn- validate-password!
[conn {:keys [profile-id old-password] :as params}]
(let [profile (db/get-by-id conn :profile profile-id)]
@@ -392,48 +424,42 @@
{:password (derive-password password)}
{:id id}))
;; --- MUTATION: Update Photo
(declare update-profile-photo)
(s/def ::content-type ::media/image-content-type)
(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
(s/def ::file ::media/upload)
(s/def ::update-profile-photo
(s/keys :req-un [::profile-id ::file]))
(sv/defmethod ::update-profile-photo
[{:keys [pool storage] :as cfg} {:keys [profile-id file] :as params}]
(db/with-atomic [conn pool]
(media/validate-media-type (:content-type file) #{"image/jpeg" "image/png" "image/webp"})
(media/run cfg {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
{::rlimit/permits (cf/get :rlimit-image)}
[cfg {:keys [file] :as params}]
;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
(let [cfg (update cfg :storage media/configure-assets-storage)]
(update-profile-photo cfg params)))
(let [profile (db/get-by-id conn :profile profile-id)
storage (media/configure-assets-storage storage conn)
cfg (assoc cfg :storage storage)
(defn update-profile-photo
[{:keys [pool storage executors] :as cfg} {:keys [profile-id] :as params}]
(p/let [profile (px/with-dispatch (:default executors)
(db/get-by-id pool :profile profile-id))
photo (teams/upload-photo cfg params)]
;; Schedule deletion of old photo
(when-let [id (:photo-id profile)]
(sto/del-object storage id))
;; Save new photo
(update-profile-photo conn profile-id photo))))
(defn- update-profile-photo
[conn profile-id sobj]
(db/update! conn :profile
{:photo-id (:id sobj)}
{:id profile-id})
nil)
;; Schedule deletion of old photo
(when-let [id (:photo-id profile)]
(sto/touch-object! storage id))
;; Save new photo
(db/update! pool :profile
{:photo-id (:id photo)}
{:id profile-id})
nil))
;; --- MUTATION: Request Email Change
(declare request-email-change)
(declare change-email-inmediatelly)
(declare change-email-immediately)
(s/def ::request-email-change
(s/keys :req-un [::email]))
@@ -449,9 +475,9 @@
(if (or (cf/get :smtp-enabled)
(contains? cf/flags :smtp))
(request-email-change cfg params)
(change-email-inmediatelly cfg params)))))
(change-email-immediately cfg params)))))
(defn- change-email-inmediatelly
(defn- change-email-immediately
[{:keys [conn]} {:keys [profile email] :as params}]
(when (not= email (:email profile))
(check-profile-existence! conn params))
@@ -554,7 +580,8 @@
(s/def ::recover-profile
(s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile {:auth false :rlimit :password}
(sv/defmethod ::recover-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool tokens] :as cfg} {:keys [token password]}]
(letfn [(validate-token [token]
(let [tdata (tokens :verify {:token token :iss :password-recovery})]
@@ -592,7 +619,8 @@
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id})
nil)))
(profile/filter-profile-props props))))
;; --- MUTATION: Delete Profile
@@ -633,7 +661,7 @@
(let [rows (db/exec! conn [sql:owned-teams profile-id])]
;; If we found owned teams with more than one profile we don't
;; allow delete profile until the user properly transfer ownership
;; or explictly removes all participants from the team.
;; or explicitly removes all participants from the team.
(when (some #(> (:num-profiles %) 1) rows)
(ex/raise :type :validation
:code :owner-teams-with-people

View File

@@ -8,20 +8,26 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.loggers.audit :as audit]
[app.media :as media]
[app.rpc.mutations.projects :as projects]
[app.rpc.permissions :as perms]
[app.rpc.queries.profile :as profile]
[app.rpc.queries.teams :as teams]
[app.rpc.rlimit :as rlimit]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[datoteka.core :as fs]))
[cuerdas.core :as str]
[promesa.core :as p]
[promesa.exec :as px]))
;; --- Helpers & Specs
@@ -102,24 +108,53 @@
;; --- Mutation: Leave Team
(declare role->params)
(s/def ::reassign-to ::us/uuid)
(s/def ::leave-team
(s/keys :req-un [::profile-id ::id]))
(s/keys :req-un [::profile-id ::id]
:opt-un [::reassign-to]))
(sv/defmethod ::leave-team
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
[{:keys [pool] :as cfg} {:keys [id profile-id reassign-to]}]
(db/with-atomic [conn pool]
(let [perms (teams/check-read-permissions! conn profile-id id)
(let [perms (teams/get-permissions conn profile-id id)
members (teams/retrieve-team-members conn id)]
(when (some :is-owner perms)
(cond
;; we can only proceed if there are more members in the team
;; besides the current profile
(<= (count members) 1)
(ex/raise :type :validation
:code :no-enough-members-for-leave
:context {:members (count members)})
;; if the `reassign-to` is filled and has a different value
;; than the current profile-id, we proceed to reassing the
;; owner role to profile identified by the `reassign-to`.
(and reassign-to (not= reassign-to profile-id))
(let [member (d/seek #(= reassign-to (:id %)) members)]
(when-not member
(ex/raise :type :not-found :code :member-does-not-exist))
;; unasign owner role to current profile
(db/update! conn :team-profile-rel
{:is-owner false}
{:team-id id
:profile-id profile-id})
;; assign owner role to new profile
(db/update! conn :team-profile-rel
(role->params :owner)
{:team-id id :profile-id reassign-to}))
;; and finally, if all other conditions does not match and the
;; current profile is owner, we dont allow it because there
;; must always be an owner.
(:is-owner perms)
(ex/raise :type :validation
:code :owner-cant-leave-team
:hint "reasing owner before leave"))
(when-not (> (count members) 1)
(ex/raise :type :validation
:code :cant-leave-team
:context {:members (count members)}))
:hint "releasing owner before leave"))
(db/delete! conn :team-profile-rel
{:profile-id profile-id
@@ -127,14 +162,13 @@
nil)))
;; --- Mutation: Delete Team
(s/def ::delete-team
(s/keys :req-un [::profile-id ::id]))
;; TODO: right now just don't allow delete default team, in future it
;; should raise a speific exception for signal that this acction is
;; should raise a specific exception for signal that this action is
;; not allowed.
(sv/defmethod ::delete-team
@@ -154,7 +188,6 @@
;; --- Mutation: Team Update Role
(declare retrieve-team-member)
(declare role->params)
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)
@@ -169,17 +202,16 @@
(sv/defmethod ::update-team-member-role
[{:keys [pool] :as cfg} {:keys [team-id profile-id member-id role] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/check-read-permissions! conn profile-id team-id)
(let [perms (teams/get-permissions conn profile-id team-id)
;; We retrieve all team members instead of query the
;; database for a single member. This is just for
;; convenience, if this bocomes a bottleneck or problematic,
;; we will change it to more efficient fetch mechanims.
;; convenience, if this becomes a bottleneck or problematic,
;; we will change it to more efficient fetch mechanisms.
members (teams/retrieve-team-members conn team-id)
member (d/seek #(= member-id (:id %)) members)
is-owner? (some :is-owner perms)
is-admin? (some :is-admin perms)]
is-owner? (:is-owner perms)
is-admin? (:is-admin perms)]
;; If no member is found, just 404
(when-not member
@@ -232,9 +264,9 @@
(sv/defmethod ::delete-team-member
[{:keys [pool] :as cfg} {:keys [team-id profile-id member-id] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/check-read-permissions! conn profile-id team-id)]
(when-not (or (some :is-owner perms)
(some :is-admin perms))
(let [perms (teams/get-permissions conn profile-id team-id)]
(when-not (or (:is-owner perms)
(:is-admin perms))
(ex/raise :type :validation
:code :insufficient-permissions))
@@ -247,56 +279,73 @@
nil)))
;; --- Mutation: Update Team Photo
(declare upload-photo)
(s/def ::content-type ::media/image-content-type)
(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
(declare ^:private upload-photo)
(declare ^:private update-team-photo)
(s/def ::file ::media/upload)
(s/def ::update-team-photo
(s/keys :req-un [::profile-id ::team-id ::file]))
(sv/defmethod ::update-team-photo
[{:keys [pool storage] :as cfg} {:keys [profile-id file team-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(media/validate-media-type (:content-type file) #{"image/jpeg" "image/png" "image/webp"})
(media/run cfg {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
{::rlimit/permits (cf/get :rlimit-image)}
[cfg {:keys [file] :as params}]
;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
(let [cfg (update cfg :storage media/configure-assets-storage)]
(update-team-photo cfg params)))
(let [team (teams/retrieve-team conn profile-id team-id)
storage (media/configure-assets-storage storage conn)
cfg (assoc cfg :storage storage)
photo (upload-photo cfg params)]
(defn update-team-photo
[{:keys [pool storage executors] :as cfg} {:keys [profile-id team-id] :as params}]
(p/let [team (px/with-dispatch (:default executors)
(teams/retrieve-team pool profile-id team-id))
photo (upload-photo cfg params)]
;; Schedule deletion of old photo
(when-let [id (:photo-id team)]
(sto/del-object storage id))
;; Mark object as touched for make it ellegible for tentative
;; garbage collection.
(when-let [id (:photo-id team)]
(sto/touch-object! storage id))
;; Save new photo
(db/update! conn :team
{:photo-id (:id photo)}
{:id team-id})
;; Save new photo
(db/update! pool :team
{:photo-id (:id photo)}
{:id team-id})
(assoc team :photo-id (:id photo)))))
(assoc team :photo-id (:id photo))))
(defn upload-photo
[{:keys [storage] :as cfg} {:keys [file]}]
(let [thumb (media/run cfg
{:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input {:path (fs/path (:tempfile file))
:mtype (:content-type file)}})]
[{:keys [storage executors] :as cfg} {:keys [file]}]
(letfn [(get-info [content]
(px/with-dispatch (:blocking executors)
(media/run {:cmd :info :input content})))
(generate-thumbnail [info]
(px/with-dispatch (:blocking executors)
(media/run {:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input info})))
(sto/put-object storage
{:content (sto/content (:data thumb) (:size thumb))
:content-type (:mtype thumb)})))
;; Function responsible of calculating cryptographyc hash of
;; the provided data. Even though it uses the hight
;; performance BLAKE2b algorithm, we prefer to schedule it
;; to be executed on the blocking executor.
(calculate-hash [data]
(px/with-dispatch (:blocking executors)
(sto/calculate-hash data)))]
(p/let [info (get-info file)
thumb (generate-thumbnail info)
hash (calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
(sto/put-object! storage {::sto/content content
::sto/deduplicate? true
:bucket "profile"
:content-type (:mtype thumb)}))))
;; --- Mutation: Invite Member
@@ -304,15 +353,20 @@
(declare create-team-invitation)
(s/def ::email ::us/email)
(s/def ::emails ::us/set-of-emails)
(s/def ::invite-team-member
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
(s/keys :req-un [::profile-id ::team-id ::role]
:opt-un [::email ::emails]))
(sv/defmethod ::invite-team-member
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
"A rpc call that allow to send a single or multiple invitations to
join the team."
[{:keys [pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)]
team (db/get-by-id conn :team team-id)
emails (cond-> (or emails #{}) (string? email) (conj email))]
(when-not (:is-admin perms)
(ex/raise :type :validation
@@ -324,42 +378,60 @@
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(create-team-invitation
(assoc cfg
:email email
:conn conn
:team team
:profile profile
:role role))
nil)))
(doseq [email emails]
(create-team-invitation
(assoc cfg
:email email
:conn conn
:team team
:profile profile
:role role))
)
(with-meta {}
{::audit/props {:invitations (count emails)}}))))
(def sql:upsert-team-invitation
"insert into team_invitation(team_id, email_to, role, valid_until)
values (?, ?, ?, ?)
on conflict(team_id, email_to) do
update set role = ?, valid_until = ?, updated_at = now();")
(defn- create-team-invitation
[{:keys [conn tokens team profile role email] :as cfg}]
(let [member (profile/retrieve-profile-data-by-email conn email)
itoken (tokens :generate
{:iss :team-invitation
:exp (dt/in-future "48h")
:profile-id (:id profile)
:role role
:team-id (:id team)
:member-email (:email member email)
:member-id (:id member)})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(let [member (profile/retrieve-profile-data-by-email conn email)
token-exp (dt/in-future "168h") ;; 7 days
itoken (tokens :generate
{:iss :team-invitation
:exp token-exp
:profile-id (:id profile)
:role role
:team-id (:id team)
:member-email (:email member email)
:member-id (:id member)})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(when (contains? cf/flags :log-invitation-tokens)
(l/trace :hint "invitation token" :token itoken))
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
:code :member-is-muted
:email email
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
;; Secondly check if the invited member email is part of the
;; global spam/bounce report.
;; Secondly check if the invited member email is part of the global spam/bounce report.
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
:code :email-has-permanent-bounces
:email email
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(db/exec-one! conn [sql:upsert-team-invitation
(:id team) (str/lower email) (name role) token-exp (name role) token-exp])
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
:public-uri (:public-uri cfg)
@@ -369,7 +441,6 @@
:token itoken
:extra-data ptoken})))
;; --- Mutation: Create Team & Invite Members
(s/def ::emails ::us/set-of-emails)
@@ -379,8 +450,9 @@
(sv/defmethod ::create-team-and-invite-members
[{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [team (create-team conn params)
profile (db/get-by-id conn :profile profile-id)]
(let [team (create-team conn params)
audit-fn (:audit cfg)
profile (db/get-by-id conn :profile profile-id)]
;; Create invitations for all provided emails.
(doseq [email emails]
@@ -391,4 +463,53 @@
:profile profile
:email email
:role role)))
team)))
(with-meta team
{::audit/props {:invitations (count emails)}
:before-complete
#(audit-fn :cmd :submit
:type "mutation"
:name "invite-team-member"
:profile-id profile-id
:props {:emails emails
:role role
:profile-id profile-id
:invitations (count emails)})}))))
;; --- Mutation: Update invitation role
(s/def ::update-team-invitation-role
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
(sv/defmethod ::update-team-invitation-role
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(db/update! conn :team-invitation
{:role (name role) :updated-at (dt/now)}
{:team-id team-id :email-to (str/lower email)})
nil)))
;; --- Mutation: Delete invitation
(s/def ::delete-team-invitation
(s/keys :req-un [::profile-id ::team-id ::email]))
(sv/defmethod ::delete-team-invitation
[{:keys [pool] :as cfg} {:keys [profile-id team-id email] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(db/delete! conn :team-invitation
{:team-id team-id :email-to (str/lower email)})
nil)))

View File

@@ -10,11 +10,11 @@
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as audit]
[app.metrics :as mtx]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(defmulti process-token (fn [_ _ claims] (:iss claims)))
@@ -44,16 +44,8 @@
::audit/props {:email email}
::audit/profile-id profile-id}))
(defn- annotate-profile-activation
"A helper for properly increase the profile-activation metric once the
transaction is completed."
[metrics]
(fn []
(let [mobj (get-in metrics [:definitions :profile-activation])]
((::mtx/fn mobj) {:by 1}))))
(defmethod process-token :verify-email
[{:keys [conn session metrics] :as cfg} _ {:keys [profile-id] :as claims}]
[{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)
claims (assoc claims :profile profile)]
@@ -69,7 +61,6 @@
(with-meta claims
{:transform-response ((:create session) profile-id)
:before-complete (annotate-profile-activation metrics)
::audit/name "verify-profile-email"
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})))
@@ -100,11 +91,18 @@
:opt-un [:internal.tokens.team-invitation/member-id]))
(defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [member-id team-id role] :as claims}]
(let [params (merge {:team-id team-id
[{:keys [conn] :as cfg} {:keys [member-id team-id role member-email] :as claims}]
(let [
member (profile/retrieve-profile conn member-id)
invitation (db/get-by-params conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)}
{:check-not-found false})
;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role)
params (merge {:team-id team-id
:profile-id member-id}
(teams/role->params role))
member (profile/retrieve-profile conn member-id)]
]
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
@@ -115,80 +113,57 @@
(db/update! conn :profile
{:is-active true}
{:id member-id}))
(assoc member :is-active true)))
(assoc member :is-active true)
;; Delete the invitation
(db/delete! conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)})))
(defmethod process-token :team-invitation
[{:keys [session] :as cfg} {:keys [profile-id token]} {:keys [member-id] :as claims}]
[cfg {:keys [profile-id token]} {:keys [member-id] :as claims}]
(us/assert ::team-invitation-claims claims)
(let [conn (:conn cfg)
team-id (:team-id claims)
member-email (:member-email claims)
invitation (db/get-by-params conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)}
{:check-not-found false})]
(when (nil? invitation)
(ex/raise :type :validation
:code :invalid-token)))
(cond
;; This happens when token is filled with member-id and current
;; user is already logged in with some account.
(and (uuid? profile-id)
(uuid? member-id))
;; user is already logged in with exactly invited account.
(and (uuid? profile-id) (uuid? member-id) (= member-id profile-id))
(let [profile (accept-invitation cfg claims)]
(if (= member-id profile-id)
;; If the current session is already matches the invited
;; member, then just return the token and leave the frontend
;; app redirect to correct team.
(assoc claims :state :created)
;; If the session does not matches the invited member, replace
;; the session with a new one matching the invited member.
;; This techinique should be considered secure because the
;; user clicking the link he already has access to the email
;; account.
(with-meta
(assoc claims :state :created)
{:transform-response ((:create session) member-id)
::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id profile-id})))
;; This happens when member-id is not filled in the invitation but
;; the user already has an account (probably with other mail) and
;; is already logged-in.
(and (uuid? profile-id)
(nil? member-id))
(let [profile (accept-invitation cfg (assoc claims :member-id profile-id))]
(with-meta
(assoc claims :state :created)
{::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id profile-id}))
;; This happens when member-id is filled but the accessing user is
;; not logged-in. In this case we proceed to accept invitation and
;; leave the user logged-in.
(and (nil? profile-id)
(uuid? member-id))
(let [profile (accept-invitation cfg claims)]
(with-meta
(assoc claims :state :created)
{:transform-response ((:create session) member-id)
::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id member-id}))
;; In this case, we wait until frontend app redirect user to
;; registeration page, the user is correctly registered and the
;; register mutation call us again with the same token to finally
;; create the corresponding team-profile relation from the first
;; condition of this if.
;; This case means that invitation token does not match with
;; registred user, so we need to indicate to frontend to redirect
;; it to register page.
(nil? member-id)
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-register
:state :pending}
;; In all other cases, just tell to fontend to redirect the user
;; to the login page.
:else
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-login
:state :pending}))
;; --- Default
(defmethod process-token :default

View File

@@ -6,24 +6,30 @@
(ns app.rpc.queries.files
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.pages.helpers :as cph]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.db.sql :as sql]
[app.rpc.helpers :as rpch]
[app.rpc.permissions :as perms]
[app.rpc.queries.projects :as projects]
[app.rpc.queries.share-link :refer [retrieve-share-link]]
[app.rpc.queries.teams :as teams]
[app.storage.impl :as simpl]
[app.util.blob :as blob]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(declare decode-row)
(declare decode-row-xf)
;; --- Helpers & Specs
(s/def ::frame-id ::us/uuid)
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::project-id ::us/uuid)
@@ -32,7 +38,6 @@
(s/def ::team-id ::us/uuid)
(s/def ::search-term ::us/string)
;; --- Query: File Permissions
(def ^:private sql:file-permissions
@@ -84,8 +89,8 @@
(let [perms (get-permissions conn profile-id file-id)
ldata (retrieve-share-link conn file-id share-id)]
;; NOTE: in a future when share-link becomes more powerfull and
;; will allow us specify which parts of the app is availabel, we
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
@@ -165,6 +170,7 @@
f.created_at,
f.modified_at,
f.name,
f.revn,
f.is_shared
from file as f
where f.project_id = ?
@@ -182,21 +188,28 @@
;; --- Query: File (By ID)
(defn- retrieve-data*
[{:keys [storage] :as cfg} file]
(when-let [backend (simpl/resolve-backend storage (:data-backend file))]
(simpl/get-object-bytes backend file)))
(defn retrieve-object-thumbnails
([{:keys [pool]} file-id]
(let [sql (str/concat
"select object_id, data "
" from file_object_thumbnail"
" where file_id=?")]
(->> (db/exec! pool [sql file-id])
(d/index-by :object-id :data))))
(defn retrieve-data
[cfg file]
(if (bytes? (:data file))
file
(assoc file :data (retrieve-data* cfg file))))
([{:keys [pool]} file-id object-ids]
(with-open [conn (db/open pool)]
(let [sql (str/concat
"select object_id, data "
" from file_object_thumbnail"
" where file_id=? and object_id = ANY(?)")
ids (db/create-array conn "text" (seq object-ids))]
(->> (db/exec! conn [sql file-id ids])
(d/index-by :object-id :data))))))
(defn retrieve-file
[{:keys [conn] :as cfg} id]
(->> (db/get-by-id conn :file id)
(retrieve-data cfg)
[{:keys [pool] :as cfg} id]
(->> (db/get-by-id pool :file id)
(decode-row)
(pmg/migrate-file)))
@@ -206,60 +219,146 @@
(sv/defmethod ::file
"Retrieve a file by its ID. Only authenticated users."
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)
perms (get-permissions conn profile-id id)]
(let [perms (get-permissions pool profile-id id)]
(check-read-permissions! perms)
(let [file (retrieve-file cfg id)
thumbs (retrieve-object-thumbnails cfg id)]
(-> file
(assoc :thumbnails thumbs)
(assoc :permissions perms)))))
(check-read-permissions! perms)
(some-> (retrieve-file cfg id)
(assoc :permissions perms)))))
;; --- QUERY: page
(defn- prune-objects
"Given the page data and the object-id returns the page data with all
other not needed objects removed from the `:objects` data
structure."
[{:keys [objects] :as page} object-id]
(let [objects (cph/get-children-with-self objects object-id)]
(assoc page :objects (d/index-by :id objects))))
(defn- prune-thumbnails
"Given the page data, removes the `:thumbnail` prop from all
shapes."
[page]
(update page :objects d/update-vals #(dissoc % :thumbnail)))
(s/def ::page-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::page
(s/keys :req-un [::profile-id ::file-id]))
(defn remove-thumbnails-frames
"Removes from data the children for frames that have a thumbnail set up"
[data]
(let [filter-shape?
(fn [objects [id shape]]
(let [frame-id (:frame-id shape)]
(or (= id uuid/zero)
(= frame-id uuid/zero)
(not (some? (get-in objects [frame-id :thumbnail]))))))
;; We need to remove from the attribute :shapes its childrens because
;; they will not be sent in the data
remove-frame-children
(fn [[id shape]]
[id (cond-> shape
(some? (:thumbnail shape))
(assoc :shapes []))])
update-objects
(fn [objects]
(into {}
(comp (map remove-frame-children)
(filter (partial filter-shape? objects)))
objects))]
(update data :objects update-objects)))
(s/and
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::page-id ::object-id])
(fn [obj]
(if (contains? obj :object-id)
(contains? obj :page-id)
true))))
(sv/defmethod ::page
[{:keys [pool] :as cfg} {:keys [profile-id file-id strip-thumbnails]}]
(db/with-atomic [conn pool]
(check-read-permissions! conn profile-id file-id)
"Retrieves the page data from file and returns it. If no page-id is
specified, the first page will be returned. If object-id is
specified, only that object and its children will be returned in the
page objects data structure.
If you specify the object-id, the page-id parameter becomes
mandatory.
Mainly used for rendering purposes."
[{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id] :as props}]
(check-read-permissions! pool profile-id file-id)
(let [file (retrieve-file cfg file-id)
page-id (or page-id (-> file :data :pages first))
page (get-in file [:data :pages-index page-id])]
(cond-> (prune-thumbnails page)
(uuid? object-id)
(prune-objects object-id))))
;; --- QUERY: file-data-for-thumbnail
(defn- get-file-thumbnail-data
[cfg {:keys [data id] :as file}]
(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.
(get-thumbnail-frame [data]
(d/seek :use-for-thumbnail?
(for [page (-> data :pages-index vals)
frame (-> page :objects cph/get-frames)]
(assoc frame :page-id (:id page)))))
;; function responsible to filter objects data strucuture of
;; all unneded shapes if a concrete frame is provided. If no
;; frame, the objects is returned untouched.
(filter-objects [objects frame-id]
(d/index-by :id (cph/get-children-with-self objects frame-id)))
;; function responsible of assoc available thumbnails
;; to frames and remove all children shapes from objects if
;; thumbnails is available
(assoc-thumbnails [objects page-id thumbnails]
(loop [objects objects
frames (filter cph/frame-shape? (vals objects))]
(if-let [frame (-> frames first)]
(let [frame-id (:id frame)
object-id (str page-id frame-id)
frame (if-let [thumb (get thumbnails object-id)]
(assoc frame :thumbnail thumb :shapes [])
(dissoc frame :thumbnail))]
(if (:thumbnail frame)
(recur (-> (assoc objects frame-id frame)
(d/without-keys (cph/get-children-ids objects frame-id)))
(rest frames))
(recur (assoc objects frame-id frame)
(rest frames))))
objects)))]
(let [frame (get-thumbnail-frame data)
frame-id (:id frame)
page-id (or (:page-id frame)
(-> data :pages first))
page (dm/get-in data [:pages-index page-id])
frame-ids (if (some? frame) (list frame-id) (map :id (cph/get-frames (:objects page))))
obj-ids (map #(str page-id %) frame-ids)
thumbs (retrieve-object-thumbnails cfg id obj-ids)]
(cond-> page
;; If we have frame, we need to specify it on the page level
;; and remove the all other unrelated objects.
(some? frame-id)
(-> (assoc :thumbnail-frame-id frame-id)
(update :objects filter-objects frame-id))
;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecesary data.
:always
(update :objects assoc-thumbnails page-id thumbs)))))
(s/def ::file-data-for-thumbnail
(s/keys :req-un [::profile-id ::file-id]))
(sv/defmethod ::file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used
mainly for render thumbnails on dashboard."
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as props}]
(check-read-permissions! pool profile-id file-id)
(let [file (retrieve-file cfg file-id)]
{:file-id file-id
:revn (:revn file)
:page (get-file-thumbnail-data cfg file)}))
(let [cfg (assoc cfg :conn conn)
file (retrieve-file cfg file-id)
page-id (get-in file [:data :pages 0])]
(cond-> (get-in file [:data :pages-index page-id])
strip-thumbnails
(remove-thumbnails-frames)))))
;; --- Query: Shared Library Files
(def ^:private sql:team-shared-files
"select f.id,
f.revn,
f.project_id,
f.created_at,
f.modified_at,
@@ -308,28 +407,26 @@
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
(defn retrieve-file-libraries
[{:keys [conn] :as cfg} is-indirect file-id]
[{:keys [pool] :as cfg} is-indirect file-id]
(let [xform (comp
(map #(assoc % :is-indirect is-indirect))
(map #(retrieve-data cfg %))
(map decode-row))]
(into #{} xform (db/exec! conn [sql:file-libraries file-id]))))
(into #{} xform (db/exec! pool [sql:file-libraries file-id]))))
(s/def ::file-libraries
(s/keys :req-un [::profile-id ::file-id]))
(sv/defmethod ::file-libraries
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)]
(check-read-permissions! conn profile-id file-id)
(retrieve-file-libraries cfg false file-id))))
(check-read-permissions! pool profile-id file-id)
(retrieve-file-libraries cfg false file-id))
;; --- QUERY: team-recent-files
(def sql:team-recent-files
"with recent_files as (
select f.id,
f.revn,
f.project_id,
f.created_at,
f.modified_at,
@@ -346,14 +443,44 @@
)
select * from recent_files where row_num <= 10;")
(s/def ::team-recent-files
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::team-recent-files
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(db/exec! conn [sql:team-recent-files team-id])))
(teams/check-read-permissions! pool profile-id team-id)
(db/exec! pool [sql:team-recent-files team-id]))
;; --- QUERY: get file thumbnail
(s/def ::revn ::us/integer)
(s/def ::file-thumbnail
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::revn]))
(sv/defmethod ::file-thumbnail
[{:keys [pool]} {:keys [profile-id file-id revn]}]
(check-read-permissions! pool profile-id file-id)
(let [sql (sql/select :file-thumbnail
(cond-> {:file-id file-id}
revn (assoc :revn revn))
{:limit 1
:order-by [[:revn :desc]]})
row (db/exec-one! pool sql)]
(when-not row
(ex/raise :type :not-found
:code :file-thumbnail-not-found))
(with-meta
{:data (:data row)
:props (some-> (:props row) db/decode-transit-pgobject)
:revn (:revn row)
:file-id (:file-id row)}
{:transform-response (rpch/http-cache {:max-age (* 1000 60 60)})})))
;; --- Helpers

View File

@@ -35,12 +35,17 @@
(s/def ::profile
(s/keys :opt-un [::profile-id]))
(sv/defmethod ::profile {:auth false}
(sv/defmethod ::profile
{:auth false}
[{:keys [pool] :as cfg} {:keys [profile-id] :as params}]
(if profile-id
(retrieve-profile pool profile-id)
{:id uuid/zero
:fullname "Anonymous User"}))
;; We need to return the anonymous profile object in two cases, when
;; no profile-id is in session, and when db call raises not found. In all other
;; cases we need to reraise the exception.
(or (ex/try*
#(some->> profile-id (retrieve-profile pool))
#(when (not= :not-found (:type (ex-data %))) (throw %)))
{:id uuid/zero
:fullname "Anonymous User"}))
(def ^:private sql:default-profile-team
"select t.id, name
@@ -70,7 +75,7 @@
[conn profile]
(merge profile (retrieve-additional-data conn (:id profile))))
(defn- filter-profile-props
(defn filter-profile-props
[props]
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
@@ -106,6 +111,6 @@
;; --- Attrs Helpers
(defn strip-private-attrs
"Only selects a publicy visible profile attrs."
"Only selects a publicly visible profile attrs."
[row]
(dissoc row :password :deleted-at))

View File

@@ -21,8 +21,10 @@
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
join team as t on (t.id = tpr.team_id)
where tpr.profile_id = ?
and tpr.team_id = ?")
and tpr.team_id = ?
and t.deleted_at is null")
(defn get-permissions
[conn profile-id team-id]
@@ -227,3 +229,21 @@
(defn retrieve-team-stats
[conn team-id]
(db/exec-one! conn [sql:team-stats team-id team-id]))
;; --- Query: Team invitations
(s/def ::team-id ::us/uuid)
(s/def ::team-invitations
(s/keys :req-un [::profile-id ::team-id]))
(def sql:team-invitations
"select email_to as email, role, (valid_until < now()) as expired
from team_invitation where team_id = ? order by valid_until desc")
(sv/defmethod ::team-invitations
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(->> (db/exec! conn [sql:team-invitations team-id])
(mapv #(update % :role keyword)))))

View File

@@ -13,27 +13,28 @@
[app.rpc.queries.share-link :as slnk]
[app.rpc.queries.teams :as teams]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
[clojure.spec.alpha :as s]
[promesa.core :as p]))
;; --- Query: View Only Bundle
(defn- retrieve-project
[conn id]
(db/get-by-id conn :project id {:columns [:id :name :team-id]}))
[pool id]
(db/get-by-id pool :project id {:columns [:id :name :team-id]}))
(defn- retrieve-bundle
[{:keys [conn] :as cfg} file-id]
(let [file (files/retrieve-file cfg file-id)
project (retrieve-project conn (:project-id file))
libs (files/retrieve-file-libraries cfg false file-id)
users (teams/retrieve-users conn (:team-id project))
[{:keys [pool] :as cfg} file-id]
(p/let [file (files/retrieve-file cfg file-id)
project (retrieve-project pool (:project-id file))
libs (files/retrieve-file-libraries cfg false file-id)
users (teams/retrieve-users pool (:team-id project))
links (->> (db/query conn :share-link {:file-id file-id})
(mapv slnk/decode-share-link-row))
links (->> (db/query pool :share-link {:file-id file-id})
(mapv slnk/decode-share-link-row))
fonts (db/query conn :team-font-variant
{:team-id (:team-id project)
:deleted-at nil})]
fonts (db/query pool :team-font-variant
{:team-id (:team-id project)
:deleted-at nil})]
{:file file
:users users
:fonts fonts
@@ -50,34 +51,31 @@
(sv/defmethod ::view-only-bundle {:auth false}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)
slink (slnk/retrieve-share-link conn file-id share-id)
perms (files/get-permissions conn profile-id file-id share-id)
(p/let [slink (slnk/retrieve-share-link pool file-id share-id)
perms (files/get-permissions pool profile-id file-id share-id)
bundle (p/-> (retrieve-bundle cfg file-id)
(assoc :permissions perms))]
bundle (some-> (retrieve-bundle cfg file-id)
(assoc :permissions perms))]
;; When we have neither profile nor share, we just return a not
;; found response to the user.
(when (and (not profile-id)
(not slink))
(ex/raise :type :not-found
:code :object-not-found))
;; When we have neither profile nor share, we just return a not
;; found response to the user.
(when (and (not profile-id)
(not slink))
(ex/raise :type :not-found
:code :object-not-found))
;; When we have only profile, we need to check read permissions
;; on file.
(when (and profile-id (not slink))
(files/check-read-permissions! pool profile-id file-id))
;; When we have only profile, we need to check read permissiones
;; on file.
(when (and profile-id (not slink))
(files/check-read-permissions! conn profile-id file-id))
(cond-> bundle
(some? slink)
(assoc :share slink)
(cond-> bundle
(some? slink)
(assoc :share slink)
(and (some? slink)
(not (contains? (:flags slink) "view-all-pages")))
(update-in [:file :data] (fn [data]
(let [allowed-pages (:pages slink)]
(-> data
(update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages)))
(update :pages-index (fn [index] (select-keys index allowed-pages)))))))))))
(and (some? slink)
(not (contains? (:flags slink) "view-all-pages")))
(update-in [:file :data] (fn [data]
(let [allowed-pages (:pages slink)]
(-> data
(update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages)))
(update :pages-index (fn [index] (select-keys index allowed-pages))))))))))

View File

@@ -0,0 +1,45 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.rpc.retry
"A fault tolerance helpers. Allow retry some operations that we know
we can retry."
(:require
[app.common.logging :as l]
[app.util.services :as sv]
[promesa.core :as p]))
(defn conflict-db-insert?
"Check if exception matches a insertion conflict on postgresql."
[e]
(and (instance? org.postgresql.util.PSQLException e)
(= "23505" (.getSQLState e))))
(defn wrap-retry
[_ f {:keys [::matches ::sv/name]
:or {matches (constantly false)}
:as mdata}]
(when (::enabled mdata)
(l/debug :hint "wrapping retry" :name name))
(if-let [max-retries (::max-retries mdata)]
(fn [cfg params]
(letfn [(run [retry]
(-> (f cfg params)
(p/catch (partial handle-error retry))))
(handle-error [retry cause]
(if (matches cause)
(let [current-retry (inc retry)]
(l/trace :hint "running retry algorithm" :retry current-retry)
(if (<= current-retry max-retries)
(run current-retry)
(throw cause)))
(throw cause)))]
(run 0)))
f))

View File

@@ -0,0 +1,67 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.rpc.rlimit
"Resource usage limits (in other words: semaphores)."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.metrics :as mtx]
[app.util.services :as sv]
[promesa.core :as p]))
(defprotocol IAsyncSemaphore
(acquire! [_])
(release! [_]))
(defn semaphore
[{:keys [permits metrics name]}]
(let [name (d/name name)
used (volatile! 0)
queue (volatile! (d/queue))
labels (into-array String [name])]
(reify IAsyncSemaphore
(acquire! [this]
(let [d (p/deferred)]
(locking this
(if (< @used permits)
(do
(vswap! used inc)
(p/resolve! d))
(vswap! queue conj d)))
(mtx/run! metrics {:id :rlimit-used-permits :val @used :labels labels })
(mtx/run! metrics {:id :rlimit-queued-submissions :val (count @queue) :labels labels})
(mtx/run! metrics {:id :rlimit-acquires-total :inc 1 :labels labels})
d))
(release! [this]
(locking this
(if-let [item (peek @queue)]
(do
(vswap! queue pop)
(p/resolve! item))
(when (pos? @used)
(vswap! used dec))))
(mtx/run! metrics {:id :rlimit-used-permits :val @used :labels labels})
(mtx/run! metrics {:id :rlimit-queued-submissions :val (count @queue) :labels labels})
))))
(defn wrap-rlimit
[{:keys [metrics executors] :as cfg} f mdata]
(if-let [permits (::permits mdata)]
(let [sem (semaphore {:permits permits
:metrics metrics
:name (::sv/name mdata)})]
(l/debug :hint "wrapping rlimit" :handler (::sv/name mdata) :permits permits)
(fn [cfg params]
(-> (acquire! sem)
(p/then (fn [_] (f cfg params)) (:default executors))
(p/finally (fn [_ _] (release! sem))))))
f))

View File

@@ -7,6 +7,7 @@
(ns app.setup
"Initial data setup of instance."
(:require
[app.common.logging :as l]
[app.common.uuid :as uuid]
[app.db :as db]
[buddy.core.codecs :as bc]
@@ -14,55 +15,49 @@
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(declare initialize-instance-id!)
(declare initialize-secret-key!)
(declare retrieve-all)
(defn- generate-random-key
[]
(-> (bn/random-bytes 64)
(bc/bytes->b64u)
(bc/bytes->str)))
(defn- retrieve-all
[conn]
(->> (db/query conn :server-prop {:preload true})
(filter #(not= "secret-key" (:id %)))
(map (fn [row]
[(keyword (:id row))
(db/decode-transit-pgobject (:content row))]))
(into {})))
(defn- handle-instance-id
[instance-id conn read-only?]
(or instance-id
(let [instance-id (uuid/random)]
(when-not read-only?
(try
(db/insert! conn :server-prop
{:id "instance-id"
:preload true
:content (db/tjson instance-id)})
(catch Throwable cause
(l/warn :hint "unable to persist instance-id"
:instance-id instance-id
:cause cause))))
instance-id)))
(defmethod ig/pre-init-spec ::props [_]
(s/keys :req-un [::db/pool]))
(defmethod ig/init-key ::props
[_ {:keys [pool] :as cfg}]
[_ {:keys [pool key] :as cfg}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)]
(initialize-secret-key! cfg)
(initialize-instance-id! cfg)
(retrieve-all cfg))))
(db/xact-lock! conn 0)
(when-not key
(l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
"all sessions on each restart, it is hightly recommeded setting up the "
"PENPOT_SECRET_KEY environment variable")))
(def sql:upsert-secret-key
"insert into server_prop (id, preload, content)
values ('secret-key', true, ?::jsonb)
on conflict (id) do update set content = ?::jsonb")
(def sql:insert-secret-key
"insert into server_prop (id, preload, content)
values ('secret-key', true, ?::jsonb)
on conflict (id) do nothing")
(defn- initialize-secret-key!
[{:keys [conn key] :as cfg}]
(if key
(let [key (db/tjson key)]
(db/exec-one! conn [sql:upsert-secret-key key key]))
(let [key (-> (bn/random-bytes 64)
(bc/bytes->b64u)
(bc/bytes->str))
key (db/tjson key)]
(db/exec-one! conn [sql:insert-secret-key key]))))
(defn- initialize-instance-id!
[{:keys [conn] :as cfg}]
(let [iid (uuid/random)]
(db/insert! conn :server-prop
{:id "instance-id"
:preload true
:content (db/tjson iid)}
{:on-conflict-do-nothing true})))
(defn- retrieve-all
[{:keys [conn] :as cfg}]
(reduce (fn [acc row]
(assoc acc (keyword (:id row)) (db/decode-transit-pgobject (:content row))))
{}
(db/query conn :server-prop {:preload true})))
(let [stored (-> (retrieve-all conn)
(assoc :secret-key (or key (generate-random-key))))]
(update stored :instance-id handle-instance-id conn (db/read-only? pool)))))

View File

@@ -2,9 +2,13 @@
"A main namespace for server repl."
#_:clj-kondo/ignore
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pages :as cp]
[app.common.uuid :as uuid]
[app.common.pages.migrations :as pmg]
[app.common.spec.file :as spec.file]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.db.sql :as sql]
@@ -12,12 +16,50 @@
[app.rpc.queries.profile :as prof]
[app.srepl.dev :as dev]
[app.util.blob :as blob]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.walk :as walk]
[cuerdas.core :as str]
[clojure.pprint :refer [pprint]]))
[expound.alpha :as expound]
[fipp.edn :refer [pprint]]))
;; ==== Utility functions
(defn reset-file-data
"Hardcode replace of the data of one file."
[system id data]
(db/with-atomic [conn (:app.db/pool system)]
(db/update! conn :file
{:data data}
{:id id})))
(defn get-file
"Get the migrated data of one file."
[system id]
(-> (:app.db/pool system)
(db/get-by-id :file id)
(update :data app.util.blob/decode)
(update :data pmg/migrate-data)))
(defn duplicate-file
"This is a raw version of duplication of file just only for forensic analysis."
[system file-id email]
(db/with-atomic [conn (:app.db/pool system)]
(when-let [profile (some->> (prof/retrieve-profile-data-by-email conn (str/lower email))
(prof/populate-additional-data conn))]
(when-let [file (db/exec-one! conn (sql/select :file {:id file-id}))]
(let [params (assoc file
:id (uuid/next)
:project-id (:default-project-id profile))]
(db/insert! conn :file params)
(:id file))))))
(defn update-file
([id f] (update-file id f false))
([id f save?]
"Apply a function to the data of one file. Optionally save the changes or not.
The function receives the decoded and migrated file data."
([system id f] (update-file system id f false))
([system id f save?]
(db/with-atomic [conn (:app.db/pool system)]
(let [file (db/get-by-id conn :file id {:for-update true})
file (-> file
@@ -32,52 +74,131 @@
{:id (:id file)}))
(update file :data blob/decode)))))
(defn update-file-raw
[id data]
(db/with-atomic [conn (:app.db/pool system)]
(db/update! conn :file
{:data data}
{:id id})))
(defn analyze-files
"Apply a function to all files in the database, reading them in batches. Do not change data.
The function receives an object with some properties of the file and the decoded data, and
an empty atom where it may accumulate statistics, if desired."
[system {:keys [sleep chunk-size max-chunks on-file]
:or {sleep 1000 chunk-size 10 max-chunks ##Inf}}]
(let [stats (atom {})]
(letfn [(retrieve-chunk [conn cursor]
(let [sql (str "select id, name, modified_at, data from file "
" where modified_at < ? and deleted_at is null "
" order by modified_at desc limit ?")]
(->> (db/exec! conn [sql cursor chunk-size])
(map #(update % :data blob/decode)))))
(defn get-file
[system id]
(with-open [conn (db/open (:app.db/pool system))]
(let [file (db/get-by-id conn :file id)]
(-> file
(update :data app.util.blob/decode)
(update :data pmg/migrate-data)))))
(process-chunk [chunk]
(loop [files chunk]
(when-let [file (first files)]
(on-file file stats)
(recur (rest files)))))]
(db/with-atomic [conn (:app.db/pool system)]
(loop [cursor (dt/now)
chunks 0]
(when (< chunks max-chunks)
(let [chunk (retrieve-chunk conn cursor)]
(when-not (empty? chunk)
(let [cursor (-> chunk last :modified-at)]
(process-chunk chunk)
(Thread/sleep (inst-ms (dt/duration sleep)))
(recur cursor (inc chunks)))))))
@stats))))
(defn update-pages
"Apply a function to all pages of one file. The function receives a page and returns an updated page."
[data f]
(update data :pages-index d/update-vals f))
(defn update-shapes
"Apply a function to all shapes of one page The function receives a shape and returns an updated shape"
[page f]
(update page :objects d/update-vals f))
;; Examples:
;; (def backup (update-file #uuid "1586e1f0-3e02-11eb-b1d2-556a2f641513" identity))
;; (def x (update-file
;; #uuid "1586e1f0-3e02-11eb-b1d2-556a2f641513"
;; (fn [{:keys [data] :as file}]
;; (update-in data [:pages-index #uuid "878278c0-3ef0-11eb-9d67-8551e7624f43" :objects] dissoc nil))))
;; ==== Specific fixes
;; Migrate
(defn repair-orphaned-shapes
"There are some shapes whose parent has been deleted. This
function detects them and puts them as children of the root node."
([file _] ; to be called from analyze-files to search for files with the problem
(repair-orphaned-shapes (:data file)))
(defn update-file-data-blob-format
[system]
(db/with-atomic [conn (:app.db/pool system)]
(doseq [id (->> (db/exec! conn ["select id from file;"]) (map :id))]
(let [{:keys [data]} (db/get-by-id conn :file id {:columns [:id :data]})]
(prn "Updating file:" id)
(db/update! conn :file
{:data (-> (blob/decode data)
(blob/encode {:version 2}))}
{:id id})))))
([data]
(let [is-orphan? (fn [shape objects]
(and (some? (:parent-id shape))
(nil? (get objects (:parent-id shape)))))
update-page (fn [page]
(let [objects (:objects page)
orphans (set (filter #(is-orphan? % objects) (vals objects)))]
(if (seq orphans)
(do
(prn (:id data) "file has" (count orphans) "broken shapes")
(-> page
(update-shapes (fn [shape]
(if (orphans shape)
(assoc shape :parent-id uuid/zero)
shape)))
(update-in [:objects uuid/zero :shapes]
(fn [shapes] (into shapes (map :id orphans))))))
page)))]
(update-pages data update-page))))
(defn duplicate-file
"This is a raw version of duplication of file just only for forensic analisys"
[system file-id email]
(db/with-atomic [conn (:app.db/pool system)]
(when-let [profile (some->> (prof/retrieve-profile-data-by-email conn (str/lower email))
(prof/populate-additional-data conn))]
(when-let [file (db/exec-one! conn (sql/select :file {:id file-id}))]
(let [params (assoc file
:id (uuid/next)
:project-id (:default-project-id profile))]
(db/insert! conn :file params)
(:id file))))))
;; DO NOT DELETE already used scripts, could be taken as templates for easyly writing new ones
;; -------------------------------------------------------------------------------------------
;; (defn repair-orphaned-components
;; "We have detected some cases of component instances that are not nested, but
;; however they have not the :component-root? attribute (so the system considers
;; them nested). This script fixes this adding them the attribute.
;;
;; Use it with the update-file function above."
;; [data]
;; (let [update-page
;; (fn [page]
;; (prn "================= Page:" (:name page))
;; (letfn [(is-nested? [object]
;; (and (some? (:component-id object))
;; (nil? (:component-root? object))))
;;
;; (is-instance? [object]
;; (some? (:shape-ref object)))
;;
;; (get-parent [object]
;; (get (:objects page) (:parent-id object)))
;;
;; (update-object [object]
;; (if (and (is-nested? object)
;; (not (is-instance? (get-parent object))))
;; (do
;; (prn "Orphan:" (:name object))
;; (assoc object :component-root? true))
;; object))]
;;
;; (update page :objects d/update-vals update-object)))]
;;
;; (update data :pages-index d/update-vals update-page)))
;; (defn check-image-shapes
;; [{:keys [data] :as file} stats]
;; (println "=> analizing file:" (:name file) (:id file))
;; (swap! stats update :total-files (fnil inc 0))
;; (let [affected? (atom false)]
;; (walk/prewalk (fn [obj]
;; (when (and (map? obj) (= :image (:type obj)))
;; (when-let [fcolor (some-> obj :fill-color str/upper)]
;; (when (or (= fcolor "#B1B2B5")
;; (= fcolor "#7B7D85"))
;; (reset! affected? true)
;; (swap! stats update :affected-shapes (fnil inc 0))
;; (println "--> image shape:" ((juxt :id :name :fill-color :fill-opacity) obj)))))
;; obj)
;; data)
;; (when @affected?
;; (swap! stats update :affected-files (fnil inc 0)))))

View File

@@ -8,6 +8,7 @@
"Objects storage abstraction layer."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
@@ -22,6 +23,7 @@
[clojure.spec.alpha :as s]
[datoteka.core :as fs]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -40,7 +42,7 @@
:db ::sdb/backend))))
(defmethod ig/pre-init-spec ::storage [_]
(s/keys :req-un [::wrk/executor ::db/pool ::backends]))
(s/keys :req-un [::db/pool ::wrk/executor ::backends]))
(defmethod ig/prep-key ::storage
[_ {:keys [backends] :as cfg}]
@@ -53,65 +55,70 @@
(assoc :backends (d/without-nils backends))))
(s/def ::storage
(s/keys :req-un [::backends ::wrk/executor ::db/pool]))
(s/keys :req-un [::backends ::db/pool]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Database Objects
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defrecord StorageObject [id size created-at expired-at backend])
(defrecord StorageObject [id size created-at expired-at touched-at backend])
(defn storage-object?
[v]
(instance? StorageObject v))
(def ^:private
sql:insert-storage-object
"insert into storage_object (id, size, backend, metadata)
values (?, ?, ?, ?::jsonb)
returning *")
(s/def ::storage-object storage-object?)
(s/def ::storage-content impl/content?)
(def ^:private
sql:insert-storage-object-with-expiration
"insert into storage_object (id, size, backend, metadata, deleted_at)
values (?, ?, ?, ?::jsonb, ?)
returning *")
(defn get-metadata
[params]
(into {}
(remove (fn [[k _]] (qualified-keyword? k)))
params))
(defn- insert-object
[conn id size backend mdata expiration]
(if expiration
(db/exec-one! conn [sql:insert-storage-object-with-expiration id size backend mdata expiration])
(db/exec-one! conn [sql:insert-storage-object id size backend mdata])))
(defn- get-database-object-by-hash
[conn backend bucket hash]
(let [sql (str "select * from storage_object "
" where (metadata->>'~:hash') = ? "
" and (metadata->>'~:bucket') = ? "
" and backend = ?"
" and deleted_at is null"
" limit 1")]
(db/exec-one! conn [sql hash bucket (name backend)])))
(defn- create-database-object
[{:keys [conn backend]} {:keys [content] :as object}]
(if (instance? StorageObject object)
;; If we in this condition branch, this means we come from the
;; clone-object, so we just need to clone it with a new backend.
[{:keys [conn backend executor]} {:keys [::content ::expired-at ::touched-at] :as params}]
(us/assert ::storage-content content)
(px/with-dispatch executor
(let [id (uuid/random)
mdata (meta object)
result (insert-object conn
id
(:size object)
(name backend)
(db/tjson mdata)
(:expired-at object))]
(assoc object
:id (:id result)
:backend backend
:created-at (:created-at result)))
(let [id (uuid/random)
mdata (dissoc object :content :expired-at)
result (insert-object conn
id
(count content)
(name backend)
(db/tjson mdata)
(:expired-at object))]
mdata (cond-> (get-metadata params)
(satisfies? impl/IContentHash content)
(assoc :hash (impl/get-hash content)))
;; NOTE: for now we don't reuse the deleted objects, but in
;; futute we can consider reusing deleted objects if we
;; found a duplicated one and is marked for deletion but
;; still not deleted.
result (when (and (::deduplicate? params)
(:hash mdata)
(:bucket mdata))
(get-database-object-by-hash conn backend (:bucket mdata) (:hash mdata)))
result (or result
(db/insert! conn :storage-object
{:id id
:size (count content)
:backend (name backend)
:metadata (db/tjson mdata)
:deleted-at expired-at
:touched-at touched-at}))]
(StorageObject. (:id result)
(:size result)
(:created-at result)
(:deleted-at result)
(:touched-at result)
backend
mdata
nil))))
@@ -120,11 +127,12 @@
"select * from storage_object where id = ? and (deleted_at is null or deleted_at > now())")
(defn row->storage-object [res]
(let [mdata (some-> (:metadata res) (db/decode-transit-pgobject))]
(let [mdata (or (some-> (:metadata res) (db/decode-transit-pgobject)) {})]
(StorageObject. (:id res)
(:size res)
(:created-at res)
(:deleted-at res)
(:touched-at res)
(keyword (:backend res))
mdata
nil)))
@@ -134,18 +142,6 @@
(when-let [res (db/exec-one! conn [sql:retrieve-storage-object id])]
(row->storage-object res)))
(def sql:delete-storage-object
"update storage_object set deleted_at=now() where id=?")
(defn- delete-database-object
[{:keys [conn] :as storage} id]
(let [result (db/exec-one! conn [sql:delete-storage-object id])]
(pos? (:next.jdbc/update-count result))))
(defn- register-recheck
[{:keys [pool] :as storage} backend id]
(db/insert! pool :storage-pending {:id id :backend (name backend)}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -158,28 +154,24 @@
[url]
(fs/path (java.net.URI. (str url))))
(defn content
([data] (impl/content data nil))
([data size] (impl/content data size)))
(dm/export impl/content)
(dm/export impl/wrap-with-hash)
(defn get-object
[{:keys [conn pool] :as storage} id]
(us/assert ::storage storage)
(-> (assoc storage :conn (or conn pool))
(retrieve-database-object id)))
(p/do
(-> (assoc storage :conn (or conn pool))
(retrieve-database-object id))))
(defn put-object
(defn put-object!
"Creates a new object with the provided content."
[{:keys [pool conn backend executor] :as storage} {:keys [content] :as params}]
[{:keys [pool conn backend] :as storage} {:keys [::content] :as params}]
(us/assert ::storage storage)
(us/assert impl/content? content)
(let [storage (assoc storage :conn (or conn pool))
object (create-database-object storage params)]
;; Schedule to execute in background; in an other transaction and
;; register the currently created storage object id for a later
;; recheck.
(px/run! executor #(register-recheck storage backend (:id object)))
(us/assert ::storage-content content)
(us/assert ::us/keyword backend)
(p/let [storage (assoc storage :conn (or conn pool))
object (create-database-object storage params)]
;; Store the data finally on the underlying storage subsystem.
(-> (impl/resolve-backend storage backend)
@@ -187,296 +179,272 @@
object))
(defn clone-object
"Creates a clone of the provided object using backend basded efficient
method. Always clones objects to the configured default."
[{:keys [pool conn] :as storage} object]
(us/assert ::storage storage)
(let [storage (assoc storage :conn (or conn pool))
object* (create-database-object storage object)]
(if (= (:backend object) (:backend storage))
;; if the source and destination backends are the same, we
;; proceed to use the fast path with specific copy
;; implementation on backend.
(-> (impl/resolve-backend storage (:backend storage))
(impl/copy-object object object*))
;; if the source and destination backends are different, we just
;; need to obtain the streams and proceed full copy of the data
(with-open [is (-> (impl/resolve-backend storage (:backend object))
(impl/get-object-data object))]
(-> (impl/resolve-backend storage (:backend storage))
(impl/put-object object* (impl/content is (:size object))))))
object*))
(defn touch-object!
"Mark object as touched."
[{:keys [pool conn] :as storage} object-or-id]
(p/do
(let [id (if (storage-object? object-or-id) (:id object-or-id) object-or-id)
res (db/update! (or conn pool) :storage-object
{:touched-at (dt/now)}
{:id id}
{:return-keys false})]
(pos? (:next.jdbc/update-count res)))))
(defn get-object-data
"Return an input stream instance of the object content."
[{:keys [pool conn] :as storage} object]
(us/assert ::storage storage)
(when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now)))
(-> (assoc storage :conn (or conn pool))
(impl/resolve-backend (:backend object))
(impl/get-object-data object))))
(p/do
(when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now)))
(-> (assoc storage :conn (or conn pool))
(impl/resolve-backend (:backend object))
(impl/get-object-data object)))))
(defn get-object-bytes
"Returns a byte array of object content."
[{:keys [pool conn] :as storage} object]
(us/assert ::storage storage)
(when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now)))
(-> (assoc storage :conn (or conn pool))
(impl/resolve-backend (:backend object))
(impl/get-object-bytes object))))
(p/do
(when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now)))
(-> (assoc storage :conn (or conn pool))
(impl/resolve-backend (:backend object))
(impl/get-object-bytes object)))))
(defn get-object-url
([storage object]
(get-object-url storage object nil))
([{:keys [conn pool] :as storage} object options]
(us/assert ::storage storage)
(when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now)))
(-> (assoc storage :conn (or conn pool))
(impl/resolve-backend (:backend object))
(impl/get-object-url object options)))))
(p/do
(when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now)))
(-> (assoc storage :conn (or conn pool))
(impl/resolve-backend (:backend object))
(impl/get-object-url object options))))))
(defn get-object-path
"Get the Path to the object. Only works with `:fs` type of
storages."
[storage object]
(let [backend (impl/resolve-backend storage (:backend object))]
(when (not= :fs (:type backend))
(ex/raise :type :internal
:code :operation-not-allowed
:hint "get-object-path only works with fs type backends"))
(when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now)))
(-> (impl/get-object-url backend object nil)
(file-url->path)))))
(p/do
(let [backend (impl/resolve-backend storage (:backend object))]
(when (not= :fs (:type backend))
(ex/raise :type :internal
:code :operation-not-allowed
:hint "get-object-path only works with fs type backends"))
(when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now)))
(p/-> (impl/get-object-url backend object nil) file-url->path)))))
(defn del-object
[{:keys [conn pool] :as storage} id-or-obj]
(defn del-object!
[{:keys [conn pool] :as storage} object-or-id]
(us/assert ::storage storage)
(-> (assoc storage :conn (or conn pool))
(delete-database-object (if (uuid? id-or-obj) id-or-obj (:id id-or-obj)))))
(p/do
(let [id (if (storage-object? object-or-id) (:id object-or-id) object-or-id)
res (db/update! (or conn pool) :storage-object
{:deleted-at (dt/now)}
{:id id}
{:return-keys false})]
(pos? (:next.jdbc/update-count res)))))
(d/export impl/resolve-backend)
(dm/export impl/resolve-backend)
(dm/export impl/calculate-hash)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Garbage Collection: Permanently delete objects
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; A task responsible to permanently delete already marked as deleted
;; storage files.
;; storage files. The storage objects are practically never marked to
;; be deleted directly by the api call. The touched-gc is responsible
;; of collecting the usage of the object and mark it as deleted.
(declare sql:retrieve-deleted-objects)
(declare sql:retrieve-deleted-objects-chunk)
(s/def ::min-age ::dt/duration)
(defmethod ig/pre-init-spec ::gc-deleted-task [_]
(s/keys :req-un [::storage ::db/pool ::min-age]))
(s/keys :req-un [::storage ::db/pool ::min-age ::wrk/executor]))
(defmethod ig/init-key ::gc-deleted-task
[_ {:keys [pool storage min-age] :as cfg}]
(letfn [(group-by-backend [rows]
(let [conj (fnil conj [])]
[(reduce (fn [acc {:keys [id backend]}]
(update acc (keyword backend) conj id))
{}
rows)
(count rows)]))
(letfn [(retrieve-deleted-objects-chunk [conn cursor]
(let [min-age (db/interval min-age)
rows (db/exec! conn [sql:retrieve-deleted-objects-chunk min-age cursor])]
[(some-> rows peek :created-at)
(some->> (seq rows) (d/group-by #(-> % :backend keyword) :id #{}) seq)]))
(retrieve-deleted-objects [conn]
(let [min-age (db/interval min-age)
rows (db/exec! conn [sql:retrieve-deleted-objects min-age])]
(some-> (seq rows) (group-by-backend))))
(->> (d/iteration (fn [cursor]
(retrieve-deleted-objects-chunk conn cursor))
:initk (dt/now)
:vf second
:kf first)
(sequence cat)))
(delete-in-bulk [conn [backend ids]]
(delete-in-bulk [conn backend ids]
(let [backend (impl/resolve-backend storage backend)
backend (assoc backend :conn conn)]
(impl/del-objects-in-bulk backend ids)))]
@(impl/del-objects-in-bulk backend ids)))]
(fn [_]
(db/with-atomic [conn pool]
(loop [n 0]
(if-let [[groups total] (retrieve-deleted-objects conn)]
(loop [total 0
groups (retrieve-deleted-objects conn)]
(if-let [[backend ids] (first groups)]
(do
(run! (partial delete-in-bulk conn) groups)
(recur (+ n ^long total)))
(delete-in-bulk conn backend ids)
(recur (+ total (count ids))
(rest groups)))
(do
(l/info :task "gc-deleted"
:action "permanently delete items"
:count n)
{:deleted n})))))))
(l/info :task "gc-deleted" :count total)
{:deleted total})))))))
(def sql:retrieve-deleted-objects
(def sql:retrieve-deleted-objects-chunk
"with items_part as (
select s.id
from storage_object as s
where s.deleted_at is not null
and s.deleted_at < (now() - ?::interval)
order by s.deleted_at
limit 100
and s.created_at < ?
order by s.created_at desc
limit 25
)
delete from storage_object
where id in (select id from items_part)
returning *;")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Garbage Collection: Analize touched objects
;; Garbage Collection: Analyze touched objects
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; This task is part of the garbage collection of storage objects and
;; is responsible on analizing the touched objects and mark them for deletion
;; if corresponds.
;; This task is part of the garbage collection process of storage
;; objects and is responsible on analyzing the touched objects and
;; mark them for deletion if corresponds.
;;
;; When file_media_object is deleted, the depending storage_object are
;; marked as touched. This means that some files that depend on a
;; concrete storage_object are no longer exists and maybe this
;; storage_object is no longer necessary and can be ellegible for
;; elimination. This task peridically analizes touched objects and
;; mark them as freeze (means that has other references and the object
;; is still valid) or deleted (no more references to this object so is
;; ready to be deleted).
;; For example: when file_media_object is deleted, the depending
;; storage_object are marked as touched. This means that some files
;; that depend on a concrete storage_object are no longer exists and
;; maybe this storage_object is no longer necessary and can be
;; eligible for elimination. This task periodically analyzes touched
;; objects and mark them as freeze (means that has other references
;; and the object is still valid) or deleted (no more references to
;; this object so is ready to be deleted).
(declare sql:retrieve-touched-objects)
(declare sql:retrieve-touched-objects-chunk)
(declare sql:retrieve-file-media-object-nrefs)
(declare sql:retrieve-team-font-variant-nrefs)
(declare sql:retrieve-profile-nrefs)
(defmethod ig/pre-init-spec ::gc-touched-task [_]
(s/keys :req-un [::db/pool]))
(defmethod ig/init-key ::gc-touched-task
[_ {:keys [pool] :as cfg}]
(letfn [(group-results [rows]
(let [conj (fnil conj [])]
(reduce (fn [acc {:keys [id nrefs]}]
(if (pos? nrefs)
(update acc :to-freeze conj id)
(update acc :to-delete conj id)))
{}
rows)))
(letfn [(has-team-font-variant-nrefs? [conn id]
(-> (db/exec-one! conn [sql:retrieve-team-font-variant-nrefs id id id id]) :nrefs pos?))
(retrieve-touched [conn]
(let [rows (db/exec! conn [sql:retrieve-touched-objects])]
(some-> (seq rows) (group-results))))
(has-file-media-object-nrefs? [conn id]
(-> (db/exec-one! conn [sql:retrieve-file-media-object-nrefs id id]) :nrefs pos?))
(mark-delete-in-bulk [conn ids]
(db/exec-one! conn ["update storage_object set deleted_at=now(), touched_at=null where id = ANY(?)"
(db/create-array conn "uuid" (into-array java.util.UUID ids))]))
(has-profile-nrefs? [conn id]
(-> (db/exec-one! conn [sql:retrieve-profile-nrefs id id]) :nrefs pos?))
(mark-freeze-in-bulk [conn ids]
(db/exec-one! conn ["update storage_object set touched_at=null where id = ANY(?)"
(db/create-array conn "uuid" (into-array java.util.UUID ids))]))]
(db/create-array conn "uuid" ids)]))
(mark-delete-in-bulk [conn ids]
(db/exec-one! conn ["update storage_object set deleted_at=now(), touched_at=null where id = ANY(?)"
(db/create-array conn "uuid" ids)]))
;; NOTE: A getter that retrieves the key witch will be used
;; for group ids; previoulsy we have no value, then we
;; introduced the `:reference` prop, and then it is renamed
;; to `:bucket` and now is string instead. This is
;; implemented in this way for backward comaptibilty.
;; NOTE: we use the "file-media-object" as default value for
;; backward compatibility because when we deploy it we can
;; have old backend instances running in the same time as
;; the new one and we can still have storage-objects created
;; without bucket value. And we know that if it does not
;; have value, it means :file-media-object.
(get-bucket [{:keys [metadata]}]
(or (some-> metadata :bucket)
(some-> metadata :reference d/name)
"file-media-object"))
(retrieve-touched-chunk [conn cursor]
(let [rows (->> (db/exec! conn [sql:retrieve-touched-objects-chunk cursor])
(mapv #(d/update-when % :metadata db/decode-transit-pgobject)))]
(when (seq rows)
[(-> rows peek :created-at)
(d/group-by get-bucket :id #{} rows)])))
(retrieve-touched [conn]
(->> (d/iteration (fn [cursor]
(retrieve-touched-chunk conn cursor))
:initk (dt/now)
:vf second
:kf first)
(sequence cat)))
(process-objects! [conn pred-fn ids]
(loop [to-freeze #{}
to-delete #{}
ids (seq ids)]
(if-let [id (first ids)]
(if (pred-fn conn id)
(recur (conj to-freeze id) to-delete (rest ids))
(recur to-freeze (conj to-delete id) (rest ids)))
(do
(some->> (seq to-freeze) (mark-freeze-in-bulk conn))
(some->> (seq to-delete) (mark-delete-in-bulk conn))
[(count to-freeze) (count to-delete)]))))
]
(fn [_]
(db/with-atomic [conn pool]
(loop [cntf 0
cntd 0]
(if-let [{:keys [to-delete to-freeze]} (retrieve-touched conn)]
(loop [to-freeze 0
to-delete 0
groups (retrieve-touched conn)]
(if-let [[bucket ids] (first groups)]
(let [[f d] (case bucket
"file-media-object" (process-objects! conn has-file-media-object-nrefs? ids)
"team-font-variant" (process-objects! conn has-team-font-variant-nrefs? ids)
"profile" (process-objects! conn has-profile-nrefs? ids)
(ex/raise :type :internal
:code :unexpected-unknown-reference
:hint (dm/fmt "unknown reference %" bucket)))]
(recur (+ to-freeze f)
(+ to-delete d)
(rest groups)))
(do
(when (seq to-delete) (mark-delete-in-bulk conn to-delete))
(when (seq to-freeze) (mark-freeze-in-bulk conn to-freeze))
(recur (+ cntf (count to-freeze))
(+ cntd (count to-delete))))
(do
(l/info :task "gc-touched"
:action "mark freeze"
:count cntf)
(l/info :task "gc-touched"
:action "mark for deletion"
:count cntd)
{:freeze cntf :delete cntd})))))))
(l/info :task "gc-touched" :to-freeze to-freeze :to-delete to-delete)
{:freeze to-freeze :delete to-delete})))))))
(def sql:retrieve-touched-objects
"select so.id,
((select count(*) from file_media_object where media_id = so.id) +
(select count(*) from file_media_object where thumbnail_id = so.id)) as nrefs
from storage_object as so
(def sql:retrieve-touched-objects-chunk
"select so.* from storage_object as so
where so.touched_at is not null
order by so.touched_at
limit 100;")
and so.created_at < ?
order by so.created_at desc
limit 500;")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Recheck Stalled Task
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def sql:retrieve-file-media-object-nrefs
"select ((select count(*) from file_media_object where media_id = ?) +
(select count(*) from file_media_object where thumbnail_id = ?)) as nrefs")
;; Because the physical storage (filesystem, s3, ... except db) is not
;; transactional, in some situations we can found physical object
;; leakage. That situations happens when the transaction that writes
;; the file aborts, leaving the file written to the underlying storage
;; but the reference on the database is lost with the rollback.
;;
;; For this situations we need to write a "log" of inserted files that
;; are checked in some time in future. If physical file exists but the
;; database refence does not exists means that leaked file is found
;; and is inmediatelly deleted. The responsability of this task is
;; check that write log for possible leaked files.
(def sql:retrieve-team-font-variant-nrefs
"select ((select count(*) from team_font_variant where woff1_file_id = ?) +
(select count(*) from team_font_variant where woff2_file_id = ?) +
(select count(*) from team_font_variant where otf_file_id = ?) +
(select count(*) from team_font_variant where ttf_file_id = ?)) as nrefs")
(def recheck-min-age (dt/duration {:hours 1}))
(declare sql:retrieve-pending-to-recheck)
(declare sql:exists-storage-object)
(defmethod ig/pre-init-spec ::recheck-task [_]
(s/keys :req-un [::storage ::db/pool]))
(defmethod ig/init-key ::recheck-task
[_ {:keys [pool storage] :as cfg}]
(letfn [(group-results [rows]
(let [conj (fnil conj [])]
(reduce (fn [acc {:keys [id exist] :as row}]
(cond-> (update acc :all conj id)
(false? exist)
(update :to-delete conj (dissoc row :exist))))
{}
rows)))
(group-by-backend [rows]
(let [conj (fnil conj [])]
(reduce (fn [acc {:keys [id backend]}]
(update acc (keyword backend) conj id))
{}
rows)))
(retrieve-pending [conn]
(let [rows (db/exec! conn [sql:retrieve-pending-to-recheck (db/interval recheck-min-age)])]
(some-> (seq rows) (group-results))))
(delete-group [conn [backend ids]]
(let [backend (impl/resolve-backend storage backend)
backend (assoc backend :conn conn)]
(impl/del-objects-in-bulk backend ids)))
(delete-all [conn ids]
(let [ids (db/create-array conn "uuid" (into-array java.util.UUID ids))]
(db/exec-one! conn ["delete from storage_pending where id = ANY(?)" ids])))]
(fn [_]
(db/with-atomic [conn pool]
(loop [n 0 d 0]
(if-let [{:keys [all to-delete]} (retrieve-pending conn)]
(let [groups (group-by-backend to-delete)]
(run! (partial delete-group conn) groups)
(delete-all conn all)
(recur (+ n (count all))
(+ d (count to-delete))))
(do
(l/info :task "recheck"
:action "recheck items"
:processed n
:deleted n)
{:processed n :deleted d})))))))
(def sql:retrieve-pending-to-recheck
"select sp.id,
sp.backend,
sp.created_at,
(case when count(so.id) > 0 then true
else false
end) as exist
from storage_pending as sp
left join storage_object as so
on (so.id = sp.id)
where sp.created_at < now() - ?::interval
group by 1,2,3
order by sp.created_at asc
limit 100")
(def sql:retrieve-profile-nrefs
"select ((select count(*) from profile where photo_id = ?) +
(select count(*) from team where photo_id = ?)) as nrefs")

View File

@@ -10,7 +10,8 @@
[app.db :as db]
[app.storage.impl :as impl]
[clojure.spec.alpha :as s]
[integrant.core :as ig])
[integrant.core :as ig]
[promesa.exec :as px])
(:import
java.io.ByteArrayInputStream))
@@ -30,26 +31,23 @@
;; --- API IMPL
(defmethod impl/put-object :db
[{:keys [conn] :as storage} {:keys [id] :as object} content]
(let [data (impl/slurp-bytes content)]
(db/insert! conn :storage-data {:id id :data data})
object))
(defmethod impl/copy-object :db
[{:keys [conn] :as storage} src-object dst-object]
(db/exec-one! conn ["insert into storage_data (id, data) select ? as id, data from storage_data where id=?"
(:id dst-object)
(:id src-object)]))
[{:keys [conn executor] :as storage} {:keys [id] :as object} content]
(px/with-dispatch executor
(let [data (impl/slurp-bytes content)]
(db/insert! conn :storage-data {:id id :data data})
object)))
(defmethod impl/get-object-data :db
[{:keys [conn] :as backend} {:keys [id] :as object}]
(let [result (db/exec-one! conn ["select data from storage_data where id=?" id])]
(ByteArrayInputStream. (:data result))))
[{:keys [conn executor] :as backend} {:keys [id] :as object}]
(px/with-dispatch executor
(let [result (db/exec-one! conn ["select data from storage_data where id=?" id])]
(ByteArrayInputStream. (:data result)))))
(defmethod impl/get-object-bytes :db
[{:keys [conn] :as backend} {:keys [id] :as object}]
(let [result (db/exec-one! conn ["select data from storage_data where id=?" id])]
(:data result)))
[{:keys [conn executor] :as backend} {:keys [id] :as object}]
(px/with-dispatch executor
(let [result (db/exec-one! conn ["select data from storage_data where id=?" id])]
(:data result))))
(defmethod impl/get-object-url :db
[_ _]

View File

@@ -14,7 +14,8 @@
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.core :as fs]
[integrant.core :as ig])
[integrant.core :as ig]
[promesa.exec :as px])
(:import
java.io.InputStream
java.io.OutputStream
@@ -47,62 +48,57 @@
;; --- API IMPL
(defmethod impl/put-object :fs
[backend {:keys [id] :as object} content]
(let [base (fs/path (:directory backend))
path (fs/path (impl/id->path id))
full (fs/normalize (fs/join base path))]
(when-not (fs/exists? (fs/parent full))
(fs/create-dir (fs/parent full)))
(with-open [^InputStream src (io/input-stream content)
^OutputStream dst (io/output-stream full)]
(io/copy src dst))))
(defmethod impl/copy-object :fs
[backend src-object dst-object]
(let [base (fs/path (:directory backend))
path (fs/path (impl/id->path (:id dst-object)))
full (fs/normalize (fs/join base path))]
(when-not (fs/exists? (fs/parent full))
(fs/create-dir (fs/parent full)))
(with-open [^InputStream src (impl/get-object-data backend src-object)
^OutputStream dst (io/output-stream full)]
(io/copy src dst))))
[{:keys [executor] :as backend} {:keys [id] :as object} content]
(px/with-dispatch executor
(let [base (fs/path (:directory backend))
path (fs/path (impl/id->path id))
full (fs/normalize (fs/join base path))]
(when-not (fs/exists? (fs/parent full))
(fs/create-dir (fs/parent full)))
(with-open [^InputStream src (io/input-stream content)
^OutputStream dst (io/output-stream full)]
(io/copy src dst)))))
(defmethod impl/get-object-data :fs
[backend {:keys [id] :as object}]
(let [^Path base (fs/path (:directory backend))
^Path path (fs/path (impl/id->path id))
^Path full (fs/normalize (fs/join base path))]
(when-not (fs/exists? full)
(ex/raise :type :internal
:code :filesystem-object-does-not-exists
:path (str full)))
(io/input-stream full)))
[{:keys [executor] :as backend} {:keys [id] :as object}]
(px/with-dispatch executor
(let [^Path base (fs/path (:directory backend))
^Path path (fs/path (impl/id->path id))
^Path full (fs/normalize (fs/join base path))]
(when-not (fs/exists? full)
(ex/raise :type :internal
:code :filesystem-object-does-not-exists
:path (str full)))
(io/input-stream full))))
(defmethod impl/get-object-bytes :fs
[backend object]
(fs/slurp-bytes (impl/get-object-data backend object)))
[{:keys [executor] :as backend} object]
(px/with-dispatch executor
(fs/slurp-bytes (impl/get-object-data backend object))))
(defmethod impl/get-object-url :fs
[{:keys [uri] :as backend} {:keys [id] :as object} _]
(update uri :path
(fn [existing]
(if (str/ends-with? existing "/")
(str existing (impl/id->path id))
(str existing "/" (impl/id->path id))))))
[{:keys [uri executor] :as backend} {:keys [id] :as object} _]
(px/with-dispatch executor
(update uri :path
(fn [existing]
(if (str/ends-with? existing "/")
(str existing (impl/id->path id))
(str existing "/" (impl/id->path id)))))))
(defmethod impl/del-object :fs
[backend {:keys [id] :as object}]
(let [base (fs/path (:directory backend))
path (fs/path (impl/id->path id))
path (fs/join base path)]
(Files/deleteIfExists ^Path path)))
[{:keys [executor] :as backend} {:keys [id] :as object}]
(px/with-dispatch executor
(let [base (fs/path (:directory backend))
path (fs/path (impl/id->path id))
path (fs/join base path)]
(Files/deleteIfExists ^Path path))))
(defmethod impl/del-objects-in-bulk :fs
[backend ids]
(let [base (fs/path (:directory backend))]
(doseq [id ids]
(let [path (fs/path (impl/id->path id))
path (fs/join base path)]
(Files/deleteIfExists ^Path path)))))
[{:keys [executor] :as backend} ids]
(px/with-dispatch executor
(let [base (fs/path (:directory backend))]
(doseq [id ids]
(let [path (fs/path (impl/id->path id))
path (fs/join base path)]
(Files/deleteIfExists ^Path path))))))

View File

@@ -7,17 +7,20 @@
(ns app.storage.impl
"Storage backends abstraction layer."
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.uuid :as uuid]
[buddy.core.codecs :as bc]
[clojure.java.io :as io]
[cuerdas.core :as str])
[buddy.core.hash :as bh]
[clojure.java.io :as io])
(:import
java.nio.ByteBuffer
java.util.UUID
java.io.ByteArrayInputStream
java.io.InputStream
java.nio.file.Files))
java.nio.file.Files
org.apache.commons.io.input.BoundedInputStream
))
;; --- API Definition
@@ -29,14 +32,6 @@
:code :invalid-storage-backend
:context cfg))
(defmulti copy-object (fn [cfg _ _] (:type cfg)))
(defmethod copy-object :default
[cfg _ _]
(ex/raise :type :internal
:code :invalid-storage-backend
:context cfg))
(defmulti get-object-data (fn [cfg _] (:type cfg)))
(defmethod get-object-data :default
@@ -106,64 +101,27 @@
:code :invalid-id-type
:hint "id should be string or uuid")))
(defprotocol IContentObject
(size [_] "get object size"))
(defprotocol IContentObject)
(defprotocol IContentHash
(get-hash [_] "get precalculated hash"))
(defn- path->content
[path]
(let [size (Files/size path)]
(reify
IContentObject
io/IOFactory
(make-reader [_ opts]
(io/make-reader path opts))
(make-writer [_ opts]
(throw (UnsupportedOperationException. "not implemented")))
(make-input-stream [_ opts]
(io/make-input-stream path opts))
(make-output-stream [_ opts]
(throw (UnsupportedOperationException. "not implemented")))
clojure.lang.Counted
(count [_] size)
java.lang.AutoCloseable
(close [_]))))
(defn string->content
[^String v]
(let [data (.getBytes v "UTF-8")
bais (ByteArrayInputStream. ^bytes data)]
(reify
IContentObject
io/IOFactory
(make-reader [_ opts]
(io/make-reader bais opts))
(make-writer [_ opts]
(throw (UnsupportedOperationException. "not implemented")))
(make-input-stream [_ opts]
(io/make-input-stream bais opts))
(make-output-stream [_ opts]
(throw (UnsupportedOperationException. "not implemented")))
clojure.lang.Counted
(count [_]
(alength data))
java.lang.AutoCloseable
(close [_]))))
(defn- input-stream->content
[^InputStream is size]
(defn- make-content
[^InputStream is ^long size]
(reify
IContentObject
(size [_] size)
io/IOFactory
(make-reader [_ opts]
(io/make-reader is opts))
(make-writer [_ opts]
(make-reader [this opts]
(io/make-reader this opts))
(make-writer [_ _]
(throw (UnsupportedOperationException. "not implemented")))
(make-input-stream [_ opts]
(io/make-input-stream is opts))
(make-output-stream [_ opts]
(make-input-stream [_ _]
(doto (BoundedInputStream. is size)
(.setPropagateClose false)))
(make-output-stream [_ _]
(throw (UnsupportedOperationException. "not implemented")))
clojure.lang.Counted
@@ -178,26 +136,63 @@
([data size]
(cond
(instance? java.nio.file.Path data)
(path->content data)
(make-content (io/input-stream data)
(Files/size data))
(instance? java.io.File data)
(path->content (.toPath ^java.io.File data))
(content (.toPath ^java.io.File data) nil)
(instance? String data)
(string->content data)
(let [data (.getBytes data "UTF-8")
bais (ByteArrayInputStream. ^bytes data)]
(make-content bais (alength data)))
(bytes? data)
(input-stream->content (ByteArrayInputStream. ^bytes data) (alength ^bytes data))
(let [size (alength ^bytes data)
bais (ByteArrayInputStream. ^bytes data)]
(make-content bais size))
(instance? InputStream data)
(do
(when-not size
(throw (UnsupportedOperationException. "size should be provided on InputStream")))
(input-stream->content data size))
(make-content data size))
:else
(throw (UnsupportedOperationException. "type not supported")))))
(defn wrap-with-hash
[content ^String hash]
(when-not (satisfies? IContentObject content)
(throw (UnsupportedOperationException. "`content` should be an instance of IContentObject")))
(when-not (satisfies? io/IOFactory content)
(throw (UnsupportedOperationException. "`content` should be an instance of IOFactory")))
(reify
IContentObject
(size [_] (size content))
IContentHash
(get-hash [_] hash)
io/IOFactory
(make-reader [_ opts]
(io/make-reader content opts))
(make-writer [_ opts]
(io/make-writer content opts))
(make-input-stream [_ opts]
(io/make-input-stream content opts))
(make-output-stream [_ opts]
(io/make-output-stream content opts))
clojure.lang.Counted
(count [_] (count content))
java.lang.AutoCloseable
(close [_]
(.close ^java.lang.AutoCloseable content))))
(defn content?
[v]
(satisfies? IContentObject v))
@@ -209,15 +204,33 @@
(io/copy input output)
(.toByteArray output)))
(defn resolve-backend
[{:keys [conn pool] :as storage} backend-id]
(when backend-id
(let [backend (get-in storage [:backends backend-id])]
(when-not backend
(ex/raise :type :internal
:code :backend-not-configured
:hint (str/fmt "backend '%s' not configured" backend-id)))
(assoc backend
:conn (or conn pool)
:id backend-id))))
(defn calculate-hash
[path-or-stream]
(let [result (cond
(instance? InputStream path-or-stream)
(let [result (-> (bh/blake2b-256 path-or-stream)
(bc/bytes->hex))]
(.reset path-or-stream)
result)
(string? path-or-stream)
(-> (bh/blake2b-256 path-or-stream)
(bc/bytes->hex))
:else
(with-open [is (io/input-stream path-or-stream)]
(-> (bh/blake2b-256 is)
(bc/bytes->hex))))]
(str "blake2b:" result)))
(defn resolve-backend
[{:keys [conn pool executor] :as storage} backend-id]
(let [backend (get-in storage [:backends backend-id])]
(when-not backend
(ex/raise :type :internal
:code :backend-not-configured
:hint (dm/fmt "backend '%' not configured" backend-id)))
(assoc backend
:executor executor
:conn (or conn pool)
:id backend-id)))

View File

@@ -13,35 +13,42 @@
[app.common.uri :as u]
[app.storage.impl :as impl]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[integrant.core :as ig])
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px])
(:import
java.time.Duration
java.io.InputStream
java.nio.ByteBuffer
java.time.Duration
java.util.Collection
software.amazon.awssdk.core.sync.RequestBody
java.util.Optional
java.util.concurrent.Semaphore
org.reactivestreams.Subscriber
org.reactivestreams.Subscription
software.amazon.awssdk.core.ResponseBytes
;; software.amazon.awssdk.core.ResponseInputStream
software.amazon.awssdk.core.async.AsyncRequestBody
software.amazon.awssdk.core.client.config.ClientAsyncConfiguration
software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption
software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
software.amazon.awssdk.regions.Region
software.amazon.awssdk.services.s3.S3Client
software.amazon.awssdk.services.s3.S3AsyncClient
software.amazon.awssdk.services.s3.model.Delete
software.amazon.awssdk.services.s3.model.CopyObjectRequest
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
software.amazon.awssdk.services.s3.model.DeleteObjectsResponse
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
software.amazon.awssdk.services.s3.model.GetObjectRequest
software.amazon.awssdk.services.s3.model.ObjectIdentifier
software.amazon.awssdk.services.s3.model.PutObjectRequest
;; software.amazon.awssdk.services.s3.model.GetObjectResponse
software.amazon.awssdk.services.s3.model.S3Error
software.amazon.awssdk.services.s3.presigner.S3Presigner
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest
))
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest))
(declare put-object)
(declare copy-object)
(declare get-object-bytes)
(declare get-object-data)
(declare get-object-url)
@@ -55,9 +62,10 @@
(s/def ::region #{:eu-central-1})
(s/def ::bucket ::us/string)
(s/def ::prefix ::us/string)
(s/def ::endpoint ::us/string)
(defmethod ig/pre-init-spec ::backend [_]
(s/keys :opt-un [::region ::bucket ::prefix]))
(s/keys :opt-un [::region ::bucket ::prefix ::endpoint ::wrk/executor]))
(defmethod ig/prep-key ::backend
[_ {:keys [prefix] :as cfg}]
@@ -73,12 +81,18 @@
(let [client (build-s3-client cfg)
presigner (build-s3-presigner cfg)]
(assoc cfg
:client client
:client @client
:presigner presigner
:type :s3))))
:type :s3
::close-fn #(.close ^java.lang.AutoCloseable client)))))
(defmethod ig/halt-key! ::backend
[_ {:keys [::close-fn]}]
(when (fn? close-fn)
(px/run! close-fn)))
(s/def ::type ::us/keyword)
(s/def ::client #(instance? S3Client %))
(s/def ::client #(instance? S3AsyncClient %))
(s/def ::presigner #(instance? S3Presigner %))
(s/def ::backend
(s/keys :req-un [::region ::bucket ::client ::type ::presigner]
@@ -90,10 +104,6 @@
[backend object content]
(put-object backend object content))
(defmethod impl/copy-object :s3
[backend src-object dst-object]
(copy-object backend src-object dst-object))
(defmethod impl/get-object-data :s3
[backend object]
(get-object-data backend object))
@@ -116,75 +126,134 @@
;; --- HELPERS
(def default-eventloop-threads 4)
(def default-timeout
(dt/duration {:seconds 30}))
(defn- ^Region lookup-region
[region]
(case region
:eu-central-1 Region/EU_CENTRAL_1))
(Region/of (name region)))
(defn build-s3-client
[{:keys [region]}]
(.. (S3Client/builder)
(region (lookup-region region))
(build)))
[{:keys [region endpoint executor]}]
(let [hclient (.. (NettyNioAsyncHttpClient/builder)
(eventLoopGroupBuilder (.. (SdkEventLoopGroup/builder)
(numberOfThreads (int default-eventloop-threads))))
(connectionAcquisitionTimeout default-timeout)
(connectionTimeout default-timeout)
(readTimeout default-timeout)
(writeTimeout default-timeout)
(build))
client (.. (S3AsyncClient/builder)
(asyncConfiguration (.. (ClientAsyncConfiguration/builder)
(advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR
executor)
(build)))
(httpClient hclient)
(region (lookup-region region)))]
(when-let [uri (some-> endpoint (java.net.URI.))]
(.endpointOverride client uri))
(let [client (.build client)]
(reify
clojure.lang.IDeref
(deref [_] client)
java.lang.AutoCloseable
(close [_]
(.close hclient)
(.close client))))))
(defn build-s3-presigner
[{:keys [region]}]
(.. (S3Presigner/builder)
(region (lookup-region region))
(build)))
[{:keys [region endpoint]}]
(if (string? endpoint)
(let [uri (java.net.URI. endpoint)]
(.. (S3Presigner/builder)
(endpointOverride uri)
(region (lookup-region region))
(build)))
(.. (S3Presigner/builder)
(region (lookup-region region))
(build))))
(defn- make-request-body
[content]
(let [is (io/input-stream content)
buff-size (* 1024 64)
sem (Semaphore. 0)
writer-fn (fn [s]
(try
(loop []
(.acquire sem 1)
(let [buffer (byte-array buff-size)
readed (.read is buffer)]
(when (pos? readed)
(.onNext ^Subscriber s (ByteBuffer/wrap buffer 0 readed))
(when (= readed buff-size)
(recur)))))
(.onComplete s)
(catch Throwable cause
(.onError s cause))
(finally
(.close ^InputStream is))))]
(reify
AsyncRequestBody
(contentLength [_]
(Optional/of (long (count content))))
(^void subscribe [_ ^Subscriber s]
(let [thread (Thread. #(writer-fn s))]
(.setDaemon thread true)
(.setName thread "penpot/storage:s3")
(.start thread)
(.onSubscribe s (reify Subscription
(cancel [_]
(.interrupt thread)
(.release sem 1))
(request [_ n]
(.release sem (int n))))))))))
(defn put-object
[{:keys [client bucket prefix]} {:keys [id] :as object} content]
(let [path (str prefix (impl/id->path id))
mdata (meta object)
mtype (:content-type mdata "application/octet-stream")
request (.. (PutObjectRequest/builder)
(bucket bucket)
(contentType mtype)
(key path)
(build))]
(p/let [path (str prefix (impl/id->path id))
mdata (meta object)
mtype (:content-type mdata "application/octet-stream")
request (.. (PutObjectRequest/builder)
(bucket bucket)
(contentType mtype)
(key path)
(build))]
(with-open [^InputStream is (io/input-stream content)]
(let [content (RequestBody/fromInputStream is (count content))]
(.putObject ^S3Client client
^PutObjectRequest request
^RequestBody content)))))
(defn copy-object
[{:keys [client bucket prefix]} src-object dst-object]
(let [source-path (str prefix (impl/id->path (:id src-object)))
source-mdata (meta src-object)
source-mtype (:content-type source-mdata "application/octet-stream")
dest-path (str prefix (impl/id->path (:id dst-object)))
request (.. (CopyObjectRequest/builder)
(copySource (u/query-encode (str bucket "/" source-path)))
(destinationBucket bucket)
(destinationKey dest-path)
(contentType source-mtype)
(build))]
(.copyObject ^S3Client client ^CopyObjectRequest request)))
(let [content (make-request-body content)]
(.putObject ^S3AsyncClient client
^PutObjectRequest request
^AsyncRequestBody content))))
(defn get-object-data
[{:keys [client bucket prefix]} {:keys [id]}]
(let [gor (.. (GetObjectRequest/builder)
(bucket bucket)
(key (str prefix (impl/id->path id)))
(build))
obj (.getObject ^S3Client client ^GetObjectRequest gor)
;; rsp (.response ^ResponseInputStream obj)
;; len (.contentLength ^GetObjectResponse rsp)
]
(p/let [gor (.. (GetObjectRequest/builder)
(bucket bucket)
(key (str prefix (impl/id->path id)))
(build))
obj (.getObject ^S3AsyncClient client ^GetObjectRequest gor)
;; rsp (.response ^ResponseInputStream obj)
;; len (.contentLength ^GetObjectResponse rsp)
]
(io/input-stream obj)))
(defn get-object-bytes
[{:keys [client bucket prefix]} {:keys [id]}]
(let [gor (.. (GetObjectRequest/builder)
(bucket bucket)
(key (str prefix (impl/id->path id)))
(build))
obj (.getObjectAsBytes ^S3Client client ^GetObjectRequest gor)]
(p/let [gor (.. (GetObjectRequest/builder)
(bucket bucket)
(key (str prefix (impl/id->path id)))
(build))
obj (.getObjectAsBytes ^S3AsyncClient client ^GetObjectRequest gor)]
(.asByteArray ^ResponseBytes obj)))
(def default-max-age
@@ -193,44 +262,48 @@
(defn get-object-url
[{:keys [presigner bucket prefix]} {:keys [id]} {:keys [max-age] :or {max-age default-max-age}}]
(us/assert dt/duration? max-age)
(let [gor (.. (GetObjectRequest/builder)
(bucket bucket)
(key (str prefix (impl/id->path id)))
(build))
gopr (.. (GetObjectPresignRequest/builder)
(signatureDuration ^Duration max-age)
(getObjectRequest ^GetObjectRequest gor)
(build))
pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)]
(u/uri (str (.url ^PresignedGetObjectRequest pgor)))))
(p/do
(let [gor (.. (GetObjectRequest/builder)
(bucket bucket)
(key (str prefix (impl/id->path id)))
(build))
gopr (.. (GetObjectPresignRequest/builder)
(signatureDuration ^Duration max-age)
(getObjectRequest ^GetObjectRequest gor)
(build))
pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)]
(u/uri (str (.url ^PresignedGetObjectRequest pgor))))))
(defn del-object
[{:keys [bucket client prefix]} {:keys [id] :as obj}]
(let [dor (.. (DeleteObjectRequest/builder)
(bucket bucket)
(key (str prefix (impl/id->path id)))
(build))]
(.deleteObject ^S3Client client
(p/let [dor (.. (DeleteObjectRequest/builder)
(bucket bucket)
(key (str prefix (impl/id->path id)))
(build))]
(.deleteObject ^S3AsyncClient client
^DeleteObjectRequest dor)))
(defn del-object-in-bulk
[{:keys [bucket client prefix]} ids]
(let [oids (map (fn [id]
(.. (ObjectIdentifier/builder)
(key (str prefix (impl/id->path id)))
(build)))
ids)
delc (.. (Delete/builder)
(objects ^Collection oids)
(build))
dor (.. (DeleteObjectsRequest/builder)
(bucket bucket)
(delete ^Delete delc)
(build))
dres (.deleteObjects ^S3Client client
^DeleteObjectsRequest dor)]
(p/let [oids (map (fn [id]
(.. (ObjectIdentifier/builder)
(key (str prefix (impl/id->path id)))
(build)))
ids)
delc (.. (Delete/builder)
(objects ^Collection oids)
(build))
dor (.. (DeleteObjectsRequest/builder)
(bucket bucket)
(delete ^Delete delc)
(build))
dres (.deleteObjects ^S3AsyncClient client
^DeleteObjectsRequest dor)]
(when (.hasErrors ^DeleteObjectsResponse dres)
(let [errors (seq (.errors ^DeleteObjectsResponse dres))]
(ex/raise :type :s3-error
:code :error-on-bulk-delete
:context errors)))))
(ex/raise :type :internal
:code :error-on-s3-bulk-delete
:s3-errors (mapv (fn [^S3Error error]
{:key (.key error)
:msg (.message error)})
errors))))))

View File

@@ -0,0 +1,170 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.tasks.file-gc
"A maintenance task that is responsible of: purge unused file media,
clean unused object thumbnails and remove old file thumbnails. The
file is eligible to be garbage collected after some period of
inactivity (the default threshold is 72h)."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.pages.helpers :as cph]
[app.common.pages.migrations :as pmg]
[app.db :as db]
[app.util.blob :as blob]
[app.util.time :as dt]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]))
(declare ^:private retrieve-candidates)
(declare ^:private process-file)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HANDLER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::max-age ::dt/duration)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::max-age]))
(defmethod ig/init-key ::handler
[_ {:keys [pool] :as cfg}]
(fn [_]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)]
(loop [total 0
files (retrieve-candidates cfg)]
(if-let [file (first files)]
(do
(process-file cfg file)
(recur (inc total)
(rest files)))
(do
(l/debug :msg "finished processing files" :processed total)
{:processed total})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private
sql:retrieve-candidates-chunk
"select f.id,
f.data,
f.revn,
f.modified_at
from file as f
where f.has_media_trimmed is false
and f.modified_at < now() - ?::interval
and f.modified_at < ?
order by f.modified_at desc
limit 1
for update skip locked")
(defn- retrieve-candidates
[{:keys [conn max-age] :as cfg}]
(let [interval (db/interval max-age)
get-chunk
(fn [cursor]
(let [rows (db/exec! conn [sql:retrieve-candidates-chunk interval cursor])]
[(some->> rows peek :modified-at) (seq rows)]))]
(sequence cat (d/iteration get-chunk
:vf second
:kf first
:initk (dt/now)))))
(defn- collect-used-media
[data]
(let [xform (comp
(map :objects)
(mapcat vals)
(keep (fn [{:keys [type] :as obj}]
(case type
:path (get-in obj [:fill-image :id])
:image (get-in obj [:metadata :id])
nil))))
pages (concat
(vals (:pages-index data))
(vals (:components data)))]
(-> #{}
(into xform pages)
(into (keys (:media data))))))
(defn- clean-file-media!
"Performs the garbage collection of file media objects."
[conn file-id data]
(let [used (collect-used-media data)
unused (->> (db/query conn :file-media-object {:file-id file-id})
(remove #(contains? used (:id %))))]
(doseq [mobj unused]
(l/debug :hint "delete file media object"
:id (:id mobj)
:media-id (:media-id mobj)
:thumbnail-id (:thumbnail-id mobj))
;; NOTE: deleting the file-media-object in the database
;; automatically marks as touched the referenced storage
;; objects. The touch mechanism is needed because many files can
;; point to the same storage objects and we can't just delete
;; them.
(db/delete! conn :file-media-object {:id (:id mobj)}))))
(defn- clean-file-frame-thumbnails!
[conn file-id data]
(let [stored (->> (db/query conn :file-object-thumbnail
{:file-id file-id}
{:columns [:object-id]})
(into #{} (map :object-id)))
get-objects-ids
(fn [{:keys [id objects]}]
(->> (cph/get-frames objects)
(map #(str id (:id %)))))
using (into #{}
(mapcat get-objects-ids)
(vals (:pages-index data)))
unused (set/difference stored using)]
(when (seq unused)
(let [sql (str/concat
"delete from file_object_thumbnail "
" where file_id=? and object_id=ANY(?)")
res (db/exec-one! conn [sql file-id (db/create-array conn "text" unused)])]
(l/debug :hint "delete object thumbnails" :total (:next.jdbc/update-count res))))))
(defn- clean-file-thumbnails!
[conn file-id revn]
(let [sql (str "delete from file_thumbnail "
" where file_id=? and revn < ?")
res (db/exec-one! conn [sql file-id revn])]
(l/debug :hint "delete file thumbnails" :total (:next.jdbc/update-count res))))
(defn- process-file
[{:keys [conn] :as cfg} {:keys [id data revn modified-at] :as file}]
(l/debug :hint "processing file" :id id :modified-at modified-at)
(let [data (-> (blob/decode data)
(assoc :id id)
(pmg/migrate-data))]
(clean-file-media! conn id data)
(clean-file-frame-thumbnails! conn id data)
(clean-file-thumbnails! conn id revn)
;; Mark file as trimmed
(db/update! conn :file
{:has-media-trimmed true}
{:id id})
nil))

View File

@@ -1,116 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.tasks.file-media-gc
"A maintenance task that is responsible to purge the unused media
objects from files. A file is ellegible to be garbage collected
after some period of inactivity (the default threshold is 72h)."
(:require
[app.common.logging :as l]
[app.common.pages.migrations :as pmg]
[app.db :as db]
[app.util.blob :as blob]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(declare process-file)
(declare retrieve-candidates)
(s/def ::max-age ::dt/duration)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::max-age]))
(defmethod ig/init-key ::handler
[_ {:keys [pool] :as cfg}]
(fn [_]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)]
(loop [n 0]
(let [files (retrieve-candidates cfg)]
(if (seq files)
(do
(run! (partial process-file cfg) files)
(recur (+ n (count files))))
(do
(l/debug :msg "finished processing files" :processed n)
{:processed n}))))))))
(def ^:private
sql:retrieve-candidates-chunk
"select f.id,
f.data,
extract(epoch from (now() - f.modified_at))::bigint as age
from file as f
where f.has_media_trimmed is false
and f.modified_at < now() - ?::interval
order by f.modified_at asc
limit 10
for update skip locked")
(defn- retrieve-candidates
[{:keys [conn max-age] :as cfg}]
(let [interval (db/interval max-age)]
(->> (db/exec! conn [sql:retrieve-candidates-chunk interval])
(mapv (fn [{:keys [age] :as row}]
(assoc row :age (dt/duration {:seconds age})))))))
(def ^:private
collect-media-xf
(comp
(map :objects)
(mapcat vals)
(map (fn [{:keys [type] :as obj}]
(case type
:path (get-in obj [:fill-image :id])
:image (get-in obj [:metadata :id])
nil)))
(filter uuid?)))
(defn- collect-used-media
[data]
(let [pages (concat
(vals (:pages-index data))
(vals (:components data)))]
(-> #{}
(into collect-media-xf pages)
(into (keys (:media data))))))
(defn- process-file
[{:keys [conn] :as cfg} {:keys [id data age] :as file}]
(let [data (-> (blob/decode data)
(assoc :id id)
(pmg/migrate-data))
used (collect-used-media data)
unused (->> (db/query conn :file-media-object {:file-id id})
(remove #(contains? used (:id %))))]
(l/debug :action "processing file"
:id id
:age age
:to-delete (count unused))
;; Mark file as trimmed
(db/update! conn :file
{:has-media-trimmed true}
{:id id})
(doseq [mobj unused]
(l/debug :action "deleting media object"
:id (:id mobj)
:media-id (:media-id mobj)
:thumbnail-id (:thumbnail-id mobj))
;; NOTE: deleting the file-media-object in the database
;; automatically marks as toched the referenced storage
;; objects. The touch mechanism is needed because many files can
;; point to the same storage objects and we can't just delete
;; them.
(db/delete! conn :file-media-object {:id (:id mobj)}))
nil))

View File

@@ -29,7 +29,7 @@
(defn- offload-candidate
[{:keys [storage conn backend] :as cfg} {:keys [id data] :as file}]
(l/debug :action "offload file data" :id id)
(l/debug :hint "offload file data" :id id)
(let [backend (simpl/resolve-backend storage backend)]
(->> (simpl/content data)
(simpl/put-object backend file))

View File

@@ -28,7 +28,8 @@
(let [interval (db/interval max-age)
result (db/exec-one! conn [sql:delete-files-xlog interval])
result (:next.jdbc/update-count result)]
(l/debug :removed result :hint "remove old file changes")
(l/info :hint "remove old file changes"
:removed result)
result))))
(def ^:private

View File

@@ -9,10 +9,9 @@
of deleted objects."
(:require
[app.common.logging :as l]
[app.config :as cf]
[app.db :as db]
[app.media :as media]
[app.storage :as sto]
[app.storage.impl :as simpl]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
@@ -48,24 +47,19 @@
result (db/exec! conn [sql max-age])]
(doseq [{:keys [id] :as item} result]
(l/trace :action "delete object" :table table :id id))
(l/trace :hint "delete object" :table table :id id))
(count result)))
;; --- IMPL: file deletion
(defmethod delete-objects "file"
[{:keys [conn max-age table storage] :as cfg}]
(let [sql (str/fmt sql:delete-objects
{:table table :limit 50})
result (db/exec! conn [sql max-age])
backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))]
[{:keys [conn max-age table] :as cfg}]
(let [sql (str/fmt sql:delete-objects {:table table :limit 50})
result (db/exec! conn [sql max-age])]
(doseq [{:keys [id] :as item} result]
(l/trace :action "delete object" :table table :id id)
(when backend
(simpl/del-object backend item)))
(l/trace :hint "delete object" :table table :id id))
(count result)))
@@ -76,13 +70,13 @@
(let [sql (str/fmt sql:delete-objects
{:table table :limit 50})
fonts (db/exec! conn [sql max-age])
storage (assoc storage :conn conn)]
storage (media/configure-assets-storage storage conn)]
(doseq [{:keys [id] :as font} fonts]
(l/trace :action "delete object" :table table :id id)
(some->> (:woff1-file-id font) (sto/del-object storage))
(some->> (:woff2-file-id font) (sto/del-object storage))
(some->> (:otf-file-id font) (sto/del-object storage))
(some->> (:ttf-file-id font) (sto/del-object storage)))
(l/trace :hint "delete object" :table table :id id)
(some->> (:woff1-file-id font) (sto/touch-object! storage) deref)
(some->> (:woff2-file-id font) (sto/touch-object! storage) deref)
(some->> (:otf-file-id font) (sto/touch-object! storage) deref)
(some->> (:ttf-file-id font) (sto/touch-object! storage) deref))
(count fonts)))
;; --- IMPL: team deletion
@@ -95,8 +89,8 @@
storage (assoc storage :conn conn)]
(doseq [{:keys [id] :as team} teams]
(l/trace :action "delete object" :table table :id id)
(some->> (:photo-id team) (sto/del-object storage)))
(l/trace :hint "delete object" :table table :id id)
(some->> (:photo-id team) (sto/touch-object! storage) deref))
(count teams)))
@@ -127,15 +121,15 @@
storage (assoc storage :conn conn)]
(doseq [{:keys [id] :as profile} profiles]
(l/trace :action "delete object" :table table :id id)
(l/trace :hint "delete object" :table table :id id)
;; Mark the owned teams as deleted; this enables them to be procesed
;; Mark the owned teams as deleted; this enables them to be processed
;; in the same transaction in the "team" table step.
(db/exec-one! conn [sql:mark-owned-teams-deleted id max-age])
;; Mark as deleted the storage object related with the photo-id
;; field.
(some->> (:photo-id profile) (sto/del-object storage))
(some->> (:photo-id profile) (sto/touch-object! storage) deref)
;; And finally, permanently delete the profile.
(db/delete! conn :profile {:id id}))

View File

@@ -28,7 +28,7 @@
(let [interval (db/interval max-age)
result (db/exec-one! conn [sql:delete-completed-tasks interval])
result (:next.jdbc/update-count result)]
(l/debug :action "trim completed tasks table" :removed result)
(l/debug :hint "trim completed tasks table" :removed result)
result))))
(def ^:private

View File

@@ -5,25 +5,29 @@
;; Copyright (c) UXBOX Labs SL
(ns app.tasks.telemetry
"A task that is reponsible to collect anonymous statistical
"A task that is responsible to collect anonymous statistical
information about the current instance and send it to the telemetry
server."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cfg]
[app.config :as cf]
[app.db :as db]
[app.util.http :as http]
[app.util.async :refer [thread-sleep]]
[app.util.json :as json]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(declare handler)
(declare acquire-lock)
(declare release-all-locks)
(declare retrieve-stats)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TASK ENTRY POINT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare get-stats)
(declare send!)
(declare get-subscriptions)
(s/def ::http-client fn?)
(s/def ::version ::us/string)
(s/def ::uri ::us/string)
(s/def ::instance-id ::us/uuid)
@@ -31,52 +35,104 @@
(s/keys :req-un [::instance-id]))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::version ::uri ::sprops]))
(s/keys :req-un [::db/pool ::http-client ::version ::uri ::sprops]))
(defmethod ig/init-key ::handler
[_ {:keys [pool] :as cfg}]
(fn [_]
(db/with-atomic [conn pool]
(try
(acquire-lock conn)
(handler (assoc cfg :conn conn))
(finally
(release-all-locks conn))))))
[_ {:keys [pool sprops version] :as cfg}]
(fn [{:keys [send? enabled?] :or {send? true enabled? false}}]
(let [subs (get-subscriptions pool)
enabled? (or enabled?
(contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
(defn- acquire-lock
[conn]
(db/exec-one! conn ["select pg_advisory_lock(87562985867332);"]))
data {:subscriptions subs
:version version
:instance-id (:instance-id sprops)}]
(cond
;; If we have telemetry enabled, then proceed the normal
;; operation.
enabled?
(let [data (merge data (get-stats pool))]
(when send?
(thread-sleep (rand-int 10000))
(send! cfg data))
data)
(defn- release-all-locks
[conn]
(db/exec-one! conn ["select pg_advisory_unlock_all();"]))
;; If we have telemetry disabled, but there are users that are
;; explicitly checked the newsletter subscription on the
;; onboarding dialog or the profile section, then proceed to
;; send a limited telemetry data, that consists in the list of
;; subscribed emails and the running penpot version.
(seq subs)
(do
(when send?
(thread-sleep (rand-int 10000))
(send! cfg data))
data)
(defn- handler
[{:keys [sprops] :as cfg}]
(let [instance-id (:instance-id sprops)
data (retrieve-stats cfg)
data (assoc data :instance-id instance-id)
response (http/send! {:method :post
:uri (:uri cfg)
:headers {"content-type" "application/json"}
:body (json/encode-str data)})]
:else
data))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- send!
[{:keys [http-client uri] :as cfg} data]
(let [response (http-client {:method :post
:uri uri
:headers {"content-type" "application/json"}
:body (json/write-str data)}
{:sync? true})]
(when (> (:status response) 206)
(ex/raise :type :internal
:code :invalid-response
:context {:status (:status response)
:body (:body response)}))))
:response-status (:status response)
:response-body (:body response)))))
(defn retrieve-num-teams
(defn- get-subscriptions
[conn]
(let [sql "select email from profile where props->>'~:newsletter-subscribed' = 'true'"]
(->> (db/exec! conn [sql])
(mapv :email))))
(defn- retrieve-num-teams
[conn]
(-> (db/exec-one! conn ["select count(*) as count from team;"]) :count))
(defn retrieve-num-projects
(defn- retrieve-num-projects
[conn]
(-> (db/exec-one! conn ["select count(*) as count from project;"]) :count))
(defn retrieve-num-files
(defn- retrieve-num-files
[conn]
(-> (db/exec-one! conn ["select count(*) as count from project;"]) :count))
(-> (db/exec-one! conn ["select count(*) as count from file;"]) :count))
(defn- retrieve-num-file-changes
[conn]
(let [sql (str "select count(*) as count "
" from file_change "
" where date_trunc('day', created_at) = date_trunc('day', now())")]
(-> (db/exec-one! conn [sql]) :count)))
(defn- retrieve-num-touched-files
[conn]
(let [sql (str "select count(distinct file_id) as count "
" from file_change "
" where date_trunc('day', created_at) = date_trunc('day', now())")]
(-> (db/exec-one! conn [sql]) :count)))
(defn- retrieve-num-users
[conn]
(-> (db/exec-one! conn ["select count(*) as count from profile;"]) :count))
(defn- retrieve-num-fonts
[conn]
(-> (db/exec-one! conn ["select count(*) as count from team_font_variant;"]) :count))
(defn- retrieve-num-comments
[conn]
(-> (db/exec-one! conn ["select count(*) as count from comment;"]) :count))
(def sql:team-averages
"with projects_by_team as (
@@ -98,7 +154,6 @@
select t.id, count(tp.profile_id) as num_users
from team as t
left join team_profile_rel as tp on(tp.team_id = t.id)
where t.is_default = false
group by 1
)
select (select avg(num_projects)::integer from projects_by_team) as avg_projects_on_team,
@@ -110,30 +165,51 @@
(select avg(num_users)::integer from users_by_team) as avg_users_on_team,
(select max(num_users)::integer from users_by_team) as max_users_on_team;")
(defn retrieve-team-averages
(defn- retrieve-team-averages
[conn]
(->> [sql:team-averages]
(db/exec-one! conn)))
(defn retrieve-jvm-stats
(defn- retrieve-enabled-auth-providers
[conn]
(let [sql (str "select auth_backend as backend, count(*) as total "
" from profile group by 1")
rows (db/exec! conn [sql])]
(->> rows
(map (fn [{:keys [backend total]}]
(let [backend (or backend "penpot")]
[(keyword (str "auth-backend-" backend))
total])))
(into {}))))
(defn- retrieve-jvm-stats
[]
(let [^Runtime runtime (Runtime/getRuntime)]
{:jvm-heap-current (.totalMemory runtime)
:jvm-heap-max (.maxMemory runtime)
:jvm-cpus (.availableProcessors runtime)}))
:jvm-cpus (.availableProcessors runtime)
:os-arch (System/getProperty "os.arch")
:os-name (System/getProperty "os.name")
:os-version (System/getProperty "os.version")
:user-tz (System/getProperty "user.timezone")}))
(defn- retrieve-stats
[{:keys [conn version]}]
(let [referer (if (cfg/get :telemetry-with-taiga)
(defn get-stats
[conn]
(let [referer (if (cf/get :telemetry-with-taiga)
"taiga"
(cfg/get :telemetry-referer))]
(-> {:version version
:referer referer
(cf/get :telemetry-referer))]
(-> {:referer referer
:total-teams (retrieve-num-teams conn)
:total-projects (retrieve-num-projects conn)
:total-files (retrieve-num-files conn)}
:total-files (retrieve-num-files conn)
:total-users (retrieve-num-users conn)
:total-fonts (retrieve-num-fonts conn)
:total-comments (retrieve-num-comments conn)
:total-file-changes (retrieve-num-file-changes conn)
:total-touched-files (retrieve-num-touched-files conn)}
(d/merge
(retrieve-team-averages conn)
(retrieve-jvm-stats))
(retrieve-jvm-stats)
(retrieve-enabled-auth-providers conn))
(d/without-nils))))

View File

@@ -7,6 +7,7 @@
(ns app.tokens
"Tokens generation service."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.transit :as t]
@@ -17,7 +18,7 @@
(defn- generate
[cfg claims]
(let [payload (t/encode claims)]
(let [payload (-> claims d/without-nils t/encode)]
(jwe/encrypt payload (::secret cfg) {:alg :a256kw :enc :a256gcm})))
(defn- verify

View File

@@ -38,6 +38,13 @@
(throw r#)
r#)))
(defmacro with-closing
[ch & body]
`(try
~@body
(finally
(some-> ~ch a/close!))))
(defn thread-call
[^Executor executor f]
(let [c (a/chan 1)]
@@ -54,7 +61,6 @@
(a/close! c)
c))))
(defmacro with-thread
[executor & body]
(if (= executor ::default)

View File

@@ -10,6 +10,7 @@
(:require
[app.common.transit :as t]
[app.config :as cf]
[app.util.fressian :as fres]
[taoensso.nippy :as n])
(:import
java.io.ByteArrayInputStream
@@ -21,23 +22,28 @@
net.jpountz.lz4.LZ4FastDecompressor
net.jpountz.lz4.LZ4Compressor))
(set! *warn-on-reflection* true)
(def lz4-factory (LZ4Factory/fastestInstance))
(declare decode-v1)
(declare decode-v2)
(declare decode-v3)
(declare decode-v4)
(declare encode-v1)
(declare encode-v2)
(declare encode-v3)
(declare encode-v4)
(defn encode
([data] (encode data nil))
([data {:keys [version]}]
(let [version (or version (cf/get :default-blob-version 1))]
(let [version (or version (cf/get :default-blob-version 3))]
(case (long version)
1 (encode-v1 data)
2 (encode-v2 data)
3 (encode-v3 data)
4 (encode-v4 data)
(throw (ex-info "unsupported version" {:version version}))))))
(defn decode
@@ -51,6 +57,7 @@
1 (decode-v1 data ulen)
2 (decode-v2 data ulen)
3 (decode-v3 data ulen)
4 (decode-v4 data ulen)
(throw (ex-info "unsupported version" {:version version}))))))
;; --- IMPL
@@ -122,3 +129,26 @@
(Zstd/decompressByteArray ^bytes udata 0 ulen
^bytes cdata 6 (- (alength cdata) 6))
(t/decode udata {:type :json})))
(defn- encode-v4
[data]
(let [data (fres/encode data)
dlen (alength ^bytes data)
mlen (Zstd/compressBound dlen)
cdata (byte-array mlen)
clen (Zstd/compressByteArray ^bytes cdata 0 mlen
^bytes data 0 dlen
0)]
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
^DataOutputStream dos (DataOutputStream. baos)]
(.writeShort dos (short 4)) ;; version number
(.writeInt dos (int dlen))
(.write dos ^bytes cdata (int 0) clen)
(.toByteArray baos))))
(defn- decode-v4
[^bytes cdata ^long ulen]
(let [udata (byte-array ulen)]
(Zstd/decompressByteArray ^bytes udata 0 ulen
^bytes cdata 6 (- (alength cdata) 6))
(fres/decode udata)))

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