mirror of
https://github.com/penpot/penpot.git
synced 2025-12-31 02:18:40 -05:00
Compare commits
842 Commits
STAGING
...
juan-compo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c565915007 | ||
|
|
2de6b6460e | ||
|
|
f905dfc699 | ||
|
|
c79f110177 | ||
|
|
f644b3744a | ||
|
|
0722af3a2f | ||
|
|
b4c6bbb191 | ||
|
|
cad9d03ca1 | ||
|
|
1d6389a3eb | ||
|
|
913a8d3148 | ||
|
|
34e3453f24 | ||
|
|
6f362f9211 | ||
|
|
979b4276ca | ||
|
|
a32fe40528 | ||
|
|
b602df549e | ||
|
|
7f1ab08ec8 | ||
|
|
1263ea11fa | ||
|
|
ce26c52b30 | ||
|
|
5c8b3ac3d6 | ||
|
|
bd4d576172 | ||
|
|
e10169b3db | ||
|
|
f119a9548d | ||
|
|
c097aef152 | ||
|
|
000fa51c73 | ||
|
|
d815494ffa | ||
|
|
a25ba6b482 | ||
|
|
e8434c3370 | ||
|
|
7cf4ec2792 | ||
|
|
365ce25996 | ||
|
|
01ef55e4f4 | ||
|
|
3b81c1d750 | ||
|
|
40b34da788 | ||
|
|
732c79b7b5 | ||
|
|
d0f34f06a9 | ||
|
|
23d5bdd20b | ||
|
|
9f2dc06c95 | ||
|
|
62563d28d0 | ||
|
|
21e2ee9904 | ||
|
|
e6c418eb9c | ||
|
|
de5ff227d2 | ||
|
|
0f67730198 | ||
|
|
3da02e2b6b | ||
|
|
ab80021fb1 | ||
|
|
f31e9b8ac9 | ||
|
|
7d16515eb7 | ||
|
|
cd9ba482e3 | ||
|
|
dff1ca23d3 | ||
|
|
c363d4d937 | ||
|
|
de25a24a6d | ||
|
|
accc9a173f | ||
|
|
2d364dde5c | ||
|
|
c892a9f254 | ||
|
|
aaae35fb51 | ||
|
|
960b76f760 | ||
|
|
d921e7eaa3 | ||
|
|
49f06b25fa | ||
|
|
5ffb7ae2ec | ||
|
|
27945ace65 | ||
|
|
e39bf0b439 | ||
|
|
deee7f7334 | ||
|
|
20d61cbce2 | ||
|
|
9ad8d3fd08 | ||
|
|
4c35571336 | ||
|
|
37679b7ec6 | ||
|
|
194eded930 | ||
|
|
4e607d8da2 | ||
|
|
f5fd978a07 | ||
|
|
b28be62845 | ||
|
|
d76a5c615c | ||
|
|
03e05da41e | ||
|
|
5f886e141a | ||
|
|
021b8f81ca | ||
|
|
f32112544e | ||
|
|
27e311277a | ||
|
|
b9030fcc73 | ||
|
|
e1519f0ee4 | ||
|
|
7fefe6dbc8 | ||
|
|
fdf70ae9c1 | ||
|
|
528315b75c | ||
|
|
42d03a0325 | ||
|
|
0346c48b03 | ||
|
|
1d54fe2e24 | ||
|
|
255f5af2e3 | ||
|
|
eea65b12dd | ||
|
|
d4b7f231c7 | ||
|
|
473066cf5c | ||
|
|
5e84bda404 | ||
|
|
c1058c7fdb | ||
|
|
9d907071aa | ||
|
|
c32b94abcf | ||
|
|
9d8ad0ea6e | ||
|
|
2b1e107a44 | ||
|
|
2196318cfc | ||
|
|
b3d1701698 | ||
|
|
042bd03beb | ||
|
|
a39a127f03 | ||
|
|
bd665f70bf | ||
|
|
e184a9a8b9 | ||
|
|
9b90236b72 | ||
|
|
bf6cdf729d | ||
|
|
361bdb4a04 | ||
|
|
3827aa6bd4 | ||
|
|
adf7b0df50 | ||
|
|
97b4491a27 | ||
|
|
015bd9e453 | ||
|
|
49d5987b15 | ||
|
|
a5e4de97e3 | ||
|
|
378be9473d | ||
|
|
412cf61d7d | ||
|
|
754a1b6fa2 | ||
|
|
a4ada6dc8a | ||
|
|
ec94d08f4a | ||
|
|
b6b2d28464 | ||
|
|
32770c685a | ||
|
|
441dc33e38 | ||
|
|
3f87e768a7 | ||
|
|
09e9340ba6 | ||
|
|
d5ff7b4144 | ||
|
|
ef0aee0a09 | ||
|
|
1e9682376e | ||
|
|
11b75408fe | ||
|
|
59f7ede4ff | ||
|
|
c9b61745a0 | ||
|
|
8954b05d76 | ||
|
|
974b76d7bd | ||
|
|
f505fcfa0d | ||
|
|
e4d610d503 | ||
|
|
cb4c155b32 | ||
|
|
0b346e02ff | ||
|
|
5c23a678cc | ||
|
|
946f641917 | ||
|
|
fb3923924b | ||
|
|
c882e8347a | ||
|
|
c1fd1a3b42 | ||
|
|
b1fe32baea | ||
|
|
fb7a7d02da | ||
|
|
20dfc2a216 | ||
|
|
d7d2d36e0a | ||
|
|
07904bcc5d | ||
|
|
9686075104 | ||
|
|
436e0e847d | ||
|
|
d50b070a64 | ||
|
|
80cb48fd6a | ||
|
|
e88039e46a | ||
|
|
3c45a8d0b4 | ||
|
|
c9d71f3b2d | ||
|
|
49c6efbc22 | ||
|
|
9f37175775 | ||
|
|
5ed870cc6e | ||
|
|
4fb1c7a630 | ||
|
|
2a3d7e470d | ||
|
|
f654eb2dcd | ||
|
|
c21d705143 | ||
|
|
85c1750706 | ||
|
|
e2151409bf | ||
|
|
4fe6cfc57a | ||
|
|
fd37fdde93 | ||
|
|
66b1d5b7bd | ||
|
|
2eed7444b7 | ||
|
|
2bf7a9dd5f | ||
|
|
7bacd8fbca | ||
|
|
ef376fbb7b | ||
|
|
b883882a32 | ||
|
|
18d5b84b00 | ||
|
|
e5e11b6383 | ||
|
|
01e963ae35 | ||
|
|
90a80c4b63 | ||
|
|
b56f237780 | ||
|
|
4970ae3eb4 | ||
|
|
c62fadac47 | ||
|
|
a264f84e6c | ||
|
|
2e21f084fc | ||
|
|
55513b9ae5 | ||
|
|
9311ee4c87 | ||
|
|
07d0062645 | ||
|
|
f4b38af649 | ||
|
|
6e7bcd1243 | ||
|
|
b8c8579ff5 | ||
|
|
82295c79d4 | ||
|
|
5174591058 | ||
|
|
ed3fc5b8b2 | ||
|
|
f5f9157786 | ||
|
|
658303fa36 | ||
|
|
6cb0cb7f98 | ||
|
|
84013c21fa | ||
|
|
f3062ade39 | ||
|
|
ca19d4deeb | ||
|
|
dfceccca3d | ||
|
|
9e2d3b1fa1 | ||
|
|
4dc0f3b4eb | ||
|
|
215288b6b4 | ||
|
|
2e2c3e7bac | ||
|
|
0210b310b7 | ||
|
|
ce1e44eda4 | ||
|
|
48825e1e59 | ||
|
|
61cfe2d142 | ||
|
|
2d68f4dfd3 | ||
|
|
1e23937aa5 | ||
|
|
aecaf51953 | ||
|
|
da05d6b67d | ||
|
|
99a100ad63 | ||
|
|
bd3bcb4b18 | ||
|
|
534c7864fc | ||
|
|
4bd2eba573 | ||
|
|
563f608255 | ||
|
|
382b5e7e3a | ||
|
|
a503f8ae93 | ||
|
|
e1935fb3fb | ||
|
|
7ac44009d5 | ||
|
|
f22eef5bf6 | ||
|
|
b3763dec3f | ||
|
|
7c61049103 | ||
|
|
fe819c6ec4 | ||
|
|
1a4594a615 | ||
|
|
41751d60d2 | ||
|
|
8bd0edca46 | ||
|
|
d2bff2853f | ||
|
|
e2f22b86c7 | ||
|
|
ff96f7be85 | ||
|
|
108b5ab225 | ||
|
|
a403af7ebd | ||
|
|
43a238a896 | ||
|
|
e3c9588c1c | ||
|
|
25b63e5675 | ||
|
|
6c59d633cd | ||
|
|
daa408e291 | ||
|
|
bb0a891638 | ||
|
|
8aed47dad3 | ||
|
|
c5bd183f73 | ||
|
|
06441063f2 | ||
|
|
0e23c9f6ab | ||
|
|
8fff9afee6 | ||
|
|
9f11a2cb32 | ||
|
|
ff55318c04 | ||
|
|
41b7957eff | ||
|
|
053b2c6248 | ||
|
|
7e52aadb98 | ||
|
|
fad058ee59 | ||
|
|
69f41c300f | ||
|
|
18c5e0b9a8 | ||
|
|
568c2fd9d7 | ||
|
|
794eb78aca | ||
|
|
e761bcac85 | ||
|
|
1929ee36ed | ||
|
|
84cccd1b79 | ||
|
|
e66d44ca81 | ||
|
|
2f3b464715 | ||
|
|
286e477ad5 | ||
|
|
6e6749f42e | ||
|
|
7b6aa0c12a | ||
|
|
409f95ac17 | ||
|
|
25950be077 | ||
|
|
5230d54551 | ||
|
|
a79be05261 | ||
|
|
9eda1d0d78 | ||
|
|
f6c4f800c4 | ||
|
|
f363d6a801 | ||
|
|
e88ce0d52f | ||
|
|
fe5fe7a933 | ||
|
|
9c77296858 | ||
|
|
34da6b64df | ||
|
|
699cc147b5 | ||
|
|
b1d792a757 | ||
|
|
18e6842e35 | ||
|
|
c4481be39f | ||
|
|
0df420d353 | ||
|
|
f60b6a4869 | ||
|
|
3e02dc550f | ||
|
|
1cf0de395c | ||
|
|
d40b68c004 | ||
|
|
50b9e8c6e6 | ||
|
|
d25f9cd4bd | ||
|
|
dac2d31b35 | ||
|
|
bedb98ad9f | ||
|
|
5f37601122 | ||
|
|
1d3a1a094a | ||
|
|
9652996f07 | ||
|
|
796aaed11e | ||
|
|
1da69cfa38 | ||
|
|
2a2735cd67 | ||
|
|
0552b6e713 | ||
|
|
30e655b1da | ||
|
|
0aeecc6268 | ||
|
|
5cec006a76 | ||
|
|
307e06372b | ||
|
|
9f24e76c27 | ||
|
|
7954eaf529 | ||
|
|
fe406b577e | ||
|
|
76c03af024 | ||
|
|
fe5cdcbdc7 | ||
|
|
2479a06f9a | ||
|
|
fc5e4a821b | ||
|
|
3cdbc27de9 | ||
|
|
a8ed1371d4 | ||
|
|
e7bac41c37 | ||
|
|
dede2a8f8e | ||
|
|
e94abad3eb | ||
|
|
f8bc6e12a9 | ||
|
|
a2c3208af9 | ||
|
|
a303df9c34 | ||
|
|
bda24f3829 | ||
|
|
94fd3119e8 | ||
|
|
c8091b42a7 | ||
|
|
41a859b444 | ||
|
|
1cf20f7604 | ||
|
|
629541bc9d | ||
|
|
44245d1b5f | ||
|
|
a8692c72c6 | ||
|
|
7b7da59ca9 | ||
|
|
b78e3159d1 | ||
|
|
2106028350 | ||
|
|
c2e7f9dc42 | ||
|
|
8568098c5e | ||
|
|
c8bc1ef1d2 | ||
|
|
59bd434d05 | ||
|
|
3cf1e53340 | ||
|
|
24f0956630 | ||
|
|
91991d5b32 | ||
|
|
447b6ed1ab | ||
|
|
336486fecd | ||
|
|
bbd417c119 | ||
|
|
e3ffd45a18 | ||
|
|
52a4a46ebd | ||
|
|
025423a75e | ||
|
|
a6f17e35dd | ||
|
|
9a8cac3cac | ||
|
|
884b857d17 | ||
|
|
a20bbeff79 | ||
|
|
2a5f1f870b | ||
|
|
cf5303a39c | ||
|
|
bf1e26c4e6 | ||
|
|
4713d943d1 | ||
|
|
df083cb315 | ||
|
|
6401b25964 | ||
|
|
65f4adc68e | ||
|
|
dfab472522 | ||
|
|
9759adf747 | ||
|
|
9ae1a08573 | ||
|
|
ddab2cab14 | ||
|
|
cf1a8fff65 | ||
|
|
45be4769d7 | ||
|
|
a68c41709a | ||
|
|
4290bce718 | ||
|
|
3dd237002b | ||
|
|
e0fb112bfb | ||
|
|
2629537fd3 | ||
|
|
cb7711f637 | ||
|
|
a114e9adb0 | ||
|
|
5fed5fa158 | ||
|
|
6c8873c7f5 | ||
|
|
37b0c4adc0 | ||
|
|
9106617436 | ||
|
|
68a95cf0d0 | ||
|
|
1438632dde | ||
|
|
112fa46896 | ||
|
|
6da5bbf33a | ||
|
|
95faf340c4 | ||
|
|
723ea508df | ||
|
|
cbe538261c | ||
|
|
1925e6782f | ||
|
|
bcd950c141 | ||
|
|
b215689566 | ||
|
|
dfe0f64c7c | ||
|
|
9de3910526 | ||
|
|
59eb75d8c3 | ||
|
|
6670b76ccc | ||
|
|
09b9383a0b | ||
|
|
31e37f352d | ||
|
|
c5958e4d61 | ||
|
|
f1e7149e88 | ||
|
|
d80ef17623 | ||
|
|
ffe469ce71 | ||
|
|
c35bb6e09a | ||
|
|
8d404d97a1 | ||
|
|
fa2b0bd67c | ||
|
|
9563d1b1f6 | ||
|
|
33fc578f96 | ||
|
|
79786dde16 | ||
|
|
926b2c9cfb | ||
|
|
c1b2aa7628 | ||
|
|
991b26b73f | ||
|
|
254a7461b2 | ||
|
|
1384ed8aba | ||
|
|
c9393c0cfb | ||
|
|
6eeb55fb88 | ||
|
|
c759afc10d | ||
|
|
090cb63568 | ||
|
|
f223831766 | ||
|
|
854f286364 | ||
|
|
2846b80cf7 | ||
|
|
ad0ef82ffc | ||
|
|
3bb547fc45 | ||
|
|
c3b326d95e | ||
|
|
8c1a97dac5 | ||
|
|
4053e8c8db | ||
|
|
ee86a3943d | ||
|
|
46b3e174ed | ||
|
|
c0c2c9489c | ||
|
|
c05c179d67 | ||
|
|
596193d34d | ||
|
|
15eee0d8d8 | ||
|
|
0b7444e8ff | ||
|
|
96a91dc710 | ||
|
|
503d431d8e | ||
|
|
ed5875f29a | ||
|
|
b12d44150b | ||
|
|
66f5eb57b9 | ||
|
|
7023880e67 | ||
|
|
0e4cf23a93 | ||
|
|
3f93b0d44b | ||
|
|
4a6c3d6ad3 | ||
|
|
478439f055 | ||
|
|
9ef2454210 | ||
|
|
a702fee3cd | ||
|
|
cac639d267 | ||
|
|
0daf548773 | ||
|
|
9249a5d4ea | ||
|
|
4c83feaa31 | ||
|
|
4d8ad19eea | ||
|
|
e7e7d576b2 | ||
|
|
7f2af1c355 | ||
|
|
ad38a21053 | ||
|
|
9bb92277e4 | ||
|
|
643621a389 | ||
|
|
ef8d2e7418 | ||
|
|
adffac4eec | ||
|
|
575342b3bb | ||
|
|
b6ecb4368e | ||
|
|
081df7fc03 | ||
|
|
358343b4df | ||
|
|
d7c19325cc | ||
|
|
b472a8ab19 | ||
|
|
2b83d0d0e9 | ||
|
|
911ac263fa | ||
|
|
545c78eb74 | ||
|
|
124b098c92 | ||
|
|
43ed430475 | ||
|
|
edd3b1512e | ||
|
|
d9623c3c88 | ||
|
|
f052e31ff0 | ||
|
|
73dfe12ec9 | ||
|
|
0c3d73745e | ||
|
|
a6ecc4fb3c | ||
|
|
625d9ab188 | ||
|
|
e6cc15e19b | ||
|
|
0af2cd6413 | ||
|
|
443e0b0206 | ||
|
|
a4fa5e9304 | ||
|
|
d94c311a1e | ||
|
|
c0c8390a7d | ||
|
|
f4be117219 | ||
|
|
7eb590e9fd | ||
|
|
6588913141 | ||
|
|
5c4a60aee7 | ||
|
|
af02e12685 | ||
|
|
675864ce0b | ||
|
|
c55f3182d8 | ||
|
|
0d6eac7656 | ||
|
|
7acfd199aa | ||
|
|
33d6f543a1 | ||
|
|
4237ef572e | ||
|
|
6babea8b12 | ||
|
|
6b7f91c671 | ||
|
|
b3b183c151 | ||
|
|
59f2ee87e6 | ||
|
|
6af8940a46 | ||
|
|
e1a1110f06 | ||
|
|
1dcf1e0b0f | ||
|
|
4c3e345c9c | ||
|
|
a3b9a9f07b | ||
|
|
17ec360720 | ||
|
|
89b67d59d5 | ||
|
|
77be00014e | ||
|
|
e336f287b6 | ||
|
|
50aa6ff306 | ||
|
|
56f162f219 | ||
|
|
f7a0c4139a | ||
|
|
7c39e321c4 | ||
|
|
ce6a863599 | ||
|
|
832690e71e | ||
|
|
7526cb0d71 | ||
|
|
3292109ab0 | ||
|
|
be376d2030 | ||
|
|
4d455b5e9f | ||
|
|
50ce28e378 | ||
|
|
1eee8e2ce8 | ||
|
|
5e6ce9172f | ||
|
|
03adbc2ae8 | ||
|
|
e63a3f76f7 | ||
|
|
6d42d456fb | ||
|
|
c818b6f88f | ||
|
|
3f3c7905b4 | ||
|
|
e8dd13beb2 | ||
|
|
fc6b64aa68 | ||
|
|
5e0a2f66e3 | ||
|
|
108d4fabba | ||
|
|
2e277a39ca | ||
|
|
814ec43714 | ||
|
|
54bb9ea755 | ||
|
|
374e921672 | ||
|
|
64e5ea93a0 | ||
|
|
2562d70880 | ||
|
|
d99ef29152 | ||
|
|
d5a2cd9cd2 | ||
|
|
82d2889e96 | ||
|
|
ff2e845f2c | ||
|
|
ccd6ae5ade | ||
|
|
36bafc0d40 | ||
|
|
f7746b8f94 | ||
|
|
537c5ca7b8 | ||
|
|
4901a80684 | ||
|
|
03b5d44a7c | ||
|
|
8e51aa8df4 | ||
|
|
029a9674ca | ||
|
|
68cee1b1f1 | ||
|
|
3f74e230b2 | ||
|
|
6bf1919f8d | ||
|
|
e69d61eaf4 | ||
|
|
2f83f22753 | ||
|
|
f9d757bb85 | ||
|
|
6b6e80f4b8 | ||
|
|
f32b92a5b0 | ||
|
|
761a0a7009 | ||
|
|
129d3e61fa | ||
|
|
aa94671002 | ||
|
|
52cf136f84 | ||
|
|
808427795c | ||
|
|
3f71734cb4 | ||
|
|
9f14edb0d7 | ||
|
|
7fa7a806a8 | ||
|
|
d364f4db62 | ||
|
|
f2c431d029 | ||
|
|
6a667c30d6 | ||
|
|
de637fcf4e | ||
|
|
132069472c | ||
|
|
172c6ad4b8 | ||
|
|
73a72ec1c7 | ||
|
|
c39a8d84ac | ||
|
|
027e5c64cc | ||
|
|
ba42c9b85e | ||
|
|
cd1be43384 | ||
|
|
6176027263 | ||
|
|
58bd7c6bd4 | ||
|
|
f02667e031 | ||
|
|
8e0a6e4123 | ||
|
|
0131cd6f8b | ||
|
|
99b40cecf2 | ||
|
|
c7a4c67d83 | ||
|
|
c29a8cb0c4 | ||
|
|
288a7b21d6 | ||
|
|
32bd08533d | ||
|
|
c1aae12327 | ||
|
|
23a6f4b7c1 | ||
|
|
a9f4fe84fa | ||
|
|
133e6e1e68 | ||
|
|
f7832585dc | ||
|
|
e34bfb50a8 | ||
|
|
6abd045273 | ||
|
|
0a106c2604 | ||
|
|
8f5f88743b | ||
|
|
9562d2f1f0 | ||
|
|
ea482f16c8 | ||
|
|
50634e1a4c | ||
|
|
56de96d25b | ||
|
|
5d1c20c47c | ||
|
|
778a608854 | ||
|
|
7de8e10721 | ||
|
|
80f41c4a69 | ||
|
|
a3557a81e4 | ||
|
|
0a02e526ee | ||
|
|
db9349e764 | ||
|
|
60903f349f | ||
|
|
b91e955486 | ||
|
|
a76a9fae41 | ||
|
|
f7cfbdd229 | ||
|
|
e28d2842f6 | ||
|
|
ccc3ca0948 | ||
|
|
515cbf7bef | ||
|
|
c320cbc47b | ||
|
|
6166f45a7f | ||
|
|
c103eb86db | ||
|
|
61d93d69b1 | ||
|
|
d5abf34538 | ||
|
|
7efc297cd9 | ||
|
|
98522a390e | ||
|
|
46969585ed | ||
|
|
47882c5419 | ||
|
|
019d5e083a | ||
|
|
6fc949844d | ||
|
|
97e8c9250a | ||
|
|
85f6cf32ae | ||
|
|
ded8e39e73 | ||
|
|
551313d3de | ||
|
|
e730200873 | ||
|
|
433e61bc4e | ||
|
|
818b03d8f2 | ||
|
|
ae3aef8dcc | ||
|
|
1b30325640 | ||
|
|
44d626d578 | ||
|
|
4501d13961 | ||
|
|
c8f5ec4698 | ||
|
|
07b15819d4 | ||
|
|
f519c6ef46 | ||
|
|
baa1cfb2f8 | ||
|
|
905699d15a | ||
|
|
fe53869308 | ||
|
|
c69ee35e18 | ||
|
|
8d5ee92f16 | ||
|
|
e55d184d2b | ||
|
|
50076bac83 | ||
|
|
e976714964 | ||
|
|
44bc4b7fa4 | ||
|
|
ce0d6ffda2 | ||
|
|
5c14f486d7 | ||
|
|
0cbd980b68 | ||
|
|
bc2308f2ce | ||
|
|
0b47a366ab | ||
|
|
1892fa6782 | ||
|
|
6f35b7db24 | ||
|
|
4d9e070bcd | ||
|
|
61fe8e8d8e | ||
|
|
0934095e96 | ||
|
|
eba2ff7d8d | ||
|
|
283eb0419c | ||
|
|
9a0c36c442 | ||
|
|
ff1d26294a | ||
|
|
63bfbbb3c6 | ||
|
|
95dda2b1af | ||
|
|
76d725559e | ||
|
|
d7ec8ccbc0 | ||
|
|
6def5e285b | ||
|
|
5170872961 | ||
|
|
b46e9ee065 | ||
|
|
871ca68e1e | ||
|
|
0ab896fc76 | ||
|
|
6a4b548457 | ||
|
|
0457ca4fe5 | ||
|
|
083be7df88 | ||
|
|
695a399941 | ||
|
|
200b69fae2 | ||
|
|
a32463fada | ||
|
|
3b04cd37ff | ||
|
|
5d44c88988 | ||
|
|
4d688b1d55 | ||
|
|
e43b6fb0b7 | ||
|
|
7895f03447 | ||
|
|
1f42b2f72d | ||
|
|
f4adfe56be | ||
|
|
33a679fbc0 | ||
|
|
9db67cc5e8 | ||
|
|
9834f0596b | ||
|
|
ce87d797d1 | ||
|
|
37cec8891f | ||
|
|
fd62141c04 | ||
|
|
4bdba6894d | ||
|
|
6c7fef29a8 | ||
|
|
a77edc5aa2 | ||
|
|
7fde1436e1 | ||
|
|
e1c5a32fcb | ||
|
|
b262e6a46f | ||
|
|
31f37a20e3 | ||
|
|
06b4ae5c96 | ||
|
|
a3e24785d3 | ||
|
|
78102210a5 | ||
|
|
7553d68100 | ||
|
|
2e726b62c3 | ||
|
|
02acd81c2c | ||
|
|
44daa1cf65 | ||
|
|
bae2de75ff | ||
|
|
b68c426cd1 | ||
|
|
5161ef15bf | ||
|
|
bdbaa6d597 | ||
|
|
36d3d94ec9 | ||
|
|
0e675a725d | ||
|
|
17447d7610 | ||
|
|
2a3046ba2e | ||
|
|
54d76123d0 | ||
|
|
6ffbf08826 | ||
|
|
708a40bff1 | ||
|
|
d84ee8bb65 | ||
|
|
a16f40cb73 | ||
|
|
02cff2740f | ||
|
|
6049d97ed9 | ||
|
|
3f657a0c04 | ||
|
|
efaf6573bd | ||
|
|
4b020dcc1a | ||
|
|
223a468bbf | ||
|
|
ddd0e447f6 | ||
|
|
0c0c81e9a5 | ||
|
|
001bcbce59 | ||
|
|
c195c07a3f | ||
|
|
f5298f51e7 | ||
|
|
46c440fef2 | ||
|
|
e6ac2c1159 | ||
|
|
4c605b8151 | ||
|
|
2913899aa5 | ||
|
|
ecd3245612 | ||
|
|
dadeda4476 | ||
|
|
d129557f77 | ||
|
|
e77f8b572a | ||
|
|
ade5eecf80 | ||
|
|
ff7e34e308 | ||
|
|
88055294a2 | ||
|
|
e473f45048 | ||
|
|
bcee670ac6 | ||
|
|
97fc7702b8 | ||
|
|
54fcd58531 | ||
|
|
b7a8747f00 | ||
|
|
b93e96a18d | ||
|
|
b70f6af2df | ||
|
|
d00de7d5a4 | ||
|
|
5ae4dde222 | ||
|
|
0e20bb6271 | ||
|
|
2fbd4b07e0 | ||
|
|
bd15ef4618 | ||
|
|
af5b942e05 | ||
|
|
098fd9fb0f | ||
|
|
a242962113 | ||
|
|
2b95e6b7a9 | ||
|
|
58a843ea23 | ||
|
|
4189d01844 | ||
|
|
57330f53e2 | ||
|
|
f6b97af148 | ||
|
|
1c79e726af | ||
|
|
76b7287bf1 | ||
|
|
019bc2f183 | ||
|
|
8c96a617be | ||
|
|
1f15e9b81e | ||
|
|
f7627e515a | ||
|
|
d08c94d5a6 | ||
|
|
cccea3dc71 | ||
|
|
c82c39caf3 | ||
|
|
01896501c1 | ||
|
|
33cf75e933 | ||
|
|
3f9a1525ca | ||
|
|
52c1e227d5 | ||
|
|
955538b12a | ||
|
|
dfc8a1da4a | ||
|
|
8254af27cb | ||
|
|
b477ca0508 | ||
|
|
f76391ecbb | ||
|
|
c49e9fbf18 | ||
|
|
122701ee7b | ||
|
|
351362bb50 | ||
|
|
9a6989d2ca | ||
|
|
8aebe1a41e | ||
|
|
1acf78d57c | ||
|
|
523373dfa2 | ||
|
|
f55e7d8165 | ||
|
|
9fdc6be465 | ||
|
|
9390c1e7be | ||
|
|
d788a4d252 | ||
|
|
b20b272eae | ||
|
|
2cddc6fb5b | ||
|
|
d46b519524 | ||
|
|
4effd375a9 | ||
|
|
4e753dc474 | ||
|
|
fbf63b98c3 | ||
|
|
3df557b370 | ||
|
|
cdb600b081 | ||
|
|
ffb688696b | ||
|
|
8bb210e7b6 | ||
|
|
9ee488009f | ||
|
|
96d9b102b6 | ||
|
|
16fba49937 | ||
|
|
af99bd620c | ||
|
|
8a58b9d459 | ||
|
|
e3c62075b8 | ||
|
|
22a70eb5b2 | ||
|
|
4e2998a366 | ||
|
|
158f759cde | ||
|
|
3e3be95420 | ||
|
|
b5808701ec | ||
|
|
35f3125fff | ||
|
|
5427d207cd | ||
|
|
ee23d72d13 | ||
|
|
f22aa606ce | ||
|
|
d914314c1c | ||
|
|
4aa9f1f62b | ||
|
|
9d288486d7 | ||
|
|
ea5521485a | ||
|
|
f768ffbdad | ||
|
|
4f0d3660de | ||
|
|
fa72bb4adf | ||
|
|
ea0044f69a | ||
|
|
7e493376a4 | ||
|
|
8c5afe5ab3 | ||
|
|
7ccb742ef3 | ||
|
|
7bc29c22ed | ||
|
|
1d550eaa18 | ||
|
|
b71ec4bfe0 | ||
|
|
827bbf6a7f | ||
|
|
2db0cc0cbf | ||
|
|
42ef01b339 | ||
|
|
fdaef2be69 | ||
|
|
ae3213f5d4 | ||
|
|
6dfd05fdd1 | ||
|
|
51107c3fc9 | ||
|
|
b6863efb3a | ||
|
|
799bceb8b7 | ||
|
|
9e573128c1 | ||
|
|
1f05511add | ||
|
|
eeee52a738 | ||
|
|
7f53860296 | ||
|
|
16d0077393 | ||
|
|
622fed2f0d | ||
|
|
d22ade3289 | ||
|
|
7febf330ac | ||
|
|
75a50ac1ac | ||
|
|
e62567d09e | ||
|
|
8d80eebeb1 | ||
|
|
ee9a42238d | ||
|
|
758c76d661 | ||
|
|
1dec46cbfa | ||
|
|
ae25d704c1 | ||
|
|
e05f8c0329 | ||
|
|
ce62e11626 | ||
|
|
9f04c2fc1d | ||
|
|
05a405a82d | ||
|
|
3c8c21c378 | ||
|
|
2dbeb884a5 | ||
|
|
931d72b41f | ||
|
|
2e3cdd872c | ||
|
|
55a13c3139 | ||
|
|
f63d1c87e3 | ||
|
|
abbfd44534 | ||
|
|
f772724f9a | ||
|
|
f3abd0f190 | ||
|
|
5d4042c861 | ||
|
|
1fbcec98fb | ||
|
|
abef9f3cf7 | ||
|
|
6f1958f9f2 | ||
|
|
6b2ce86d5f | ||
|
|
0cfd70da2e | ||
|
|
4167faf39d | ||
|
|
90e6e8c5eb | ||
|
|
b40b1fa2e4 | ||
|
|
bb1ec109d8 | ||
|
|
4c21468850 | ||
|
|
40c300fa1a | ||
|
|
e2b55d814b | ||
|
|
77a47e4b2b |
@@ -226,14 +226,29 @@ jobs:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
|
||||
|
||||
# Build frontend
|
||||
- run:
|
||||
name: "integration tests"
|
||||
name: "frontend build"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run build:app:assets
|
||||
yarn run build:app
|
||||
yarn run build:app:libs
|
||||
|
||||
# Build the wasm bundle
|
||||
- run:
|
||||
name: "wasm build"
|
||||
working_directory: "./render-wasm"
|
||||
command: |
|
||||
EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh
|
||||
./build release
|
||||
|
||||
# Run integration tests
|
||||
- run:
|
||||
name: "integration tests"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn run playwright install chromium
|
||||
yarn run test:e2e -x --workers=4
|
||||
|
||||
|
||||
@@ -45,10 +45,16 @@
|
||||
:potok/reify-type
|
||||
{:level :error}
|
||||
|
||||
:missing-protocol-method
|
||||
{:level :off}
|
||||
|
||||
:unresolved-namespace
|
||||
{:level :warning
|
||||
:exclude [data_readers]}
|
||||
|
||||
:unused-value
|
||||
{:level :off}
|
||||
|
||||
:single-key-in
|
||||
{:level :warning}
|
||||
|
||||
@@ -64,6 +70,9 @@
|
||||
:redundant-nested-call
|
||||
{:level :off}
|
||||
|
||||
:redundant-str-call
|
||||
{:level :off}
|
||||
|
||||
:earmuffed-var-not-dynamic
|
||||
{:level :off}
|
||||
|
||||
|
||||
89
.github/workflows/build-bundle.yml
vendored
Normal file
89
.github/workflows/build-bundle.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
name: Bundles Builder
|
||||
|
||||
on:
|
||||
# Create bundle from manual action
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch or ref'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
build_wasm:
|
||||
description: 'BUILD_WASM. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
build_storybook:
|
||||
description: 'BUILD_STORYBOOK. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch or ref'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
build_wasm:
|
||||
description: 'BUILD_WASM. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
build_storybook:
|
||||
description: 'BUILD_STORYBOOK. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
name: Build and Upload Penpot Bundle
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
- name: Extract some useful variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build bundle
|
||||
env:
|
||||
BUILD_WASM: ${{ inputs.build_wasm }}
|
||||
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
|
||||
run: ./manage.sh build-bundle
|
||||
|
||||
- name: Prepare directories for zipping
|
||||
run: |
|
||||
mkdir zips
|
||||
mv bundles penpot
|
||||
|
||||
- name: Create zip bundle
|
||||
run: |
|
||||
echo "📦 Packaging Penpot bundle..."
|
||||
zip -r zips/penpot.zip penpot
|
||||
|
||||
- name: Upload Penpot bundle to S3
|
||||
run: |
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
TEXT: |
|
||||
❌ *[PENPOT] Error during the execution of the job*
|
||||
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
129
.github/workflows/build-bundles.yml
vendored
129
.github/workflows/build-bundles.yml
vendored
@@ -1,129 +0,0 @@
|
||||
name: Build and Upload Penpot Bundles non-prod
|
||||
|
||||
on:
|
||||
# Create bundler for every tag
|
||||
push:
|
||||
tags:
|
||||
- '**' # Pattern matched against refs/tags
|
||||
# Create bundler every hour between 5:00 and 20:00 on working days
|
||||
schedule:
|
||||
- cron: '0 5-20 * * 1-5'
|
||||
# Create bundler from manual action
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
zip_mode:
|
||||
# zip_mode defines how the build artifacts are packaged:
|
||||
# - 'individual': creates one ZIP file per component (frontend, backend, exporter)
|
||||
# - 'all': creates a single ZIP containing all components
|
||||
# - null: for the rest of cases (non-manual events)
|
||||
description: 'Bundle packaging mode'
|
||||
required: false
|
||||
default: 'individual'
|
||||
type: choice
|
||||
options:
|
||||
- individual
|
||||
- all
|
||||
|
||||
jobs:
|
||||
build-bundles:
|
||||
name: Build and Upload Penpot Bundles
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract somer useful variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "gh_branch=${{ github.base_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Set up Docker Buildx for multi-arch build
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Run manage.sh build-bundle from host
|
||||
run: ./manage.sh build-bundle
|
||||
|
||||
- name: Prepare directories for zipping
|
||||
run: |
|
||||
mkdir zips
|
||||
mv bundles penpot
|
||||
|
||||
- name: Create zip bundles for zip_mode == 'all'
|
||||
if: ${{ github.event.inputs.zip_mode == 'all' }}
|
||||
run: |
|
||||
echo "📦 Packaging Penpot 'all' bundles..."
|
||||
zip -r zips/penpot-all-bundles.zip penpot
|
||||
|
||||
- name: Create zip bundles for zip_mode != 'all'
|
||||
if: ${{ github.event.inputs.zip_mode != 'all' }}
|
||||
run: |
|
||||
echo "📦 Packaging Penpot 'individual' bundles..."
|
||||
zip -r zips/penpot-frontend.zip penpot/frontend
|
||||
zip -r zips/penpot-backend.zip penpot/backend
|
||||
zip -r zips/penpot-exporter.zip penpot/exporter
|
||||
|
||||
- name: Upload unified 'all' bundle
|
||||
if: ${{ github.event.inputs.zip_mode == 'all' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: penpot-all-bundles
|
||||
path: zips/penpot-all-bundles.zip
|
||||
|
||||
- name: Upload individual bundles
|
||||
if: ${{ github.event.inputs.zip_mode != 'all' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: penpot-individual-bundles
|
||||
path: |
|
||||
zips/penpot-frontend.zip
|
||||
zips/penpot-backend.zip
|
||||
zips/penpot-exporter.zip
|
||||
|
||||
- name: Upload unified 'all' bundle to S3
|
||||
if: ${{ github.event.inputs.zip_mode == 'all' }}
|
||||
run: |
|
||||
aws s3 cp zips/penpot-all-bundles.zip s3://${{ secrets.S3_BUCKET }}/penpot-all-bundles-${{ steps.vars.outputs.gh_branch}}.zip
|
||||
aws s3 cp zips/penpot-all-bundles.zip s3://${{ secrets.S3_BUCKET }}/penpot-all-bundles-${{ steps.vars.outputs.commit_hash }}.zip
|
||||
|
||||
- name: Upload 'individual' bundles to S3
|
||||
if: ${{ github.event.inputs.zip_mode != 'all' }}
|
||||
run: |
|
||||
for name in penpot-frontend penpot-backend penpot-exporter; do
|
||||
aws s3 cp zips/${name}.zip s3://${{ secrets.S3_BUCKET }}/${name}-${{ steps.vars.outputs.gh_branch }}-latest.zip
|
||||
aws s3 cp zips/${name}.zip s3://${{ secrets.S3_BUCKET }}/${name}-${{ steps.vars.outputs.commit_hash }}.zip
|
||||
done
|
||||
|
||||
- name: Notify Mattermost about automatic bundles
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
TEXT: |
|
||||
📦 *Penpot bundle automatically generated*
|
||||
📄 PR: ${{ github.event.pull_request.title }}
|
||||
🔁 From: \`${{ github.head_ref }}\` to \`{{ github.base_ref }}\`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: Notify Mattermost about manual bundles
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
TEXT: |
|
||||
📦 *Penpot bundle manually generated*
|
||||
📄 Triggered from branch: `${{ github.ref_name}}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: Print artifact summary URL
|
||||
run: |
|
||||
echo "📦 Artifacts available at:"
|
||||
echo "🔗 https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
21
.github/workflows/build-develop.yml
vendored
Normal file
21
.github/workflows/build-develop.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: _DEVELOP
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '16 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "develop"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
uses: ./.github/workflows/build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "develop"
|
||||
36
.github/workflows/build-docker-devenv.yml
vendored
Normal file
36
.github/workflows/build-docker-devenv.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: DevEnv Docker Image Builder
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and push DevEnv Docker image
|
||||
environment: release-admins
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push DevEnv Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
env:
|
||||
DOCKER_IMAGE: 'penpotapp/devenv'
|
||||
with:
|
||||
context: ./docker/devenv/
|
||||
file: ./docker/devenv/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_IMAGE }}:latest
|
||||
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
101
.github/workflows/build-docker.yml
vendored
Normal file
101
.github/workflows/build-docker.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
name: Docker Images Builder
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch or ref'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch or ref'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push Penpot Docker Images
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
- name: Extract some useful variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download Penpot Bundles
|
||||
env:
|
||||
FILE_NAME: penpot-${{ steps.vars.outputs.gh_ref }}.zip
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
pushd docker/images
|
||||
aws s3 cp s3://${{ secrets.S3_BUCKET }}/$FILE_NAME .
|
||||
unzip $FILE_NAME > /dev/null
|
||||
mv penpot/backend bundle-backend
|
||||
mv penpot/frontend bundle-frontend
|
||||
mv penpot/exporter bundle-exporter
|
||||
popd
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push Backend Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
env:
|
||||
DOCKER_IMAGE: 'backend'
|
||||
BUNDLE_PATH: './bundle-backend'
|
||||
with:
|
||||
context: ./docker/images/
|
||||
file: ./docker/images/Dockerfile.backend
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
- name: Build and push Frontend Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
env:
|
||||
DOCKER_IMAGE: 'frontend'
|
||||
BUNDLE_PATH: './bundle-frontend'
|
||||
with:
|
||||
context: ./docker/images/
|
||||
file: ./docker/images/Dockerfile.frontend
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
- name: Build and push Exporter Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
env:
|
||||
DOCKER_IMAGE: 'exporter'
|
||||
BUNDLE_PATH: './bundle-exporter'
|
||||
with:
|
||||
context: ./docker/images/
|
||||
file: ./docker/images/Dockerfile.exporter
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
21
.github/workflows/build-staging.yml
vendored
Normal file
21
.github/workflows/build-staging.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: _STAGING
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '36 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "staging"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
uses: ./.github/workflows/build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "staging"
|
||||
30
.github/workflows/build-tag.yml
vendored
Normal file
30
.github/workflows/build-tag.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: _TAG
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: ${{ github.ref_name }}
|
||||
build_wasm: "no"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
uses: ./.github/workflows/build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: ${{ github.ref_name }}
|
||||
|
||||
# publish-final-tag:
|
||||
# if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
|
||||
# needs: build-docker
|
||||
# uses: ./.github/workflows/release.yml
|
||||
# secrets: inherit
|
||||
# with:
|
||||
# gh_ref: ${{ github.ref_name }}
|
||||
2
.github/workflows/commit-checker.yml
vendored
2
.github/workflows/commit-checker.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check Commit Type
|
||||
uses: gsactions/commit-message-checker@v2
|
||||
with:
|
||||
pattern: '^(Merge|:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):)\s[A-Z].*[^.]$'
|
||||
pattern: '^(Merge|Revert|:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind):)\s["A-Z].*[^.]$'
|
||||
flags: 'gm'
|
||||
error: 'Commit should match CONTRIBUTING.md guideline'
|
||||
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
|
||||
|
||||
95
.github/workflows/release.yml
vendored
Normal file
95
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
name: Release Publisher
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Tag to release'
|
||||
type: string
|
||||
required: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Tag to release'
|
||||
type: string
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
environment: release-admins
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
version: ${{ steps.vars.outputs.gh_ref }}
|
||||
release_notes: ${{ steps.extract_release_notes.outputs.release_notes }}
|
||||
steps:
|
||||
- name: Extract some useful variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ steps.vars.outputs.gh_ref }}
|
||||
|
||||
# # --- Publicly release the docker images ---
|
||||
# - name: Login to private registry
|
||||
# uses: docker/login-action@v3
|
||||
# with:
|
||||
# registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||
# username: ${{ secrets.DOCKER_USERNAME }}
|
||||
# password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# - name: Login to DockerHub
|
||||
# uses: docker/login-action@v3
|
||||
# with:
|
||||
# username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||
# password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||
|
||||
# - name: Publish docker images to DockerHub
|
||||
# env:
|
||||
# TAG: ${{ steps.vars.outputs.gh_ref }}
|
||||
# REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
|
||||
# HUB: ${{ secrets.PUB_DOCKER_HUB }}
|
||||
# run: |
|
||||
# IMAGES=("frontend" "backend" "exporter")
|
||||
# EXTRA_TAGS=("main" "latest")
|
||||
|
||||
# for image in "${IMAGES[@]}"; do
|
||||
# docker pull "$REGISTRY/penpotapp/$image:$TAG"
|
||||
# docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$TAG"
|
||||
# docker push "penpotapp/$image:$TAG"
|
||||
|
||||
# for tag in "${EXTRA_TAGS[@]}"; do
|
||||
# docker tag "$REGISTRY/penpotapp/$image:$TAG" "penpotapp/$image:$tag"
|
||||
# docker push "penpotapp/$image:$tag"
|
||||
# done
|
||||
# done
|
||||
|
||||
# --- Release notes extraction ---
|
||||
- name: Extract release notes from CHANGES.md
|
||||
id: extract_release_notes
|
||||
env:
|
||||
TAG: ${{ steps.vars.outputs.gh_ref }}
|
||||
run: |
|
||||
RELEASE_NOTES=$(awk "/^## $TAG$/{flag=1; next} /^## /{flag=0} flag" CHANGES.md | awk '{$1=$1};1')
|
||||
if [ -z "$RELEASE_NOTES" ]; then
|
||||
RELEASE_NOTES="No changes for $TAG according to CHANGES.md"
|
||||
fi
|
||||
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# --- Create GitHub release ---
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.vars.outputs.gh_ref }}
|
||||
name: ${{ steps.vars.outputs.gh_ref }}
|
||||
body: ${{ steps.extract_release_notes.outputs.release_notes }}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -31,6 +31,7 @@
|
||||
/.clj-kondo/.cache
|
||||
/_dump
|
||||
/notes
|
||||
/playground/
|
||||
/backend/*.md
|
||||
/backend/*.sql
|
||||
/backend/*.txt
|
||||
@@ -41,6 +42,7 @@
|
||||
/backend/resources/public/assets
|
||||
/backend/resources/public/media
|
||||
/backend/target/
|
||||
/backend/experiments
|
||||
/bundle*
|
||||
/cd.md
|
||||
/clj-profiler/
|
||||
@@ -51,9 +53,6 @@
|
||||
/exporter/target
|
||||
/frontend/.storybook/preview-body.html
|
||||
/frontend/.storybook/preview-head.html
|
||||
/frontend/cypress/fixtures/validuser.json
|
||||
/frontend/cypress/videos/*/
|
||||
/frontend/cypress/videos/*/
|
||||
/frontend/dist/
|
||||
/frontend/npm-debug.log
|
||||
/frontend/out/
|
||||
@@ -70,6 +69,8 @@
|
||||
/vendor/svgclean/bundle*.js
|
||||
/web
|
||||
/library/target/
|
||||
/library/*.zip
|
||||
/external
|
||||
|
||||
clj-profiler/
|
||||
node_modules
|
||||
|
||||
216
CHANGES.md
216
CHANGES.md
@@ -1,6 +1,117 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.9.0 (Unreleased)
|
||||
## 2.11.0 (Unreleased)
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be
|
||||
removed in future versions:
|
||||
|
||||
- The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its
|
||||
values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`)
|
||||
- The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY`
|
||||
- The `PENPOT_STORAGE_ASSETS_S3_BUCKET` becomes `PENPOT_OBJECTS_STORAGE_S3_BUCKET`
|
||||
- The `PENPOT_STORAGE_ASSETS_S3_REGION` becomes `PENPOT_OBJECTS_STORAGE_S3_REGION`
|
||||
- The `PENPOT_STORAGE_ASSETS_S3_ENDPOINT` becomes `PENPOT_OBJECTS_STORAGE_S3_ENDPOINT`
|
||||
- The `PENPOT_STORAGE_ASSETS_S3_IO_THREADS` replaced (see below)
|
||||
|
||||
- Add `PENPOT_NETTY_IO_THREADS` and `PENPOT_EXECUTOR_THREADS` variables to provide the
|
||||
control over concurrency of the shared resources used by netty. Penpot uses the netty IO
|
||||
threads for AWS S3 SDK and Redis/Valkey communication, and the EXEC threads to perform
|
||||
out of HTTP serving threads tasks such that cache invalidation, S3 response completion,
|
||||
configuration reloading and many other auxiliar tasks. By default they use a half number
|
||||
if available cpus with a minumum of 2 for both executors. You should not touch that
|
||||
variables unless you are know what you are doing.
|
||||
|
||||
- Replace the `PENPOT_STORAGE_ASSETS_S3_IO_THREADS` with a more general configuration
|
||||
`PENPOT_NETTY_IO_THREADS` used to configure a shared netty resources across different
|
||||
services which use netty internally (redis connection, S3 SDK client). This
|
||||
configuration is not very commonly used so don't expected real impact on any user.
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
- Show current Penpot version [Taiga #11603](https://tree.taiga.io/project/penpot/us/11603)
|
||||
- Switch several variant copies at the same time [Taiga #11411](https://tree.taiga.io/project/penpot/us/11411)
|
||||
- Invitations management improvements [Taiga #3479](https://tree.taiga.io/project/penpot/us/3479)
|
||||
- Alternative ways of creating variants - Button Viewport [Taiga #11931](https://tree.taiga.io/project/penpot/us/11931)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix selection problems when devtools open [Taiga #11950](https://tree.taiga.io/project/penpot/issue/11950)
|
||||
- Fix long font names overlap [Taiga #11844](https://tree.taiga.io/project/penpot/issue/11844)
|
||||
- Fix paste behavior according to the selected element [Taiga #11979](https://tree.taiga.io/project/penpot/issue/11979)
|
||||
- Fix problem with export size [Github #7160](https://github.com/penpot/penpot/issues/7160)
|
||||
- Fix multi level library dependencies [Taiga #12155](https://tree.taiga.io/project/penpot/issue/12155)
|
||||
- Fix component context menu options order in assets tab [Taiga #11941](https://tree.taiga.io/project/penpot/issue/11941)
|
||||
|
||||
## 2.10.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- Variants
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Add efficiency enhancements to right sidebar [Github #7182](https://github.com/penpot/penpot/pull/7182)
|
||||
- Add defaults for artboard drawing [Taiga #494](https://tree.taiga.io/project/penpot/us/494?milestone=465047)
|
||||
- Continuous display of distances between elements when moving a layer with the keyboard [Taiga #1780](https://tree.taiga.io/project/penpot/us/1780)
|
||||
- New Number token - unitless values [Taiga #10936](https://tree.taiga.io/project/penpot/us/10936)
|
||||
- New font-family token [Taiga #10937](https://tree.taiga.io/project/penpot/us/10937)
|
||||
- New text case token [Taiga #10942](https://tree.taiga.io/project/penpot/us/10942)
|
||||
- New text-decoration token [Taiga #10941](https://tree.taiga.io/project/penpot/us/10941)
|
||||
- New letter spacing token [Taiga #10940](https://tree.taiga.io/project/penpot/us/10940)
|
||||
- New font weight token [Taiga #10939](https://tree.taiga.io/project/penpot/us/10939)
|
||||
- Upgrade Node to v22.18.0 [Github #7283](https://github.com/penpot/penpot/pull/7283)
|
||||
- Upgrade the base docker image for penpot frontend to v1.29.1 [Github #7283](https://github.com/penpot/penpot/pull/7283)
|
||||
- Create variant from an existing component [Taiga #2088](https://tree.taiga.io/project/penpot/us/2088)
|
||||
- Create variant from an existing variant [Taiga #8282](https://tree.taiga.io/project/penpot/us/8282)
|
||||
- Actions over a component with variants [Taiga #10503](https://tree.taiga.io/project/penpot/us/10503)
|
||||
- Create a variant by dragging a component into a component with variants [Taiga #8134](https://tree.taiga.io/project/penpot/us/8134)
|
||||
- Transform a variant into an individual component [Taiga #8141](https://tree.taiga.io/project/penpot/us/8141)
|
||||
- Delete variant [Taiga #6890](https://tree.taiga.io/project/penpot/us/6890)
|
||||
- Restore an orphaned copy of a variant [Taiga #10446](https://tree.taiga.io/project/penpot/us/10446)
|
||||
- Add, Edit & Delete variant properties name and value [Taiga #6892](https://tree.taiga.io/project/penpot/us/6892)
|
||||
- Retrieve variants [Taiga #6888](https://tree.taiga.io/project/penpot/us/6888)
|
||||
- Retrieve variants with nested components [Taiga #10277](https://tree.taiga.io/project/penpot/us/10277)
|
||||
- Create variants in bulk from existing components [Taiga #7926](https://tree.taiga.io/project/penpot/us/7926)
|
||||
- Alternative ways of creating variants - Button Design Tab [Taiga #10316](https://tree.taiga.io/project/penpot/us/10316)
|
||||
- Fix problem with component swapping panel [Taiga #12175](https://tree.taiga.io/project/penpot/issue/12175)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Display strokes information in inspect tab [Taiga #11154](https://tree.taiga.io/project/penpot/issue/11154)
|
||||
- Fix problem with booleans selection [Taiga #11627](https://tree.taiga.io/project/penpot/issue/11627)
|
||||
- Fix missing font when copy&paste a chunk of text [Taiga #11522](https://tree.taiga.io/project/penpot/issue/11522)
|
||||
- Fix bad swap slot after two swaps [Taiga #11659](https://tree.taiga.io/project/penpot/issue/11659)
|
||||
- Fix missing package for the `penpot_exporter` Docker image [GitHub #7205](https://github.com/penpot/penpot/issues/7025)
|
||||
- Fix issue where multiple dropdown menus could be opened simultaneously on the dashboard page [Taiga #11500](https://tree.taiga.io/project/penpot/issue/11500)
|
||||
- Fix font size/variant not updated when editing a text [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11552)
|
||||
- Fix issue where Alt + arrow keys shortcut interferes with letter-spacing when moving text layers [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11771)
|
||||
- Fix consistency issues on how font variants are visualized [Taiga #11499](https://tree.taiga.io/project/penpot/us/11499)
|
||||
- Fix parsing rx and ry SVG values for rect radius [Taiga #11861](https://tree.taiga.io/project/penpot/issue/11861)
|
||||
- Fix misleading affordance in saved versions [Taiga #11887](https://tree.taiga.io/project/penpot/issue/11887)
|
||||
- Fix pasting RTF text crashes penpot [Taiga #11717](https://tree.taiga.io/project/penpot/issue/11717)
|
||||
- Fix navigation arrows in Libraries & Templates carousel [Taiga #10609](https://tree.taiga.io/project/penpot/issue/10609)
|
||||
- Fix applying tokens with zero value to size [Taiga #11618](https://tree.taiga.io/project/penpot/issue/11618)
|
||||
- Fix typo [Taiga #11969](https://tree.taiga.io/project/penpot/issue/11969)
|
||||
- Fix typo [Taiga #11970](https://tree.taiga.io/project/penpot/issue/11970)
|
||||
- Fix typos [Taiga #11971](https://tree.taiga.io/project/penpot/issue/11971)
|
||||
- Fix inconsistent naming for "Flatten" [Taiga #8371](https://tree.taiga.io/project/penpot/issue/8371)
|
||||
- Layout item tokens should be unapplied when moving out of a layout [Taiga #11012](https://tree.taiga.io/project/penpot/issue/11012)
|
||||
- Fix incorrect date displayed for support plan [Taiga #11986](https://tree.taiga.io/project/penpot/issue/11986)
|
||||
- Fix can't import 'borderWidth' type token [#132](https://github.com/tokens-studio/penpot/issues/132)
|
||||
- Fix moving elements up or down while pressing alt [Taiga Issue #11992](https://tree.taiga.io/project/penpot/issue/11992)
|
||||
- Fix conflicting shortcuts (remove dec/inc line height and letter spacing) [Taiga #12102](https://tree.taiga.io/project/penpot/issue/12102)
|
||||
- Fix conflicting shortcuts (remove text-align shortcuts) [Taiga #12047](https://tree.taiga.io/project/penpot/issue/12047)
|
||||
- Fix export file with empty tokens library [Taiga #12137](https://tree.taiga.io/project/penpot/issue/12137)
|
||||
- Fix context menu on spacing tokens [Taiga #12141](https://tree.taiga.io/project/penpot/issue/12141)
|
||||
|
||||
## 2.9.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
@@ -8,23 +119,67 @@
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Clarify message when inviting existing team members to make it more user-friendly and clear which invitations will be sent. [Taiga #11441](https://tree.taiga.io/project/penpot/issue/11441) by [@iprithvitharun](https://github.com/iprithvitharun)
|
||||
- Update email change confirmation message for clarity and correct grammar. [GitHub #6786](https://github.com/penpot/penpot/issues/6786) by [@iprithvitharun](https://github.com/iprithvitharun)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Add visual indicator for new comments in the workspace [Taiga #11328](https://tree.taiga.io/project/penpot/issue/11328)
|
||||
- On components overrides, separate the content of the text from the rest of properties [Taiga #7434](https://tree.taiga.io/project/penpot/us/7434)
|
||||
- Improve dashboard's sidebar [Taiga #10700](https://tree.taiga.io/project/penpot/us/10700)
|
||||
- Change "Save color" button to primary button [Taiga #9410](https://tree.taiga.io/project/penpot/issue/9410)
|
||||
- Support for exif rotated images [GitHub #6767](https://github.com/penpot/penpot/issues/6767)
|
||||
- Display Blend Mode and Layer Opacity properties in the Inspect tab [Taiga #11283](https://tree.taiga.io/project/penpot/issue/11283)
|
||||
- Provide CSS `mix-blend-mode` property in code editor when present on shape [Taiga #11282](https://tree.taiga.io/project/penpot/issue/11282)
|
||||
- Add the option to import tokens in a .zip file. [Taiga #11378](https://tree.taiga.io/project/penpot/us/11378)
|
||||
- New typography token type - font size token [Taiga #10938](https://tree.taiga.io/project/penpot/us/10938)
|
||||
- Hide bounding box while editing visual effects [Taiga #11576](https://tree.taiga.io/project/penpot/issue/11576)
|
||||
- Improved text layer resizing: Allow double-click on text bounding box to set auto-width/auto-height [Taiga #11577](https://tree.taiga.io/project/penpot/issue/11577)
|
||||
- Improve text layer auto-resize: auto-width switches to auto-height on horizontal resize, and only switches to fixed on vertical resize [Taiga #11578](https://tree.taiga.io/project/penpot/issue/11578)
|
||||
- Add the ability to show login dialog on profile settings [Github #6871](https://github.com/penpot/penpot/pull/6871)
|
||||
- Improve the application of tokens with object specific tokens [Taiga #10209](https://tree.taiga.io/project/penpot/us/10209)
|
||||
- Add info to apply-token event [Taiga #11710](https://tree.taiga.io/project/penpot/task/11710)
|
||||
- Fix double click on set name input [Taiga #11747](https://tree.taiga.io/project/penpot/issue/11747)
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Copying font size does not copy the unit [Taiga #11143](https://tree.taiga.io/project/penpot/issue/11143)
|
||||
- Fix text-decoration line-through that displays a wrong property value [Taiga #11145](https://tree.taiga.io/project/penpot/issue/11145)
|
||||
- Fix display error message on register form [Taiga #11444](https://tree.taiga.io/project/penpot/issue/11444)
|
||||
- Fix toggle focus mode did not restore viewport and selection upon exit [GitHub #6280](https://github.com/penpot/penpot/issues/6820)
|
||||
- Fix problem when creating a layout from an existing layout [Taiga #11554](https://tree.taiga.io/project/penpot/issue/11554)
|
||||
- Fix title button from Title Case to Capitalize [Taiga #11476](https://tree.taiga.io/project/penpot/issue/11476)
|
||||
- Fix touchpad swipe leading to navigating back/forth [GitHub #4246](https://github.com/penpot/penpot/issues/4246)
|
||||
- Keep color data when copying from info tab into CSS [Taiga #11144](https://tree.taiga.io/project/penpot/issue/11144)
|
||||
- Update HSL values to modern syntax as defined in W3C CSS Color Module Level 4 [Taiga #11144](https://tree.taiga.io/project/penpot/issue/11144)
|
||||
- Fix main component receives focus and is selected when using 'Show Main Component' [Taiga #11402](https://tree.taiga.io/project/penpot/issue/11402)
|
||||
- Fix UI theme selection from main menu [Taiga #11567](https://tree.taiga.io/project/penpot/issue/11567)
|
||||
- Fix duplicating pages with mainInstance shapes nested inside groups [Taiga #10774](https://tree.taiga.io/project/penpot/issue/10774)
|
||||
- Fix ESC key not closing Add/Manage Libraries modal [Taiga #11523](https://tree.taiga.io/project/penpot/issue/11523)
|
||||
- Fix copying a shadow color from info tab [Taiga #11211](https://tree.taiga.io/project/penpot/issue/11211)
|
||||
- Fix remove color button in the gradient editor [Taiga #11623](https://tree.taiga.io/project/penpot/issue/11623)
|
||||
- Fix "Copy as SVG" generates different code from the Inspect panel [Taiga #11519](https://tree.taiga.io/project/penpot/issue/11519)
|
||||
- Fix overriden tokens in text copies are not preserved [Taiga #11486](https://tree.taiga.io/project/penpot/issue/11486)
|
||||
- Fix problem when changing between flex/grid layout [Taiga #11625](https://tree.taiga.io/project/penpot/issue/11625)
|
||||
- Fix opacity on stroke gradients [Taiga #11646](https://tree.taiga.io/project/penpot/issue/11646)
|
||||
- Fix change from gradient to solid color [Taiga #11648](https://tree.taiga.io/project/penpot/issue/11648)
|
||||
- Fix the context menu always closes after any action [Taiga #11624](https://tree.taiga.io/project/penpot/issue/11624)
|
||||
- Fix X & Y position do not sincronize with tokens [Taiga #11617](https://tree.taiga.io/project/penpot/issue/11617)
|
||||
- Fix tooltip position after first time [Taiga #11688](https://tree.taiga.io/project/penpot/issue/11688)
|
||||
- Fix inconsistent ordering of pinned projects on dashboard sidebar [Taiga #11674](https://tree.taiga.io/project/penpot/issue/11674)
|
||||
- Fix export button width on inspect tab [Taiga #11394](https://tree.taiga.io/project/penpot/issue/11394)
|
||||
- Fix stroke width token application [Taiga #11724](https://tree.taiga.io/project/penpot/issue/11724)
|
||||
- Fix number token application on shape [Taiga #11331](https://tree.taiga.io/project/penpot/task/11331)
|
||||
- Fix auto height is fixed in the HTML inspect tab for text elements [Taiga #11680](https://tree.taiga.io/project/penpot/task/11680)
|
||||
|
||||
## 2.8.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix unexpected exception on processing old texts [Github #6889](https://github.com/penpot/penpot/pull/6889)
|
||||
- Fix error on inspect tab when selecting multiple shapes [Taiga #11655](https://tree.taiga.io/project/penpot/issue/11655)
|
||||
- Fix missing package for the penport_exporter Docker image [GitHub #7205](https://github.com/penpot/penpot/issues/7025)
|
||||
|
||||
## 2.8.0
|
||||
|
||||
@@ -34,7 +189,7 @@
|
||||
|
||||
**Penpot Library**
|
||||
|
||||
The initial prototype is completly reworked for provide a more consistent API
|
||||
The initial prototype is completly reworked to provide a more consistent API
|
||||
and to have proper validation and params decoding. All the details can be found
|
||||
on [its own changelog](library/CHANGES.md)
|
||||
|
||||
@@ -46,6 +201,7 @@ in future versions. Therefore, **migration from Redis to ValKey is recommended f
|
||||
on-premises instances** that want to keep up to date.
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Add Serbian language [GitHub #5002](https://github.com/penpot/penpot/issues/5002) by [crnobog69](https://github.com/crnobog69)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
@@ -101,7 +257,6 @@ on-premises instances** that want to keep up to date.
|
||||
- Fix copy in error message [GitHub #6615](https://github.com/penpot/penpot/pull/6615)
|
||||
- Fix url on invitation link [Taiga #11284](https://tree.taiga.io/project/penpot/issue/11284)
|
||||
|
||||
|
||||
## 2.7.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -109,7 +264,6 @@ on-premises instances** that want to keep up to date.
|
||||
- Fix incorrect handling of strokes with images on importing files
|
||||
- Fix tokens disappearing after manual additions [Taiga #11063](https://tree.taiga.io/project/penpot/issue/11063)
|
||||
|
||||
|
||||
## 2.7.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -241,7 +395,6 @@ on-premises instances** that want to keep up to date.
|
||||
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
|
||||
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
|
||||
|
||||
|
||||
## 2.5.4
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
@@ -286,7 +439,7 @@ on-premises instances** that want to keep up to date.
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
Although this is not a breaking change, we believe it’s important to highlight it in this
|
||||
Although this is not a breaking change, we believe it's important to highlight it in this
|
||||
section:
|
||||
|
||||
This release includes a fix for an internal bug in Penpot that caused incorrect handling
|
||||
@@ -294,9 +447,9 @@ of media assets (e.g., fill images). The issue has been resolved since version 2
|
||||
no new incorrect references will be generated. However, existing files may still contain
|
||||
incorrect references.
|
||||
|
||||
To address this, we’ve provided a script to correct these references in existing files.
|
||||
To address this, we've provided a script to correct these references in existing files.
|
||||
|
||||
While having incorrect references generally doesn’t result in visible issues, there are
|
||||
While having incorrect references generally doesn't result in visible issues, there are
|
||||
rare cases where it can cause problems. For example, if a component library (containing
|
||||
images) is deleted, and that library is being used in other files, running the FileGC task
|
||||
(responsible for freeing up space and performing logical deletions) could leave those
|
||||
@@ -371,7 +524,6 @@ is a number of cores)
|
||||
- Fix missing methods reference on API Docs
|
||||
- Fix memory usage issue on file-gc asynchronous task (related to snapshots feature)
|
||||
|
||||
|
||||
## 2.4.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -379,7 +531,6 @@ is a number of cores)
|
||||
- Fix error when importing files with touched components [Taiga #9625](https://tree.taiga.io/project/penpot/issue/9625)
|
||||
- Fix problem when changing color libraries [Plugins #184](https://github.com/penpot/penpot-plugins/issues/184)
|
||||
|
||||
|
||||
## 2.4.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -433,7 +584,6 @@ is a number of cores)
|
||||
|
||||
- Add initial documentation for Kubernetes
|
||||
|
||||
|
||||
## 2.3.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -441,7 +591,6 @@ is a number of cores)
|
||||
- Fix unexpected issue on interaction between plugins sandbox and
|
||||
internal impl of promise
|
||||
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -467,7 +616,6 @@ is a number of cores)
|
||||
|
||||
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with constraints buttons [Taiga #8465](https://tree.taiga.io/project/penpot/issue/8465)
|
||||
@@ -507,8 +655,8 @@ is a number of cores)
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- Removed "merge assets" option when exporting ".svg + .json" files. After the components changes the option wasn't
|
||||
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
|
||||
time being.
|
||||
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
|
||||
time being.
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
@@ -524,7 +672,7 @@ time being.
|
||||
freeing up space in the database. It can be enabled with the
|
||||
`enable-enable-tiered-file-data-storage` flag.
|
||||
|
||||
*(On-Premise feature, EXPERIMENTAL).*
|
||||
_(On-Premise feature, EXPERIMENTAL)._
|
||||
|
||||
- **JSON Interoperability for HTTP API** [Taiga #8372](https://tree.taiga.io/project/penpot/us/8372)
|
||||
|
||||
@@ -567,7 +715,7 @@ time being.
|
||||
|
||||
- **Design System**
|
||||
|
||||
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
|
||||
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
|
||||
|
||||
- **Storybook** [Taiga #6329](https://tree.taiga.io/project/penpot/us/6329)
|
||||
|
||||
@@ -622,11 +770,11 @@ time being.
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
|
||||
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix the “search” label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
|
||||
- Fix the "search" label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
|
||||
- Fix pencil loader [Taiga #8348](https://tree.taiga.io/project/penpot/issue/8348)
|
||||
- Fix several issues on the OIDC.
|
||||
- Fix regression on the `email-verification` flag [Taiga #8398](https://tree.taiga.io/project/penpot/issue/8398)
|
||||
@@ -706,22 +854,21 @@ time being.
|
||||
- Fix color palette sorting [Taiga #7458](https://tree.taiga.io/project/penpot/issue/7458)
|
||||
- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671)
|
||||
|
||||
|
||||
## 2.0.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
|
||||
|
||||
|
||||
## 2.0.0 - I Just Can't Get Enough
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- Grid CSS layout [Taiga #4915](https://tree.taiga.io/project/penpot/epic/4915)
|
||||
- UI redesign [Taiga #4958](https://tree.taiga.io/project/penpot/epic/4958)
|
||||
- New components System [Taiga #2662](https://tree.taiga.io/project/penpot/epic/2662)
|
||||
- Swap components [Taiga #1331](https://tree.taiga.io/project/penpot/us/1331)
|
||||
- Images as fill [Taiga #2983](https://tree.taiga.io/project/penpot/us/2983)
|
||||
- Images as fill [Taiga #2983](https://tree.taiga.io/project/penpot/us/2983)
|
||||
- HTML code generation [Taiga #5277](https://tree.taiga.io/project/penpot/us/5277)
|
||||
- Light and dark themes [Taiga #2287](https://tree.taiga.io/project/penpot/us/2287)
|
||||
|
||||
@@ -730,9 +877,9 @@ time being.
|
||||
- New strokes default to inside border [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
|
||||
- Change default z ordering on layers in flex layout. The previous behavior was inconsistent with how HTML works and we changed it to be more consistent. Previous layers that overlapped could be hidden, the fastest way to fix this is changing the z-index property but a better way is to change the order of your layers.
|
||||
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
|
||||
|
||||
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
|
||||
- Hide bounding-box when editing shape (by @VasilevsVV) [#3930](https://github.com/penpot/penpot/pull/3930)
|
||||
- CTRL + "+" to zoom into canvas instead of browser (by @audriu) [#3848](https://github.com/penpot/penpot/pull/3848)
|
||||
- Add dev deps.edn in the project root (by @PEZ) [#3794](https://github.com/penpot/penpot/pull/3794)
|
||||
@@ -741,6 +888,7 @@ time being.
|
||||
- Typo (by StephanEggermont) [#157](https://github.com/penpot/penpot-docs/pull/157)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Send comments with Ctrl+Enter / Cmd + Enter [Taiga #6085](https://tree.taiga.io/project/penpot/issue/6085)
|
||||
- Select through stroke only rectangle [Taiga #5484](https://tree.taiga.io/project/penpot/issue/5484)
|
||||
- Stroke default position [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
|
||||
@@ -808,6 +956,7 @@ time being.
|
||||
- [REDESIGN] Onboarding slides [Taiga #6678](https://tree.taiga.io/project/penpot/us/6678)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix pixelated thumbnails [Github #3681](https://github.com/penpot/penpot/issues/3681), [Github #3661](https://github.com/penpot/penpot/issues/3661)
|
||||
- Fix problem with not applying colors to boards [Github #3941](https://github.com/penpot/penpot/issues/3941)
|
||||
- Fix problem with path editor undoing changes [Github #3998](https://github.com/penpot/penpot/issues/3998)
|
||||
@@ -816,7 +965,7 @@ time being.
|
||||
- Selecting from Color Palette does not work for board when there is no existing fill [Taiga #6464](https://tree.taiga.io/project/penpot/issue/6464)
|
||||
- Color thumbnails are consistently rounded in the inspect code mode [Taiga #5886](https://tree.taiga.io/project/penpot/issue/5886)
|
||||
- Adding vector path points before first point of existing open path not working [Taiga #6593](https://tree.taiga.io/project/penpot/issue/6593)
|
||||
- Some image formats include the extension when importing [Taiga #5485](https://tree.taiga.io/project/penpot/issue/5485)
|
||||
- Some image formats include the extension when importing [Taiga #5485](https://tree.taiga.io/project/penpot/issue/5485)
|
||||
- Gradient color tool doesn't work properly with flipped items [Taiga #6485](https://tree.taiga.io/project/penpot/issue/6485)
|
||||
- [TEXT] Align options are not shown when several text are selected [Taiga #5948](https://tree.taiga.io/project/penpot/issue/5948)
|
||||
- [VIEW MODE] Comments not working properly on multiple pages [Taiga #6281](https://tree.taiga.io/project/penpot/issue/6281)
|
||||
@@ -860,7 +1009,7 @@ time being.
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Improve selected colors [Taiga #5805]( https://tree.taiga.io/project/penpot/us/5805)
|
||||
- Improve selected colors [Taiga #5805](https://tree.taiga.io/project/penpot/us/5805)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -895,7 +1044,6 @@ time being.
|
||||
- Fix deleted pages comments shown in right sidebar [Taiga #5648](https://tree.taiga.io/project/penpot/us/5648)
|
||||
- Fix tooltip on toggle visibility and toggle lock buttons [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
|
||||
|
||||
|
||||
## 1.19.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -1009,7 +1157,6 @@ time being.
|
||||
|
||||
- Update google fonts catalog (at 2023/07/06) [Taiga #5592](https://tree.taiga.io/project/penpot/issue/5592)
|
||||
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- Update Typography palette order (by @akshay-gupta7) [Github #3156](https://github.com/penpot/penpot/pull/3156)
|
||||
@@ -1163,12 +1310,14 @@ time being.
|
||||
- Fix problem with opacity in imported SVG's [Taiga #4923](https://tree.taiga.io/project/penpot/issue/4923)
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To @ondrejkonec: for contributing to the code with:
|
||||
- Refactor CSS variables [Github #2948](https://github.com/penpot/penpot/pull/2948)
|
||||
|
||||
## 1.17.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix copy and paste very nested inside itself [Taiga #4848](https://tree.taiga.io/project/penpot/issue/4848)
|
||||
- Fix custom fonts not rendered correctly [Taiga #4874](https://tree.taiga.io/project/penpot/issue/4874)
|
||||
- Fix problem with shadows and blur on multiple selection
|
||||
@@ -1201,6 +1350,7 @@ time being.
|
||||
## 1.17.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix components groups items show the component name in list mode [Taiga #4770](https://tree.taiga.io/project/penpot/issue/4770)
|
||||
- Fix typing CMD+Z on MacOS turns the cursor into a Zoom cursor [Taiga #4778](https://tree.taiga.io/project/penpot/issue/4778)
|
||||
- Fix white space on small screens [Taiga #4774](https://tree.taiga.io/project/penpot/issue/4774)
|
||||
@@ -1315,7 +1465,7 @@ time being.
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- Removed the support for v2 internal file data blob format. This
|
||||
- Removed the support for v2 internal file data blob format. This
|
||||
version has never been documented nor set as default value so
|
||||
technically this is not a breaking change because we are removing
|
||||
a "private API".
|
||||
@@ -1420,7 +1570,6 @@ time being.
|
||||
- Fix when ungrouping, the items previously grouped should ALWAYS remain selected [Taiga #4064](https://tree.taiga.io/project/penpot/issue/4064)
|
||||
- Change shortcut for "Clear undo" [#2219](https://github.com/penpot/penpot/issues/2219)
|
||||
|
||||
|
||||
## 1.15.2-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -1504,6 +1653,7 @@ time being.
|
||||
- Fix bringing complete file data when launching the export dialog [Taiga #4006](https://tree.taiga.io/project/penpot/issue/4006)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
## 1.14.2-beta
|
||||
@@ -1544,10 +1694,10 @@ time being.
|
||||
- Prototype connection should be under the rules [Taiga #3384](https://tree.taiga.io/project/penpot/issue/3384)
|
||||
- Fix problem with empty text boxes events [Taiga #3627](https://tree.taiga.io/project/penpot/issue/3627)
|
||||
|
||||
|
||||
## 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)
|
||||
- Fix background on export arboards [Taiga #1991](https://tree.taiga.io/project/penpot/issue/1991)
|
||||
|
||||
@@ -1691,6 +1841,7 @@ time being.
|
||||
- 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
|
||||
@@ -1708,7 +1859,7 @@ time being.
|
||||
### :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 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
|
||||
@@ -1726,7 +1877,6 @@ time being.
|
||||
- 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
|
||||
|
||||
@@ -77,17 +77,14 @@ Provide your team or organization with a completely owned collaborative design t
|
||||
### Integrations ###
|
||||
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
|
||||
|
||||
### What’s great for design ###
|
||||
With Penpot you can design libraries to share and reuse; turn design elements into components and tokens to allow reusability and scalability; and build realistic user flows and interactions.
|
||||
|
||||
### Design Tokens ###
|
||||
With Penpot’s standardized [design tokens](https://penpot.dev/collaboration/design-tokens) format, you can easily reuse and sync tokens across different platforms, workflows, and disciplines.
|
||||
### Building Design Systems: design tokens, components and variants ###
|
||||
Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms.
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.plasmic.app/img-optimizer/v1/img?src=https%3A%2F%2Fimg.plasmic.app%2Fimg-optimizer%2Fv1%2Fimg%2F9dd677c36afb477e9666ccd1d3f009ad.png" alt="Open Source" style="width: 65%;">
|
||||
<img src="https://github.com/user-attachments/assets/cce75ad6-f783-473f-8803-da9eb8255fef">
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
:deps
|
||||
{penpot/common {:local/root "../common"}
|
||||
org.clojure/clojure {:mvn/version "1.12.1"}
|
||||
org.clojure/clojure {:mvn/version "1.12.2"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-3"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "6.7.0.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.8.1.RELEASE"}
|
||||
;; Minimal dependencies required by lettuce, we need to include them
|
||||
;; explicitly because clojure dependency management does not support
|
||||
;; yet the BOM format.
|
||||
@@ -28,29 +28,30 @@
|
||||
com.google.guava/guava {:mvn/version "33.4.8-jre"}
|
||||
|
||||
funcool/yetti
|
||||
{:git/tag "v11.4"
|
||||
:git/sha "ce50d42"
|
||||
{:git/tag "v11.6"
|
||||
:git/sha "94dc017"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
com.github.seancorfield/next.jdbc
|
||||
{:mvn/version "1.3.1002"}
|
||||
{:mvn/version "1.3.1070"}
|
||||
|
||||
metosin/reitit-core {:mvn/version "0.9.1"}
|
||||
nrepl/nrepl {:mvn/version "1.3.1"}
|
||||
nrepl/nrepl {:mvn/version "1.4.0"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.6"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
|
||||
org.postgresql/postgresql {:mvn/version "42.7.7"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
|
||||
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
|
||||
|
||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||
|
||||
buddy/buddy-hashers {:mvn/version "2.0.167"}
|
||||
buddy/buddy-sign {:mvn/version "3.6.1-359"}
|
||||
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.0"}
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.2"}
|
||||
|
||||
org.jsoup/jsoup {:mvn/version "1.20.1"}
|
||||
org.jsoup/jsoup {:mvn/version "1.21.2"}
|
||||
org.im4java/im4java
|
||||
{:git/tag "1.4.0-penpot-2"
|
||||
:git/sha "e2b3e16"
|
||||
@@ -60,12 +61,12 @@
|
||||
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||
|
||||
dawran6/emoji {:mvn/version "0.1.5"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.3"}
|
||||
dawran6/emoji {:mvn/version "0.2.0"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.4"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.31.55"}}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.33.10"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
@@ -80,12 +81,14 @@
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
|
||||
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
{:main-opts ["-m" "kaocha.runner"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
|
||||
"--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
|
||||
|
||||
:outdated
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
|
||||
(ns user
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data :as d]
|
||||
[app.common.debug :as debug]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.perf :as perf]
|
||||
[app.common.pprint :as pp]
|
||||
@@ -19,8 +21,9 @@
|
||||
[app.common.schema.desc-js-like :as smdj]
|
||||
[app.common.schema.desc-native :as smdn]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as oapi]
|
||||
[app.common.spec :as us]
|
||||
[app.common.json :as json]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -30,9 +33,9 @@
|
||||
[app.srepl.helpers :as srepl.helpers]
|
||||
[app.srepl.main :as srepl]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.time :as dt]
|
||||
[clj-async-profiler.core :as prof]
|
||||
[clojure.contrib.humanize :as hum]
|
||||
[clojure.datafy :refer [datafy]]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.pprint :refer [pprint print-table]]
|
||||
[clojure.repl :refer :all]
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Click to the link below to confirm the change:</div>
|
||||
Click the link below to confirm the change.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -217,8 +217,7 @@
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
If you received this email by mistake, please consider changing your password for security
|
||||
reasons.</div>
|
||||
If you did not request this change, consider changing your password for security reasons.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -2,12 +2,11 @@ Hello {{name|abbreviate:25}}!
|
||||
|
||||
We received a request to change your current email to {{ pending-email }}.
|
||||
|
||||
Click to the link below to confirm the change:
|
||||
Click the link below to confirm the change.
|
||||
|
||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||
|
||||
If you received this email by mistake, please consider changing your password
|
||||
for security reasons.
|
||||
If you did not request this change, consider changing your password for security reasons.
|
||||
|
||||
Enjoy!
|
||||
The Penpot team.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Invitation to join {{team}}
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”
|
||||
@@ -17,38 +17,6 @@ Debug Main Page
|
||||
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Download file data:</legend>
|
||||
<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">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="submit" name="download" value="Download" />
|
||||
<input type="submit" name="clone" value="Clone" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Upload File Data:</legend>
|
||||
<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">
|
||||
<div class="row">
|
||||
<input type="file" name="file" value="" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Import with same id?</label>
|
||||
<input type="checkbox" name="reuseid" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Upload" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Profile Management</legend>
|
||||
<form method="post" action="/dbg/actions/resend-email-verification">
|
||||
@@ -81,6 +49,50 @@ Debug Main Page
|
||||
|
||||
</section>
|
||||
|
||||
<section class="widget">
|
||||
|
||||
<fieldset>
|
||||
<legend>Download RAW file data:</legend>
|
||||
<desc>Given an FILE-ID, downloads the file AS-IS (no validation
|
||||
checks, just exports the file data and related objects in raw)
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<b>WARNING: this operation does not performs any checks</b>
|
||||
</desc>
|
||||
<form method="get" action="/dbg/actions/file-raw-export-import">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="submit" name="download" value="Download" />
|
||||
<input type="submit" name="clone" value="Clone" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Upload File Data:</legend>
|
||||
<desc>Create a new file on your draft projects using the file downloaded from the previous section.
|
||||
<br/>
|
||||
<br/>
|
||||
<b>WARNING: this operation does not performs any checks</b>
|
||||
</desc>
|
||||
<form method="post" enctype="multipart/form-data" action="/dbg/actions/file-raw-export-import">
|
||||
<div class="row">
|
||||
<input type="file" name="file" value="" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Import with same id?</label>
|
||||
<input type="checkbox" name="reuseid" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Upload" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</section>
|
||||
<section class="widget">
|
||||
<fieldset>
|
||||
<legend>Export binfile:</legend>
|
||||
@@ -88,7 +100,7 @@ Debug Main Page
|
||||
the related libraries in a single custom formatted binary
|
||||
file.</desc>
|
||||
|
||||
<form method="get" action="/dbg/file/export">
|
||||
<form method="get" action="/dbg/actions/file-export">
|
||||
<div class="row set-of-inputs">
|
||||
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
|
||||
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
|
||||
@@ -116,7 +128,7 @@ Debug Main Page
|
||||
<legend>Import binfile:</legend>
|
||||
<desc>Import penpot file in binary format.</desc>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" action="/dbg/file/import">
|
||||
<form method="post" enctype="multipart/form-data" action="/dbg/actions/file-import">
|
||||
<div class="row">
|
||||
<input type="file" name="file" value="" />
|
||||
</div>
|
||||
@@ -130,79 +142,27 @@ Debug Main Page
|
||||
|
||||
<section class="widget">
|
||||
<fieldset>
|
||||
<legend>Reset file version</legend>
|
||||
<desc>Allows reset file data version to a specific number/</desc>
|
||||
|
||||
<form method="post" action="/dbg/actions/reset-file-version">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="number" style="width:100px" name="version" placeholder="version" value="32" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-version">Are you sure?</label>
|
||||
<input id="force-version" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<section class="widget">
|
||||
<h2>Feature Flags</h2>
|
||||
<fieldset>
|
||||
<legend>Enable</legend>
|
||||
<legend>Feature Flags for Team</legend>
|
||||
<desc>Add a feature flag to a team</desc>
|
||||
<form method="post" action="/dbg/actions/add-team-feature">
|
||||
<form method="post" action="/dbg/actions/handle-team-features">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
|
||||
<select type="text" style="width:100px" name="feature">
|
||||
{% for feature in supported-features %}
|
||||
<option value="{{feature}}">{{feature}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="check-feature">Skip feature check</label>
|
||||
<input id="check-feature" type="checkbox" name="skip-check" />
|
||||
<br />
|
||||
<small>
|
||||
Do not check if the feature is supported
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-version">Are you sure?</label>
|
||||
<input id="force-version" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Disable</legend>
|
||||
<desc>Remove a feature flag from a team</desc>
|
||||
<form method="post" action="/dbg/actions/remove-team-feature">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
|
||||
<select style="width:100px" name="action">
|
||||
<option value="">Action...</option>
|
||||
<option value="show">Show</option>
|
||||
<option value="enable">Enable</option>
|
||||
<option value="disable">Disable</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -7,7 +7,9 @@ penpot - error list
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div class="title">
|
||||
<h1>Error reports (last 200)</h1>
|
||||
<h1>Error reports (last 200)
|
||||
<a href="/dbg">[GO BACK]</a>
|
||||
</h1>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="horizontal-list">
|
||||
|
||||
@@ -12,7 +12,7 @@ export PENPOT_FLAGS="\
|
||||
enable-login-with-gitlab \
|
||||
enable-backend-worker \
|
||||
enable-backend-asserts \
|
||||
enable-feature-fdata-pointer-map \
|
||||
disable-feature-fdata-pointer-map \
|
||||
enable-feature-fdata-objects-map \
|
||||
enable-audit-log \
|
||||
enable-transit-readable-response \
|
||||
@@ -28,11 +28,10 @@ export PENPOT_FLAGS="\
|
||||
enable-auto-file-snapshot \
|
||||
enable-webhooks \
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
disable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptions \
|
||||
enable-subscriptions-old";
|
||||
enable-subscriptions";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
@@ -78,10 +77,14 @@ export JAVA_OPTS="\
|
||||
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-Dim4java.useV7=true \
|
||||
-XX:+UseShenandoahGC \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockExperimentalVMOptions \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints \
|
||||
-XX:ShenandoahGCMode=generational \
|
||||
-XX:+UseCompactObjectHeaders \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
@@ -13,7 +13,7 @@ export PENPOT_FLAGS="\
|
||||
enable-login-with-ldap \
|
||||
enable-transit-readable-response \
|
||||
enable-demo-users \
|
||||
enable-feature-fdata-pointer-map \
|
||||
disable-feature-fdata-pointer-map \
|
||||
enable-feature-fdata-objects-map \
|
||||
disable-secure-session-cookies \
|
||||
enable-rpc-climit \
|
||||
@@ -21,11 +21,10 @@ export PENPOT_FLAGS="\
|
||||
enable-quotes \
|
||||
enable-file-snapshot \
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
disable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation \
|
||||
enable-subscriptions \
|
||||
enable-subscriptions-old ";
|
||||
enable-subscriptions";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -28,7 +29,6 @@
|
||||
[app.tokens :as tokens]
|
||||
[app.util.inet :as inet]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[buddy.sign.jwk :as jwk]
|
||||
[buddy.sign.jwt :as jwt]
|
||||
[clojure.set :as set]
|
||||
@@ -514,7 +514,7 @@
|
||||
[cfg info request]
|
||||
(let [info (assoc info
|
||||
:iss :prepared-register
|
||||
:exp (dt/in-future {:hours 48}))
|
||||
:exp (ct/in-future {:hours 48}))
|
||||
|
||||
params {:token (tokens/generate (::setup/props cfg) info)
|
||||
:provider (:provider (:path-params request))
|
||||
@@ -571,7 +571,7 @@
|
||||
token (or (:invitation-token info)
|
||||
(tokens/generate (::setup/props cfg)
|
||||
{:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:exp (ct/in-future "15m")
|
||||
:profile-id (:id profile)}))
|
||||
props (audit/profile->props profile)
|
||||
context (d/without-nils {:external-session-id (:external-session-id info)})]
|
||||
@@ -619,7 +619,7 @@
|
||||
:invitation-token (:invitation-token params)
|
||||
:external-session-id esid
|
||||
:props props
|
||||
:exp (dt/in-future "4h")}
|
||||
:exp (ct/in-future "4h")}
|
||||
state (tokens/generate (::setup/props cfg)
|
||||
(d/without-nils params))
|
||||
uri (build-auth-uri cfg state)]
|
||||
|
||||
@@ -15,19 +15,21 @@
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.files.validate :as fval]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.weak :as weak]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.features.file-migrations :as feat.fmigr]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.features.file-migrations :as fmigr]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
@@ -38,6 +40,7 @@
|
||||
|
||||
(def ^:dynamic *state* nil)
|
||||
(def ^:dynamic *options* nil)
|
||||
(def ^:dynamic *reference-file* nil)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; DEFAULTS
|
||||
@@ -53,17 +56,12 @@
|
||||
(* 1024 1024 100))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare get-resolved-file-libraries)
|
||||
(declare update-file!)
|
||||
|
||||
(def file-attrs
|
||||
#{:id
|
||||
:name
|
||||
:migrations
|
||||
:features
|
||||
:project-id
|
||||
:is-shared
|
||||
:version
|
||||
:data})
|
||||
(sm/keys ctf/schema:file))
|
||||
|
||||
(defn parse-file-format
|
||||
[template]
|
||||
@@ -151,24 +149,35 @@
|
||||
changes (assoc :changes (blob/decode changes))
|
||||
data (assoc :data (blob/decode data)))))
|
||||
|
||||
(def sql:get-minimal-file
|
||||
"SELECT f.id,
|
||||
f.revn,
|
||||
f.modified_at,
|
||||
f.deleted_at
|
||||
FROM file AS f
|
||||
WHERE f.id = ?")
|
||||
|
||||
(defn get-minimal-file
|
||||
[cfg id & {:as opts}]
|
||||
(db/get-with-sql cfg [sql:get-minimal-file id] opts))
|
||||
|
||||
(defn decode-file
|
||||
"A general purpose file decoding function that resolves all external
|
||||
pointers, run migrations and return plain vanilla file map"
|
||||
[cfg {:keys [id] :as file}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
[cfg {:keys [id] :as file} & {:keys [migrate?] :or {migrate? true}}]
|
||||
(binding [pmap/*load-fn* (partial fdata/load-pointer cfg id)]
|
||||
(let [file (->> file
|
||||
(feat.fmigr/resolve-applied-migrations cfg)
|
||||
(feat.fdata/resolve-file-data cfg))
|
||||
(fmigr/resolve-applied-migrations cfg)
|
||||
(fdata/resolve-file-data cfg))
|
||||
libs (delay (get-resolved-file-libraries cfg file))]
|
||||
|
||||
(-> file
|
||||
(update :features db/decode-pgarray #{})
|
||||
(update :data blob/decode)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(update :data fdata/process-pointers deref)
|
||||
(update :data fdata/process-objects (partial into {}))
|
||||
(update :data assoc :id id)
|
||||
(fmg/migrate-file libs)))))
|
||||
(cond-> migrate? (fmg/migrate-file libs))))))
|
||||
|
||||
(defn get-file
|
||||
"Get file, resolve all features and apply migrations.
|
||||
@@ -178,9 +187,9 @@
|
||||
and decoding."
|
||||
[cfg file-id & {:as opts}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(some->> (db/get* conn :file {:id file-id}
|
||||
(assoc opts ::db/remove-deleted false))
|
||||
(decode-file cfg)))))
|
||||
(when-let [row (db/get* conn :file {:id file-id}
|
||||
(assoc opts ::db/remove-deleted false))]
|
||||
(decode-file cfg row opts)))))
|
||||
|
||||
(defn clean-file-features
|
||||
[file]
|
||||
@@ -421,6 +430,27 @@
|
||||
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"])
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])))
|
||||
|
||||
(defn invalidate-thumbnails
|
||||
[cfg file-id]
|
||||
(let [storage (sto/resolve cfg)
|
||||
|
||||
sql-1
|
||||
(str "update file_tagged_object_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")
|
||||
|
||||
sql-2
|
||||
(str "update file_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")]
|
||||
|
||||
(run! #(sto/touch-object! storage %)
|
||||
(sequence
|
||||
(keep :media-id)
|
||||
(concat
|
||||
(db/exec! cfg [sql-1 file-id])
|
||||
(db/exec! cfg [sql-2 file-id]))))))
|
||||
|
||||
(defn process-file
|
||||
[cfg {:keys [id] :as file}]
|
||||
(let [libs (delay (get-resolved-file-libraries cfg file))]
|
||||
@@ -445,77 +475,78 @@
|
||||
(vary-meta dissoc ::fmg/migrated))))
|
||||
|
||||
(defn encode-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id features] :as file}]
|
||||
(let [file (if (contains? features "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
[cfg {:keys [id features] :as file}]
|
||||
(let [file (if (and (contains? features "fdata/objects-map")
|
||||
(:data file))
|
||||
(fdata/enable-objects-map file)
|
||||
file)
|
||||
|
||||
file (if (contains? features "fdata/pointer-map")
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [file (feat.fdata/enable-pointer-map file)]
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
file (if (and (contains? features "fdata/pointer-map")
|
||||
(:data file))
|
||||
|
||||
(binding [pmap/*tracked* (pmap/create-tracked :inherit true)]
|
||||
(let [file (fdata/enable-pointer-map file)]
|
||||
(fdata/persist-pointers! cfg id)
|
||||
file))
|
||||
file)]
|
||||
|
||||
(-> file
|
||||
(update :features db/encode-pgarray conn "text")
|
||||
(update :data blob/encode))))
|
||||
(d/update-when :features into-array)
|
||||
(d/update-when :data blob/encode))))
|
||||
|
||||
(defn get-params-from-file
|
||||
(defn- file->params
|
||||
[file]
|
||||
(let [params {:has-media-trimmed (:has-media-trimmed file)
|
||||
:ignore-sync-until (:ignore-sync-until file)
|
||||
:project-id (:project-id file)
|
||||
:features (:features file)
|
||||
:name (:name file)
|
||||
:is-shared (:is-shared file)
|
||||
:version (:version file)
|
||||
:data (:data file)
|
||||
:id (:id file)
|
||||
:deleted-at (:deleted-at file)
|
||||
:created-at (:created-at file)
|
||||
:modified-at (:modified-at file)
|
||||
:revn (:revn file)
|
||||
:vern (:vern file)}]
|
||||
|
||||
(-> (d/without-nils params)
|
||||
(assoc :data-backend nil)
|
||||
(assoc :data-ref-id nil))))
|
||||
(-> (select-keys file file-attrs)
|
||||
(dissoc :team-id)
|
||||
(dissoc :migrations)))
|
||||
|
||||
(defn insert-file!
|
||||
"Insert a new file into the database table"
|
||||
"Insert a new file into the database table. Expectes a not-encoded file.
|
||||
Returns nil."
|
||||
[{:keys [::db/conn] :as cfg} file & {:as opts}]
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(let [params (-> (encode-file cfg file)
|
||||
(get-params-from-file))]
|
||||
(db/insert! conn :file params opts)))
|
||||
|
||||
(when (:migrations file)
|
||||
(fmigr/upsert-migrations! conn file))
|
||||
|
||||
(let [file (encode-file cfg file)]
|
||||
(db/insert! conn :file
|
||||
(file->params file)
|
||||
{::db/return-keys false})
|
||||
nil))
|
||||
|
||||
(defn update-file!
|
||||
"Update an existing file on the database."
|
||||
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [id] :as file} & {:as opts}]
|
||||
(let [file (encode-file cfg file)
|
||||
params (-> (get-params-from-file file)
|
||||
(dissoc :id))]
|
||||
"Update an existing file on the database. Expects not encoded file."
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file} & {:as opts}]
|
||||
|
||||
;; If file was already offloaded, we touch the underlying storage
|
||||
;; object for properly trigger storage-gc-touched task
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(some->> (:data-ref-id file) (sto/touch-object! storage)))
|
||||
(if (::reset-migrations opts false)
|
||||
(fmigr/reset-migrations! conn file)
|
||||
(fmigr/upsert-migrations! conn file))
|
||||
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(db/update! conn :file params {:id id} opts)))
|
||||
(let [file
|
||||
(encode-file cfg file)
|
||||
|
||||
params
|
||||
(file->params (dissoc file :id))]
|
||||
|
||||
(db/update! conn :file params
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
|
||||
nil))
|
||||
|
||||
(defn save-file!
|
||||
"Applies all the final validations and perist the file, binfile
|
||||
specific, should not be used outside of binfile domain"
|
||||
[{:keys [::timestamp] :as cfg} file & {:as opts}]
|
||||
specific, should not be used outside of binfile domain.
|
||||
|
||||
(assert (dt/instant? timestamp) "expected valid timestamp")
|
||||
Returns nil"
|
||||
[{:keys [::timestamp] :as cfg} file & {:as opts}]
|
||||
(assert (ct/inst? timestamp) "expected valid timestamp")
|
||||
|
||||
(let [file (-> file
|
||||
(assoc :created-at timestamp)
|
||||
(assoc :modified-at timestamp)
|
||||
(assoc :ignore-sync-until (dt/plus timestamp (dt/duration {:seconds 5})))
|
||||
(cond-> (not (::overwrite cfg))
|
||||
(assoc :ignore-sync-until (ct/plus timestamp (ct/duration {:seconds 5}))))
|
||||
(update :features
|
||||
(fn [features]
|
||||
(-> (::features cfg #{})
|
||||
@@ -532,8 +563,9 @@
|
||||
(when (ex/exception? result)
|
||||
(l/error :hint "file schema validation error" :cause result))))
|
||||
|
||||
(insert-file! cfg file opts)))
|
||||
|
||||
(if (::overwrite cfg)
|
||||
(update-file! cfg file (assoc opts ::reset-migrations true))
|
||||
(insert-file! cfg file opts))))
|
||||
|
||||
(def ^:private sql:get-file-libraries
|
||||
"WITH RECURSIVE libs AS (
|
||||
@@ -558,7 +590,8 @@
|
||||
l.revn,
|
||||
l.vern,
|
||||
l.synced_at,
|
||||
l.is_shared
|
||||
l.is_shared,
|
||||
l.version
|
||||
FROM libs AS l
|
||||
INNER JOIN project AS p ON (p.id = l.project_id)
|
||||
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||
@@ -574,8 +607,18 @@
|
||||
(db/exec! conn [sql:get-file-libraries file-id])))
|
||||
|
||||
(defn get-resolved-file-libraries
|
||||
"A helper for preload file libraries"
|
||||
[{:keys [::db/conn] :as cfg} file]
|
||||
(->> (get-file-libraries conn (:id file))
|
||||
(into [file] (map #(get-file cfg (:id %))))
|
||||
(d/index-by :id)))
|
||||
"Get all file libraries including itself. Returns an instance of
|
||||
LoadableWeakValueMap that allows do not have strong references to
|
||||
the loaded libraries and reduce possible memory pressure on having
|
||||
all this libraries loaded at same time on processing file validation
|
||||
or file migration.
|
||||
|
||||
This still requires at least one library at time to be loaded while
|
||||
access to it is performed, but it improves considerable not having
|
||||
the need of loading all the libraries at the same time."
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(let [library-ids (->> (get-file-libraries conn (:id file))
|
||||
(map :id)
|
||||
(cons (:id file)))
|
||||
load-fn #(get-file cfg % :migrate? false)]
|
||||
(weak/loadable-weak-value-map library-ids load-fn {id file})))
|
||||
|
||||
@@ -36,11 +36,6 @@
|
||||
"fdata/shape-data-type"
|
||||
nil
|
||||
|
||||
;; There is no migration needed, but we don't want to allow
|
||||
;; copy paste nor import of variant files into no-variant teams
|
||||
"variants/v1"
|
||||
nil
|
||||
|
||||
(ex/raise :type :internal
|
||||
:code :no-migration-defined
|
||||
:hint (str/ffmt "no migation for feature '%' on file importation" feature)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -30,7 +31,6 @@
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.tasks.file-gc]
|
||||
[app.util.events :as events]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.java.io :as jio]
|
||||
[clojure.set :as set]
|
||||
@@ -434,7 +434,7 @@
|
||||
(defn read-import!
|
||||
"Do the importation of the specified resource in penpot custom binary
|
||||
format."
|
||||
[{:keys [::bfc/input ::bfc/timestamp] :or {timestamp (dt/now)} :as options}]
|
||||
[{:keys [::bfc/input ::bfc/timestamp] :or {timestamp (ct/now)} :as options}]
|
||||
|
||||
(dm/assert!
|
||||
"expected input stream"
|
||||
@@ -442,7 +442,7 @@
|
||||
|
||||
(dm/assert!
|
||||
"expected valid instant"
|
||||
(dt/instant? timestamp))
|
||||
(ct/inst? timestamp))
|
||||
|
||||
(let [version (read-header! input)]
|
||||
(read-import (assoc options ::version version ::bfc/timestamp timestamp))))
|
||||
@@ -682,7 +682,7 @@
|
||||
(io/coercible? output))
|
||||
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
tp (ct/tpoint)
|
||||
ab (volatile! false)
|
||||
cs (volatile! nil)]
|
||||
(try
|
||||
@@ -720,7 +720,7 @@
|
||||
(satisfies? jio/IOFactory input))
|
||||
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
tp (ct/tpoint)
|
||||
cs (volatile! nil)]
|
||||
|
||||
(l/info :hint "import: started" :id (str id))
|
||||
@@ -742,6 +742,6 @@
|
||||
(finally
|
||||
(l/info :hint "import: terminated"
|
||||
:id (str id)
|
||||
:elapsed (dt/format-duration (tp))
|
||||
:elapsed (ct/format-duration (tp))
|
||||
:error? (some? @cs))))))
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -23,7 +24,6 @@
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.events :as events]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
@@ -344,7 +344,7 @@
|
||||
(defn export-team!
|
||||
[cfg team-id]
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
tp (ct/tpoint)
|
||||
cfg (create-database cfg)]
|
||||
|
||||
(l/inf :hint "start"
|
||||
@@ -378,15 +378,15 @@
|
||||
(l/inf :hint "end"
|
||||
:operation "export"
|
||||
:id (str id)
|
||||
:elapsed (dt/format-duration elapsed)))))))
|
||||
:elapsed (ct/format-duration elapsed)))))))
|
||||
|
||||
(defn import-team!
|
||||
[cfg path]
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
tp (ct/tpoint)
|
||||
|
||||
cfg (-> (create-database cfg path)
|
||||
(assoc ::bfc/timestamp (dt/now)))]
|
||||
(assoc ::bfc/timestamp (ct/now)))]
|
||||
|
||||
(l/inf :hint "start"
|
||||
:operation "import"
|
||||
@@ -434,4 +434,4 @@
|
||||
(l/inf :hint "end"
|
||||
:operation "import"
|
||||
:id (str id)
|
||||
:elapsed (dt/format-duration elapsed)))))))
|
||||
:elapsed (ct/format-duration elapsed)))))))
|
||||
|
||||
@@ -20,13 +20,14 @@
|
||||
[app.common.media :as cmedia]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.thumbnails :as cth]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.color :as ctcl]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.plugins :as ctpg]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.tokens-lib :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.typography :as cty]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -35,14 +36,15 @@
|
||||
[app.storage :as sto]
|
||||
[app.storage.impl :as sto.impl]
|
||||
[app.util.events :as events]
|
||||
[app.util.time :as dt]
|
||||
[clojure.java.io :as jio]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io])
|
||||
(:import
|
||||
java.io.File
|
||||
java.io.InputStream
|
||||
java.io.OutputStreamWriter
|
||||
java.lang.AutoCloseable
|
||||
java.util.zip.ZipEntry
|
||||
java.util.zip.ZipFile
|
||||
java.util.zip.ZipOutputStream))
|
||||
@@ -92,7 +94,7 @@
|
||||
|
||||
(defn- default-now
|
||||
[o]
|
||||
(or o (dt/now)))
|
||||
(or o (ct/now)))
|
||||
|
||||
;; --- ENCODERS
|
||||
|
||||
@@ -103,25 +105,25 @@
|
||||
(sm/encoder ctp/schema:page sm/json-transformer))
|
||||
|
||||
(def encode-shape
|
||||
(sm/encoder ::cts/shape sm/json-transformer))
|
||||
(sm/encoder cts/schema:shape sm/json-transformer))
|
||||
|
||||
(def encode-media
|
||||
(sm/encoder ::ctf/media sm/json-transformer))
|
||||
(sm/encoder ctf/schema:media sm/json-transformer))
|
||||
|
||||
(def encode-component
|
||||
(sm/encoder ::ctc/component sm/json-transformer))
|
||||
(sm/encoder ctc/schema:component sm/json-transformer))
|
||||
|
||||
(def encode-color
|
||||
(sm/encoder ctcl/schema:library-color sm/json-transformer))
|
||||
|
||||
(def encode-typography
|
||||
(sm/encoder ::cty/typography sm/json-transformer))
|
||||
(sm/encoder cty/schema:typography sm/json-transformer))
|
||||
|
||||
(def encode-tokens-lib
|
||||
(sm/encoder ::cto/tokens-lib sm/json-transformer))
|
||||
(sm/encoder ctob/schema:tokens-lib sm/json-transformer))
|
||||
|
||||
(def encode-plugin-data
|
||||
(sm/encoder ::ctpg/plugin-data sm/json-transformer))
|
||||
(sm/encoder ctpg/schema:plugin-data sm/json-transformer))
|
||||
|
||||
(def encode-storage-object
|
||||
(sm/encoder schema:storage-object sm/json-transformer))
|
||||
@@ -138,7 +140,7 @@
|
||||
(sm/decoder ctf/schema:media sm/json-transformer))
|
||||
|
||||
(def decode-component
|
||||
(sm/decoder ::ctc/component sm/json-transformer))
|
||||
(sm/decoder ctc/schema:component sm/json-transformer))
|
||||
|
||||
(def decode-color
|
||||
(sm/decoder ctcl/schema:library-color sm/json-transformer))
|
||||
@@ -147,19 +149,19 @@
|
||||
(sm/decoder schema:file sm/json-transformer))
|
||||
|
||||
(def decode-page
|
||||
(sm/decoder ::ctp/page sm/json-transformer))
|
||||
(sm/decoder ctp/schema:page sm/json-transformer))
|
||||
|
||||
(def decode-shape
|
||||
(sm/decoder ::cts/shape sm/json-transformer))
|
||||
(sm/decoder cts/schema:shape sm/json-transformer))
|
||||
|
||||
(def decode-typography
|
||||
(sm/decoder ::cty/typography sm/json-transformer))
|
||||
(sm/decoder cty/schema:typography sm/json-transformer))
|
||||
|
||||
(def decode-tokens-lib
|
||||
(sm/decoder cto/schema:tokens-lib sm/json-transformer))
|
||||
(sm/decoder ctob/schema:tokens-lib sm/json-transformer))
|
||||
|
||||
(def decode-plugin-data
|
||||
(sm/decoder ::ctpg/plugin-data sm/json-transformer))
|
||||
(sm/decoder ctpg/schema:plugin-data sm/json-transformer))
|
||||
|
||||
(def decode-storage-object
|
||||
(sm/decoder schema:storage-object sm/json-transformer))
|
||||
@@ -173,31 +175,31 @@
|
||||
(sm/check-fn schema:manifest))
|
||||
|
||||
(def validate-file
|
||||
(sm/check-fn ::ctf/file))
|
||||
(sm/check-fn ctf/schema:file))
|
||||
|
||||
(def validate-page
|
||||
(sm/check-fn ::ctp/page))
|
||||
(sm/check-fn ctp/schema:page))
|
||||
|
||||
(def validate-shape
|
||||
(sm/check-fn ::cts/shape))
|
||||
(sm/check-fn cts/schema:shape))
|
||||
|
||||
(def validate-media
|
||||
(sm/check-fn ::ctf/media))
|
||||
(sm/check-fn ctf/schema:media))
|
||||
|
||||
(def validate-color
|
||||
(sm/check-fn ctcl/schema:library-color))
|
||||
|
||||
(def validate-component
|
||||
(sm/check-fn ::ctc/component))
|
||||
(sm/check-fn ctc/schema:component))
|
||||
|
||||
(def validate-typography
|
||||
(sm/check-fn ::cty/typography))
|
||||
(sm/check-fn cty/schema:typography))
|
||||
|
||||
(def validate-tokens-lib
|
||||
(sm/check-fn ::cto/tokens-lib))
|
||||
(sm/check-fn ctob/schema:tokens-lib))
|
||||
|
||||
(def validate-plugin-data
|
||||
(sm/check-fn ::ctpg/plugin-data))
|
||||
(sm/check-fn ctpg/schema:plugin-data))
|
||||
|
||||
(def validate-storage-object
|
||||
(sm/check-fn schema:storage-object))
|
||||
@@ -251,9 +253,9 @@
|
||||
(write-entry! output path params)
|
||||
|
||||
(with-open [input (sto/get-object-data storage sobject)]
|
||||
(.putNextEntry output (ZipEntry. (str "objects/" id ext)))
|
||||
(.putNextEntry ^ZipOutputStream output (ZipEntry. (str "objects/" id ext)))
|
||||
(io/copy input output :size (:size sobject))
|
||||
(.closeEntry output))))))
|
||||
(.closeEntry ^ZipOutputStream output))))))
|
||||
|
||||
(defn- export-file
|
||||
[{:keys [::file-id ::output] :as cfg}]
|
||||
@@ -284,10 +286,12 @@
|
||||
(assoc :options (:options data))
|
||||
|
||||
:always
|
||||
(dissoc :data)
|
||||
(dissoc :data))
|
||||
|
||||
file (cond-> file
|
||||
:always
|
||||
(encode-file))
|
||||
|
||||
path (str "files/" file-id ".json")]
|
||||
(write-entry! output path file))
|
||||
|
||||
@@ -345,7 +349,8 @@
|
||||
typography (encode-typography object)]
|
||||
(write-entry! output path typography)))
|
||||
|
||||
(when tokens-lib
|
||||
(when (and tokens-lib
|
||||
(not (ctob/empty-lib? tokens-lib)))
|
||||
(let [path (str "files/" file-id "/tokens.json")
|
||||
encoded-tokens (encode-tokens-lib tokens-lib)]
|
||||
(write-entry! output path encoded-tokens)))))
|
||||
@@ -445,7 +450,7 @@
|
||||
(defn- read-manifest
|
||||
[^ZipFile input]
|
||||
(let [entry (get-zip-entry input "manifest.json")]
|
||||
(with-open [reader (zip-entry-reader input entry)]
|
||||
(with-open [^AutoCloseable reader (zip-entry-reader input entry)]
|
||||
(let [manifest (json/read reader :key-fn json/read-kebab-key)]
|
||||
(decode-manifest manifest)))))
|
||||
|
||||
@@ -535,24 +540,27 @@
|
||||
|
||||
(defn- read-entry
|
||||
[^ZipFile input entry]
|
||||
(with-open [reader (zip-entry-reader input entry)]
|
||||
(with-open [^AutoCloseable reader (zip-entry-reader input entry)]
|
||||
(json/read reader :key-fn json/read-kebab-key)))
|
||||
|
||||
(defn- read-plain-entry
|
||||
[^ZipFile input entry]
|
||||
(with-open [reader (zip-entry-reader input entry)]
|
||||
(with-open [^AutoCloseable reader (zip-entry-reader input entry)]
|
||||
(json/read reader)))
|
||||
|
||||
(defn- read-file
|
||||
[{:keys [::bfc/input ::file-id]}]
|
||||
[{:keys [::bfc/input ::bfc/timestamp]} file-id]
|
||||
(let [path (str "files/" file-id ".json")
|
||||
entry (get-zip-entry input path)]
|
||||
(-> (read-entry input entry)
|
||||
(decode-file)
|
||||
(update :revn d/nilv 1)
|
||||
(update :created-at d/nilv timestamp)
|
||||
(update :modified-at d/nilv timestamp)
|
||||
(validate-file))))
|
||||
|
||||
(defn- read-file-plugin-data
|
||||
[{:keys [::bfc/input ::file-id]}]
|
||||
[{:keys [::bfc/input]} file-id]
|
||||
(let [path (str "files/" file-id "/plugin-data.json")
|
||||
entry (get-zip-entry* input path)]
|
||||
(some->> entry
|
||||
@@ -561,7 +569,7 @@
|
||||
(validate-plugin-data))))
|
||||
|
||||
(defn- read-file-media
|
||||
[{:keys [::bfc/input ::file-id ::entries]}]
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
(->> (keep (match-media-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
@@ -581,7 +589,7 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-colors
|
||||
[{:keys [::bfc/input ::file-id ::entries]}]
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
(->> (keep (match-color-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
@@ -594,7 +602,7 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-components
|
||||
[{:keys [::bfc/input ::file-id ::entries]}]
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
(let [clean-component-post-decode
|
||||
(fn [component]
|
||||
(d/update-when component :objects
|
||||
@@ -625,7 +633,7 @@
|
||||
(not-empty))))
|
||||
|
||||
(defn- read-file-typographies
|
||||
[{:keys [::bfc/input ::file-id ::entries]}]
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
(->> (keep (match-typography-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
@@ -638,14 +646,14 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-tokens-lib
|
||||
[{:keys [::bfc/input ::file-id ::entries]}]
|
||||
[{:keys [::bfc/input ::entries]} file-id]
|
||||
(when-let [entry (d/seek (match-tokens-lib-entry-fn file-id) entries)]
|
||||
(->> (read-plain-entry input entry)
|
||||
(decode-tokens-lib)
|
||||
(validate-tokens-lib))))
|
||||
|
||||
(defn- read-file-shapes
|
||||
[{:keys [::bfc/input ::file-id ::page-id ::entries] :as cfg}]
|
||||
[{:keys [::bfc/input ::entries] :as cfg} file-id page-id]
|
||||
(->> (keep (match-shape-entry-fn file-id page-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
@@ -659,15 +667,14 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-pages
|
||||
[{:keys [::bfc/input ::file-id ::entries] :as cfg}]
|
||||
[{:keys [::bfc/input ::entries] :as cfg} file-id]
|
||||
(->> (keep (match-page-entry-fn file-id) entries)
|
||||
(keep (fn [{:keys [id entry]}]
|
||||
(let [page (->> (read-entry input entry)
|
||||
(decode-page))
|
||||
page (dissoc page :options)]
|
||||
(when (= id (:id page))
|
||||
(let [objects (-> (assoc cfg ::page-id id)
|
||||
(read-file-shapes))]
|
||||
(let [objects (read-file-shapes cfg file-id id)]
|
||||
(assoc page :objects objects))))))
|
||||
(sort-by :index)
|
||||
(reduce (fn [result {:keys [id] :as page}]
|
||||
@@ -675,7 +682,7 @@
|
||||
(d/ordered-map))))
|
||||
|
||||
(defn- read-file-thumbnails
|
||||
[{:keys [::bfc/input ::file-id ::entries] :as cfg}]
|
||||
[{:keys [::bfc/input ::entries] :as cfg} file-id]
|
||||
(->> (keep (match-thumbnail-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [page-id frame-id tag entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
@@ -690,13 +697,13 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-data
|
||||
[cfg]
|
||||
(let [colors (read-file-colors cfg)
|
||||
typographies (read-file-typographies cfg)
|
||||
tokens-lib (read-file-tokens-lib cfg)
|
||||
components (read-file-components cfg)
|
||||
plugin-data (read-file-plugin-data cfg)
|
||||
pages (read-file-pages cfg)]
|
||||
[cfg file-id]
|
||||
(let [colors (read-file-colors cfg file-id)
|
||||
typographies (read-file-typographies cfg file-id)
|
||||
tokens-lib (read-file-tokens-lib cfg file-id)
|
||||
components (read-file-components cfg file-id)
|
||||
plugin-data (read-file-plugin-data cfg file-id)
|
||||
pages (read-file-pages cfg file-id)]
|
||||
{:pages (-> pages keys vec)
|
||||
:pages-index (into {} pages)
|
||||
:colors colors
|
||||
@@ -706,11 +713,11 @@
|
||||
:plugin-data plugin-data}))
|
||||
|
||||
(defn- import-file
|
||||
[{:keys [::bfc/project-id ::file-id ::file-name] :as cfg}]
|
||||
[{:keys [::bfc/project-id] :as cfg} {file-id :id file-name :name}]
|
||||
(let [file-id' (bfc/lookup-index file-id)
|
||||
file (read-file cfg)
|
||||
media (read-file-media cfg)
|
||||
thumbnails (read-file-thumbnails cfg)]
|
||||
file (read-file cfg file-id)
|
||||
media (read-file-media cfg file-id)
|
||||
thumbnails (read-file-thumbnails cfg file-id)]
|
||||
|
||||
(l/dbg :hint "processing file"
|
||||
:id (str file-id')
|
||||
@@ -740,7 +747,7 @@
|
||||
(vswap! bfc/*state* update :index bfc/update-index (map :media-id thumbnails))
|
||||
(vswap! bfc/*state* update :thumbnails into thumbnails))
|
||||
|
||||
(let [data (-> (read-file-data cfg)
|
||||
(let [data (-> (read-file-data cfg file-id)
|
||||
(d/without-nils)
|
||||
(assoc :id file-id')
|
||||
(cond-> (:options file)
|
||||
@@ -757,7 +764,7 @@
|
||||
file (ctf/check-file file)]
|
||||
|
||||
(bfm/register-pending-migrations! cfg file)
|
||||
(bfc/save-file! cfg file ::db/return-keys false)
|
||||
(bfc/save-file! cfg file)
|
||||
|
||||
file-id')))
|
||||
|
||||
@@ -853,7 +860,8 @@
|
||||
:file-id (str (:file-id params))
|
||||
::l/sync? true)
|
||||
|
||||
(db/insert! conn :file-media-object params))))
|
||||
(db/insert! conn :file-media-object params
|
||||
::db/on-conflict-do-nothing? (::bfc/overwrite cfg)))))
|
||||
|
||||
(defn- import-file-thumbnails
|
||||
[{:keys [::db/conn] :as cfg}]
|
||||
@@ -873,17 +881,77 @@
|
||||
:media-id (str media-id)
|
||||
::l/sync? true)
|
||||
|
||||
(db/insert! conn :file-tagged-object-thumbnail params))))
|
||||
(db/insert! conn :file-tagged-object-thumbnail params
|
||||
{::db/on-conflict-do-nothing? true}))))
|
||||
|
||||
(defn- import-files*
|
||||
[{:keys [::manifest] :as cfg}]
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
|
||||
(vswap! bfc/*state* update :index bfc/update-index (:files manifest) :id)
|
||||
|
||||
(let [files (get manifest :files)
|
||||
result (reduce (fn [result {:keys [id] :as file}]
|
||||
(let [name' (get file :name)
|
||||
name' (if (map? name)
|
||||
(get name id)
|
||||
name')
|
||||
file (assoc file :name name')]
|
||||
(conj result (import-file cfg file))))
|
||||
[]
|
||||
files)]
|
||||
|
||||
(import-file-relations cfg)
|
||||
(import-storage-objects cfg)
|
||||
(import-file-media cfg)
|
||||
(import-file-thumbnails cfg)
|
||||
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
|
||||
result))
|
||||
|
||||
(defn- import-file-and-overwrite*
|
||||
[{:keys [::manifest ::bfc/file-id] :as cfg}]
|
||||
|
||||
(when (not= 1 (count (:files manifest)))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-condition
|
||||
:hint "unable to perform in-place update with binfile containing more than 1 file"
|
||||
:manifest manifest))
|
||||
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
|
||||
(let [ref-file (bfc/get-minimal-file cfg file-id ::db/for-update true)
|
||||
file (first (get manifest :files))
|
||||
cfg (assoc cfg ::bfc/overwrite true)]
|
||||
|
||||
(vswap! bfc/*state* update :index assoc (:id file) file-id)
|
||||
|
||||
(binding [bfc/*options* cfg
|
||||
bfc/*reference-file* ref-file]
|
||||
|
||||
(import-file cfg file)
|
||||
(import-storage-objects cfg)
|
||||
(import-file-media cfg)
|
||||
|
||||
(bfc/invalidate-thumbnails cfg file-id)
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
|
||||
[file-id])))
|
||||
|
||||
(defn- import-files
|
||||
[{:keys [::bfc/timestamp ::bfc/input ::bfc/name] :or {timestamp (dt/now)} :as cfg}]
|
||||
[{:keys [::bfc/timestamp ::bfc/input] :or {timestamp (ct/now)} :as cfg}]
|
||||
|
||||
(assert (instance? ZipFile input) "expected zip file")
|
||||
(assert (dt/instant? timestamp) "expected valid instant")
|
||||
(assert (ct/inst? timestamp) "expected valid instant")
|
||||
|
||||
(let [manifest (-> (read-manifest input)
|
||||
(validate-manifest))
|
||||
entries (read-zip-entries input)]
|
||||
entries (read-zip-entries input)
|
||||
cfg (-> cfg
|
||||
(assoc ::entries entries)
|
||||
(assoc ::manifest manifest)
|
||||
(assoc ::bfc/timestamp timestamp))]
|
||||
|
||||
(when-not (= "penpot/export-files" (:type manifest))
|
||||
(ex/raise :type :validation
|
||||
@@ -891,7 +959,6 @@
|
||||
:hint "unexpected type on manifest"
|
||||
:manifest manifest))
|
||||
|
||||
|
||||
;; Check if all files referenced on manifest are present
|
||||
(doseq [{file-id :id features :features} (:files manifest)]
|
||||
(let [path (str "files/" file-id ".json")]
|
||||
@@ -907,35 +974,10 @@
|
||||
|
||||
(events/tap :progress {:section :manifest})
|
||||
|
||||
(let [index (bfc/update-index (map :id (:files manifest)))
|
||||
state {:media [] :index index}
|
||||
cfg (-> cfg
|
||||
(assoc ::entries entries)
|
||||
(assoc ::manifest manifest)
|
||||
(assoc ::bfc/timestamp timestamp))]
|
||||
|
||||
(binding [bfc/*state* (volatile! state)]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
(let [ids (->> (:files manifest)
|
||||
(reduce (fn [result {:keys [id] :as file}]
|
||||
(let [name' (get file :name)
|
||||
name' (if (map? name)
|
||||
(get name id)
|
||||
name')]
|
||||
(conj result (-> cfg
|
||||
(assoc ::file-id id)
|
||||
(assoc ::file-name name')
|
||||
(import-file)))))
|
||||
[]))]
|
||||
(import-file-relations cfg)
|
||||
(import-storage-objects cfg)
|
||||
(import-file-media cfg)
|
||||
(import-file-thumbnails cfg)
|
||||
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
|
||||
ids)))))))
|
||||
(binding [bfc/*state* (volatile! {:media [] :index {}})]
|
||||
(if (::bfc/file-id cfg)
|
||||
(db/tx-run! cfg import-file-and-overwrite*)
|
||||
(db/tx-run! cfg import-files*)))))
|
||||
|
||||
;; --- PUBLIC API
|
||||
|
||||
@@ -961,14 +1003,14 @@
|
||||
"expected instance of jio/IOFactory for `input`")
|
||||
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
tp (ct/tpoint)
|
||||
ab (volatile! false)
|
||||
cs (volatile! nil)]
|
||||
(try
|
||||
(l/info :hint "start exportation" :export-id (str id))
|
||||
(binding [bfc/*state* (volatile! (bfc/initial-state))]
|
||||
(with-open [output (io/output-stream output)]
|
||||
(with-open [output (ZipOutputStream. output)]
|
||||
(with-open [^AutoCloseable output (io/output-stream output)]
|
||||
(with-open [^AutoCloseable output (ZipOutputStream. output)]
|
||||
(let [cfg (assoc cfg ::output output)]
|
||||
(export-files cfg)
|
||||
(export-storage-objects cfg)))))
|
||||
@@ -1007,12 +1049,12 @@
|
||||
"expected instance of jio/IOFactory for `input`")
|
||||
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
tp (ct/tpoint)
|
||||
cs (volatile! nil)]
|
||||
|
||||
(l/info :hint "import: started" :id (str id))
|
||||
(try
|
||||
(with-open [input (ZipFile. (fs/file input))]
|
||||
(with-open [input (ZipFile. ^File (fs/file input))]
|
||||
(import-files (assoc cfg ::bfc/input input)))
|
||||
|
||||
(catch Throwable cause
|
||||
@@ -1022,11 +1064,11 @@
|
||||
(finally
|
||||
(l/info :hint "import: terminated"
|
||||
:id (str id)
|
||||
:elapsed (dt/format-duration (tp))
|
||||
:elapsed (ct/format-duration (tp))
|
||||
:error? (some? @cs))))))
|
||||
|
||||
(defn get-manifest
|
||||
[path]
|
||||
(with-open [input (ZipFile. (fs/file path))]
|
||||
(with-open [^AutoCloseable input (ZipFile. ^File (fs/file path))]
|
||||
(-> (read-manifest input)
|
||||
(validate-manifest))))
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.flags :as flags]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.common.version :as v]
|
||||
[app.util.overrides]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core :as c]
|
||||
[clojure.java.io :as io]
|
||||
[cuerdas.core :as str]
|
||||
@@ -59,10 +59,10 @@
|
||||
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
||||
:smtp-default-from "Penpot <no-reply@example.com>"
|
||||
|
||||
:profile-complaint-max-age (dt/duration {:days 7})
|
||||
:profile-complaint-max-age (ct/duration {:days 7})
|
||||
:profile-complaint-threshold 2
|
||||
|
||||
:profile-bounce-max-age (dt/duration {:days 7})
|
||||
:profile-bounce-max-age (ct/duration {:days 7})
|
||||
:profile-bounce-threshold 10
|
||||
|
||||
:telemetry-uri "https://telemetry.penpot.app/"
|
||||
@@ -96,16 +96,16 @@
|
||||
[:http-server-max-body-size {:optional true} ::sm/int]
|
||||
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
|
||||
[:http-server-io-threads {:optional true} ::sm/int]
|
||||
[:http-server-worker-threads {:optional true} ::sm/int]
|
||||
[:http-server-max-worker-threads {:optional true} ::sm/int]
|
||||
|
||||
[:telemetry-uri {:optional true} :string]
|
||||
[:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
|
||||
|
||||
[:auto-file-snapshot-every {:optional true} ::sm/int]
|
||||
[:auto-file-snapshot-timeout {:optional true} ::dt/duration]
|
||||
[:auto-file-snapshot-timeout {:optional true} ::ct/duration]
|
||||
|
||||
[:media-max-file-size {:optional true} ::sm/int]
|
||||
[:deletion-delay {:optional true} ::dt/duration] ;; REVIEW
|
||||
[:deletion-delay {:optional true} ::ct/duration] ;; REVIEW
|
||||
[:telemetry-enabled {:optional true} ::sm/boolean]
|
||||
[:default-blob-version {:optional true} ::sm/int]
|
||||
[:allow-demo-users {:optional true} ::sm/boolean]
|
||||
@@ -148,10 +148,10 @@
|
||||
|
||||
[:auth-data-cookie-domain {:optional true} :string]
|
||||
[:auth-token-cookie-name {:optional true} :string]
|
||||
[:auth-token-cookie-max-age {:optional true} ::dt/duration]
|
||||
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
|
||||
|
||||
[:registration-domain-whitelist {:optional true} [::sm/set :string]]
|
||||
[:email-verify-threshold {:optional true} ::dt/duration]
|
||||
[:email-verify-threshold {:optional true} ::ct/duration]
|
||||
|
||||
[:github-client-id {:optional true} :string]
|
||||
[:github-client-secret {:optional true} :string]
|
||||
@@ -186,9 +186,9 @@
|
||||
[:ldap-starttls {:optional true} ::sm/boolean]
|
||||
[:ldap-user-query {:optional true} :string]
|
||||
|
||||
[:profile-bounce-max-age {:optional true} ::dt/duration]
|
||||
[:profile-bounce-max-age {:optional true} ::ct/duration]
|
||||
[:profile-bounce-threshold {:optional true} ::sm/int]
|
||||
[:profile-complaint-max-age {:optional true} ::dt/duration]
|
||||
[:profile-complaint-max-age {:optional true} ::ct/duration]
|
||||
[:profile-complaint-threshold {:optional true} ::sm/int]
|
||||
|
||||
[:redis-uri {:optional true} ::sm/uri]
|
||||
@@ -214,20 +214,21 @@
|
||||
[:media-uri {:optional true} :string]
|
||||
[:assets-path {:optional true} :string]
|
||||
|
||||
;; Legacy, will be removed in 2.5
|
||||
[:netty-io-threads {:optional true} ::sm/int]
|
||||
[:executor-threads {:optional true} ::sm/int]
|
||||
|
||||
;; DEPRECATED
|
||||
[:assets-storage-backend {:optional true} :keyword]
|
||||
[:storage-assets-fs-directory {:optional true} :string]
|
||||
[:storage-assets-s3-bucket {:optional true} :string]
|
||||
[:storage-assets-s3-region {:optional true} :keyword]
|
||||
[:storage-assets-s3-endpoint {:optional true} ::sm/uri]
|
||||
[:storage-assets-s3-io-threads {:optional true} ::sm/int]
|
||||
|
||||
[:objects-storage-backend {:optional true} :keyword]
|
||||
[:objects-storage-fs-directory {:optional true} :string]
|
||||
[:objects-storage-s3-bucket {:optional true} :string]
|
||||
[:objects-storage-s3-region {:optional true} :keyword]
|
||||
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]
|
||||
[:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
|
||||
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]]))
|
||||
|
||||
(defn- parse-flags
|
||||
[config]
|
||||
@@ -298,7 +299,7 @@
|
||||
(defn get-deletion-delay
|
||||
[]
|
||||
(or (c/get config :deletion-delay)
|
||||
(dt/duration {:days 7})))
|
||||
(ct/duration {:days 7})))
|
||||
|
||||
(defn get
|
||||
"A configuration getter. Helps code be more testable."
|
||||
|
||||
@@ -10,19 +10,20 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db.sql :as sql]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.set :as set]
|
||||
[integrant.core :as ig]
|
||||
[next.jdbc :as jdbc]
|
||||
[next.jdbc.date-time :as jdbc-dt]
|
||||
[next.jdbc.prepare :as jdbc.prepare]
|
||||
[next.jdbc.transaction])
|
||||
(:import
|
||||
com.zaxxer.hikari.HikariConfig
|
||||
@@ -33,6 +34,7 @@
|
||||
java.io.InputStream
|
||||
java.io.OutputStream
|
||||
java.sql.Connection
|
||||
java.sql.PreparedStatement
|
||||
java.sql.Savepoint
|
||||
org.postgresql.PGConnection
|
||||
org.postgresql.geometric.PGpoint
|
||||
@@ -377,9 +379,9 @@
|
||||
|
||||
(defn is-row-deleted?
|
||||
[{:keys [deleted-at]}]
|
||||
(and (dt/instant? deleted-at)
|
||||
(and (ct/inst? deleted-at)
|
||||
(< (inst-ms deleted-at)
|
||||
(inst-ms (dt/now)))))
|
||||
(inst-ms (ct/now)))))
|
||||
|
||||
(defn get*
|
||||
"Retrieve a single row from database that matches a simple filters. Do
|
||||
@@ -404,6 +406,24 @@
|
||||
:hint "database object not found"))
|
||||
row))
|
||||
|
||||
|
||||
(defn get-with-sql
|
||||
[ds sql & {:as opts}]
|
||||
(let [rows (cond->> (exec! ds sql opts)
|
||||
(::remove-deleted opts true)
|
||||
(remove is-row-deleted?)
|
||||
|
||||
:always
|
||||
(not-empty))]
|
||||
|
||||
(when (and (not rows) (::throw-if-not-exists opts true))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "database object not found"))
|
||||
|
||||
(first rows)))
|
||||
|
||||
|
||||
(def ^:private default-plan-opts
|
||||
(-> default-opts
|
||||
(assoc :fetch-size 1000)
|
||||
@@ -585,7 +605,7 @@
|
||||
(string? o)
|
||||
(pginterval o)
|
||||
|
||||
(dt/duration? o)
|
||||
(ct/duration? o)
|
||||
(interval (inst-ms o))
|
||||
|
||||
:else
|
||||
@@ -599,7 +619,7 @@
|
||||
val (.getValue o)]
|
||||
(if (or (= typ "json")
|
||||
(= typ "jsonb"))
|
||||
(json/decode val)
|
||||
(json/decode val :key-fn keyword)
|
||||
val))))
|
||||
|
||||
(defn decode-transit-pgobject
|
||||
@@ -640,7 +660,7 @@
|
||||
(when data
|
||||
(doto (org.postgresql.util.PGobject.)
|
||||
(.setType "jsonb")
|
||||
(.setValue (json/encode-str data)))))
|
||||
(.setValue (json/encode data)))))
|
||||
|
||||
;; --- Locks
|
||||
|
||||
@@ -686,3 +706,8 @@
|
||||
[cause]
|
||||
(and (sql-exception? cause)
|
||||
(= "40001" (.getSQLState ^java.sql.SQLException cause))))
|
||||
|
||||
(extend-protocol jdbc.prepare/SettableParameter
|
||||
clojure.lang.Keyword
|
||||
(set-parameter [^clojure.lang.Keyword v ^PreparedStatement s ^long i]
|
||||
(.setObject s i ^String (d/name v))))
|
||||
|
||||
@@ -53,8 +53,15 @@
|
||||
opts (cond-> opts
|
||||
(::order-by opts) (assoc :order-by (::order-by opts))
|
||||
(::columns opts) (assoc :columns (::columns opts))
|
||||
(::for-update opts) (assoc :suffix "FOR UPDATE")
|
||||
(::for-share opts) (assoc :suffix "FOR SHARE"))]
|
||||
|
||||
(or (::db/for-update opts)
|
||||
(::for-update opts))
|
||||
(assoc :suffix "FOR UPDATE")
|
||||
|
||||
(or (::db/for-share opts)
|
||||
(::for-share opts))
|
||||
(assoc :suffix "FOR SHARE"))]
|
||||
|
||||
(sql/for-query table where-params opts))))
|
||||
|
||||
(defn update
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.logging :as l]
|
||||
[app.common.types.objects-map :as omap]
|
||||
[app.common.types.path :as path]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.objects-map :as omap]
|
||||
[app.util.objects-map :as omap.legacy]
|
||||
[app.util.pointer-map :as pmap]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -36,10 +37,7 @@
|
||||
[file & _opts]
|
||||
(let [update-page
|
||||
(fn [page]
|
||||
(if (and (pmap/pointer-map? page)
|
||||
(not (pmap/loaded? page)))
|
||||
page
|
||||
(update page :objects omap/wrap)))
|
||||
(update page :objects omap/wrap))
|
||||
|
||||
update-data
|
||||
(fn [fdata]
|
||||
@@ -49,6 +47,20 @@
|
||||
(update :data update-data)
|
||||
(update :features conj "fdata/objects-map"))))
|
||||
|
||||
(defn disable-objects-map
|
||||
[file & _opts]
|
||||
(let [update-page
|
||||
(fn [page]
|
||||
(update page :objects #(into {} %)))
|
||||
|
||||
update-data
|
||||
(fn [fdata]
|
||||
(update fdata :pages-index d/update-vals update-page))]
|
||||
|
||||
(-> file
|
||||
(update :data update-data)
|
||||
(update :features disj "fdata/objects-map"))))
|
||||
|
||||
(defn process-objects
|
||||
"Apply a function to all objects-map on the file. Usualy used for convert
|
||||
the objects-map instances to plain maps"
|
||||
@@ -58,7 +70,8 @@
|
||||
(fn [page]
|
||||
(update page :objects
|
||||
(fn [objects]
|
||||
(if (omap/objects-map? objects)
|
||||
(if (or (omap/objects-map? objects)
|
||||
(omap.legacy/objects-map? objects))
|
||||
(update-fn objects)
|
||||
objects)))))
|
||||
fdata))
|
||||
@@ -81,6 +94,12 @@
|
||||
(let [data (get-file-data system file)]
|
||||
(assoc file :data data)))
|
||||
|
||||
(defn decode-file-data
|
||||
[_system {:keys [data] :as file}]
|
||||
(cond-> file
|
||||
(bytes? data)
|
||||
(assoc :data (blob/decode data))))
|
||||
|
||||
(defn load-pointer
|
||||
"A database loader pointer helper"
|
||||
[system file-id id]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"Backend specific code for file migrations. Implemented as permanent feature of files."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.migrations :as fmg :refer [xf:map-name]]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]))
|
||||
@@ -26,14 +27,27 @@
|
||||
(defn upsert-migrations!
|
||||
"Persist or update file migrations. Return the updated/inserted number
|
||||
of rows"
|
||||
[conn {:keys [id] :as file}]
|
||||
(let [migrations (or (-> file meta ::fmg/migrated)
|
||||
(-> file :migrations not-empty)
|
||||
fmg/available-migrations)
|
||||
[cfg {:keys [id] :as file}]
|
||||
(let [conn (db/get-connection cfg)
|
||||
migrations (or (-> file meta ::fmg/migrated)
|
||||
(-> file :migrations))
|
||||
columns [:file-id :name]
|
||||
rows (mapv (fn [name] [id name]) migrations)]
|
||||
rows (->> migrations
|
||||
(mapv (fn [name] [id name]))
|
||||
(not-empty))]
|
||||
|
||||
(when-not rows
|
||||
(ex/raise :type :internal
|
||||
:code :missing-migrations
|
||||
:hint "no migrations available on file"))
|
||||
|
||||
(-> (db/insert-many! conn :file-migration columns rows
|
||||
{::db/return-keys false
|
||||
::sql/on-conflict-do-nothing true})
|
||||
(db/get-update-count))))
|
||||
|
||||
(defn reset-migrations!
|
||||
"Replace file migrations"
|
||||
[cfg {:keys [id] :as file}]
|
||||
(db/delete! cfg :file-migration {:file-id id})
|
||||
(upsert-migrations! cfg file))
|
||||
|
||||
@@ -7,22 +7,23 @@
|
||||
(ns app.features.logical-deletion
|
||||
"A code related to handle logical deletion mechanism"
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.util.time :as dt]))
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]))
|
||||
|
||||
(def ^:private canceled-status
|
||||
#{"canceled" "unpaid"})
|
||||
|
||||
(defn get-deletion-delay
|
||||
"Calculate the next deleted-at for a resource (file, team, etc) in function
|
||||
of team settings"
|
||||
[team]
|
||||
(if-let [subscription (get team :subscription)]
|
||||
(if-let [{:keys [type status]} (get team :subscription)]
|
||||
(cond
|
||||
(and (= (:type subscription) "unlimited")
|
||||
(= (:status subscription) "active"))
|
||||
(dt/duration {:days 30})
|
||||
(and (= "unlimited" type) (not (contains? canceled-status status)))
|
||||
(ct/duration {:days 30})
|
||||
|
||||
(and (= (:type subscription) "enterprise")
|
||||
(= (:status subscription) "active"))
|
||||
(dt/duration {:days 90})
|
||||
(and (= "enterprise" type) (not (contains? canceled-status status)))
|
||||
(ct/duration {:days 90})
|
||||
|
||||
:else
|
||||
(cf/get-deletion-delay))
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.http.awsns :as-alias awsns]
|
||||
[app.http.debug :as-alias debug]
|
||||
[app.http.errors :as errors]
|
||||
[app.http.management :as mgmt]
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.session :as session]
|
||||
[app.http.websocket :as-alias ws]
|
||||
@@ -26,7 +27,6 @@
|
||||
[app.rpc.doc :as-alias rpc.doc]
|
||||
[app.setup :as-alias setup]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
[reitit.core :as r]
|
||||
[reitit.middleware :as rr]
|
||||
[yetti.adapter :as yt]
|
||||
@@ -53,6 +53,8 @@
|
||||
[:map
|
||||
[::port ::sm/int]
|
||||
[::host ::sm/text]
|
||||
[::io-threads {:optional true} ::sm/int]
|
||||
[::max-worker-threads {:optional true} ::sm/int]
|
||||
[::max-body-size {:optional true} ::sm/int]
|
||||
[::max-multipart-body-size {:optional true} ::sm/int]
|
||||
[::router {:optional true} [:fn r/router?]]
|
||||
@@ -63,30 +65,41 @@
|
||||
(assert (sm/check schema:server-params params)))
|
||||
|
||||
(defmethod ig/init-key ::server
|
||||
[_ {:keys [::handler ::router ::host ::port] :as cfg}]
|
||||
[_ {:keys [::handler ::router ::host ::port ::mtx/metrics] :as cfg}]
|
||||
(l/info :hint "starting http server" :port port :host host)
|
||||
(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 (or (::io-threads cfg)
|
||||
(max 3 (px/get-available-processors)))
|
||||
:xnio/dispatch :virtual
|
||||
:ring/compat :ring2
|
||||
:socket/backlog 4069}
|
||||
(let [on-dispatch
|
||||
(fn [_ start-at-ns]
|
||||
(let [timing (- (System/nanoTime) start-at-ns)
|
||||
timing (int (/ timing 1000000))]
|
||||
(mtx/run! metrics
|
||||
:id :http-server-dispatch-timing
|
||||
:val timing)))
|
||||
|
||||
handler (cond
|
||||
(some? router)
|
||||
(router-handler router)
|
||||
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/direct-buffers false
|
||||
:xnio/io-threads (::io-threads cfg)
|
||||
:xnio/max-worker-threads (::max-worker-threads cfg)
|
||||
:ring/compat :ring2
|
||||
:events/on-dispatch on-dispatch
|
||||
:socket/backlog 4069}
|
||||
|
||||
(some? handler)
|
||||
handler
|
||||
handler
|
||||
(cond
|
||||
(some? router)
|
||||
(router-handler router)
|
||||
|
||||
:else
|
||||
(throw (UnsupportedOperationException. "handler or router are required")))
|
||||
(some? handler)
|
||||
handler
|
||||
|
||||
options (d/without-nils options)
|
||||
server (yt/server handler options)]
|
||||
:else
|
||||
(throw (UnsupportedOperationException. "handler or router are required")))
|
||||
|
||||
server
|
||||
(yt/server handler (d/without-nils options))]
|
||||
|
||||
(assoc cfg ::server (yt/start! server))))
|
||||
|
||||
@@ -141,6 +154,7 @@
|
||||
[::debug/routes schema:routes]
|
||||
[::mtx/routes schema:routes]
|
||||
[::awsns/routes schema:routes]
|
||||
[::mgmt/routes schema:routes]
|
||||
::session/manager
|
||||
::setup/props
|
||||
::db/pool])
|
||||
@@ -168,6 +182,9 @@
|
||||
["/webhooks"
|
||||
(::awsns/routes cfg)]
|
||||
|
||||
["/management"
|
||||
(::mgmt/routes cfg)]
|
||||
|
||||
(::ws/routes cfg)
|
||||
|
||||
["/api" {:middleware [[mw/cors]]}
|
||||
|
||||
@@ -9,18 +9,18 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.util.time :as dt]
|
||||
[integrant.core :as ig]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
(def ^:private cache-max-age
|
||||
(dt/duration {:hours 24}))
|
||||
(ct/duration {:hours 24}))
|
||||
|
||||
(def ^:private signature-max-age
|
||||
(dt/duration {:hours 24 :minutes 15}))
|
||||
(ct/duration {:hours 24 :minutes 15}))
|
||||
|
||||
(defn get-id
|
||||
[{:keys [path-params]}]
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
[app.main :as-alias main]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.data.json :as j]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
@@ -40,8 +38,8 @@
|
||||
[_ cfg]
|
||||
(letfn [(handler [request]
|
||||
(let [data (-> request yreq/body slurp)]
|
||||
(px/run! :vthread (partial handle-request cfg data)))
|
||||
{::yres/status 200})]
|
||||
(handle-request cfg data)
|
||||
{::yres/status 200}))]
|
||||
["/sns" {:handler handler
|
||||
:allowed-methods #{:post}}]))
|
||||
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.features.file-migrations :as feat.fmig]
|
||||
[app.http.session :as session]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.files-create :refer [create-file]]
|
||||
@@ -29,7 +32,6 @@
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.template :as tmpl]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]
|
||||
[emoji.core :as emj]
|
||||
@@ -50,26 +52,26 @@
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/html"}
|
||||
::yres/body (-> (io/resource "app/templates/debug.tmpl")
|
||||
(tmpl/render {:version (:full cf/version)}))})
|
||||
(tmpl/render {:version (:full cf/version)
|
||||
:supported-features cfeat/supported-features}))})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FILE CHANGES
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn prepare-response
|
||||
[body]
|
||||
(let [headers {"content-type" "application/transit+json"}]
|
||||
{::yres/status 200
|
||||
::yres/body body
|
||||
::yres/headers headers}))
|
||||
(defn- get-resolved-file
|
||||
[cfg file-id]
|
||||
(some-> (bfc/get-file cfg file-id :migrate? false)
|
||||
(update :data blob/encode)))
|
||||
|
||||
(defn prepare-download-response
|
||||
[body filename]
|
||||
(let [headers {"content-disposition" (str "attachment; filename=" filename)
|
||||
"content-type" "application/octet-stream"}]
|
||||
{::yres/status 200
|
||||
::yres/body body
|
||||
::yres/headers headers}))
|
||||
(defn prepare-download
|
||||
[file filename]
|
||||
{::yres/status 200
|
||||
::yres/headers
|
||||
{"content-disposition" (str "attachment; filename=" filename ".json")
|
||||
"content-type" "application/octet-stream"}
|
||||
::yres/body
|
||||
(t/encode file {:type :json-verbose})})
|
||||
|
||||
(def sql:retrieve-range-of-changes
|
||||
"select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn")
|
||||
@@ -77,45 +79,51 @@
|
||||
(def sql:retrieve-single-change
|
||||
"select revn, changes, data from file_change where file_id=? and revn = ?")
|
||||
|
||||
(defn- retrieve-file-data
|
||||
[{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}]
|
||||
(defn- download-file-data
|
||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||
(let [file-id (some-> params :file-id parse-uuid)
|
||||
revn (some-> params :revn parse-long)
|
||||
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))]
|
||||
|
||||
(when-not data
|
||||
(ex/raise :type :not-found
|
||||
:code :enpty-data
|
||||
:hint "empty response"))
|
||||
(if-let [file (get-resolved-file cfg file-id)]
|
||||
(cond
|
||||
(contains? params :download)
|
||||
(prepare-download-response data filename)
|
||||
(prepare-download file filename)
|
||||
|
||||
(contains? params :clone)
|
||||
(let [profile (profile/get-profile pool profile-id)
|
||||
project-id (:default-project-id profile)]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [profile (profile/get-profile conn profile-id)
|
||||
project-id (:default-project-id profile)
|
||||
file (-> (create-file cfg {:id (uuid/next)
|
||||
:name (str "Cloned: " (:name file))
|
||||
:features (:features file)
|
||||
:project-id project-id
|
||||
:profile-id profile-id})
|
||||
(assoc :data (:data file))
|
||||
(assoc :migrations (:migrations file)))]
|
||||
|
||||
(db/run! pool (fn [{:keys [::db/conn] :as cfg}]
|
||||
(create-file cfg {:id file-id
|
||||
:name (str "Cloned file: " filename)
|
||||
:project-id project-id
|
||||
:profile-id profile-id})
|
||||
(db/update! conn :file
|
||||
{:data data}
|
||||
{:id file-id})
|
||||
{::yres/status 201
|
||||
::yres/body "OK CREATED"})))
|
||||
(feat.fmig/reset-migrations! conn file)
|
||||
(db/update! conn :file
|
||||
{:data (:data file)}
|
||||
{:id (:id file)}
|
||||
{::db/return-keys false})
|
||||
|
||||
|
||||
{::yres/status 201
|
||||
::yres/body "OK CLONED"})))
|
||||
|
||||
:else
|
||||
(prepare-response (blob/decode data))))))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-params
|
||||
:hint "invalid button"))
|
||||
|
||||
(ex/raise :type :not-found
|
||||
:code :enpty-data
|
||||
:hint "empty response"))))
|
||||
|
||||
(defn- is-file-exists?
|
||||
[pool id]
|
||||
@@ -123,81 +131,61 @@
|
||||
(-> (db/exec-one! pool [sql id]) :exists)))
|
||||
|
||||
(defn- upload-file-data
|
||||
[{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::session/profile-id params] :as request}]
|
||||
(let [profile (profile/get-profile pool profile-id)
|
||||
project-id (:default-project-id profile)
|
||||
data (some-> params :file :path io/read*)]
|
||||
file (some-> params :file :path io/read* t/decode)]
|
||||
|
||||
(if (and data project-id)
|
||||
(let [fname (str "Imported file *: " (dt/now))
|
||||
(if (and file project-id)
|
||||
(let [fname (str "Imported: " (:name file) "(" (ct/now) ")")
|
||||
reuse-id? (contains? params :reuseid)
|
||||
file-id (or (and reuse-id? (ex/ignoring (-> params :file :filename parse-uuid)))
|
||||
(uuid/next))]
|
||||
|
||||
(if (and reuse-id? file-id
|
||||
(is-file-exists? pool file-id))
|
||||
(do
|
||||
(db/update! pool :file
|
||||
{:data data
|
||||
:deleted-at nil}
|
||||
{:id file-id})
|
||||
{::yres/status 200
|
||||
::yres/body "OK UPDATED"})
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(db/update! conn :file
|
||||
{:data (:data file)
|
||||
:features (into-array (:features file))
|
||||
:deleted-at nil}
|
||||
{:id file-id}
|
||||
{::db/return-keys false})
|
||||
(feat.fmig/reset-migrations! conn file)
|
||||
{::yres/status 200
|
||||
::yres/body "OK UPDATED"}))
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [file (-> (create-file cfg {:id file-id
|
||||
:name fname
|
||||
:features (:features file)
|
||||
:project-id project-id
|
||||
:profile-id profile-id})
|
||||
(assoc :data (:data file))
|
||||
(assoc :migrations (:migrations file)))]
|
||||
|
||||
(db/run! pool (fn [{:keys [::db/conn] :as cfg}]
|
||||
(create-file cfg {:id file-id
|
||||
:name fname
|
||||
:project-id project-id
|
||||
:profile-id profile-id})
|
||||
(db/update! conn :file
|
||||
{:data data}
|
||||
{:id file-id})
|
||||
{:data (:data file)}
|
||||
{:id file-id}
|
||||
{::db/return-keys false})
|
||||
(feat.fmig/reset-migrations! conn file)
|
||||
{::yres/status 201
|
||||
::yres/body "OK CREATED"}))))
|
||||
::yres/body "OK CREATED"})))))
|
||||
|
||||
{::yres/status 500
|
||||
::yres/body "ERROR"})))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-params
|
||||
:hint "invalid file uploaded"))))
|
||||
|
||||
(defn file-data-handler
|
||||
(defn raw-export-import-handler
|
||||
[cfg request]
|
||||
(case (yreq/method request)
|
||||
:get (retrieve-file-data cfg request)
|
||||
:get (download-file-data cfg request)
|
||||
:post (upload-file-data cfg request)
|
||||
(ex/raise :type :http
|
||||
:code :method-not-found)))
|
||||
|
||||
(defn file-changes-handler
|
||||
[{:keys [::db/pool]} {:keys [params] :as request}]
|
||||
(letfn [(retrieve-changes [file-id revn]
|
||||
(if (str/includes? revn ":")
|
||||
(let [[start end] (->> (str/split revn #":")
|
||||
(map str/trim)
|
||||
(map parse-long))]
|
||||
(some->> (db/exec! pool [sql:retrieve-range-of-changes file-id start end])
|
||||
(map :changes)
|
||||
(map blob/decode)
|
||||
(mapcat identity)
|
||||
(vec)))
|
||||
|
||||
(if-let [revn (parse-long revn)]
|
||||
(let [item (db/exec-one! pool [sql:retrieve-single-change file-id revn])]
|
||||
(some-> item :changes blob/decode vec))
|
||||
(ex/raise :type :validation :code :invalid-arguments))))]
|
||||
|
||||
(let [file-id (some-> params :id parse-uuid)
|
||||
revn (or (some-> params :revn parse-long) "latest")
|
||||
filename (str file-id)]
|
||||
|
||||
(when (or (not file-id) (not revn))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-arguments
|
||||
:hint "missing arguments"))
|
||||
|
||||
(let [data (retrieve-changes file-id revn)]
|
||||
(if (contains? params :download)
|
||||
(prepare-download-response data filename)
|
||||
(prepare-response data))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ERROR BROWSER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -234,7 +222,7 @@
|
||||
(-> (io/resource "app/templates/error-report.v3.tmpl")
|
||||
(tmpl/render (-> content
|
||||
(assoc :id id)
|
||||
(assoc :created-at (dt/format-instant created-at :rfc1123))))))]
|
||||
(assoc :created-at (ct/format-inst created-at :rfc1123))))))]
|
||||
|
||||
(if-let [report (get-report request)]
|
||||
(let [result (case (:version report)
|
||||
@@ -258,7 +246,7 @@
|
||||
(defn error-list-handler
|
||||
[{:keys [::db/pool]} _request]
|
||||
(let [items (->> (db/exec! pool [sql:error-reports])
|
||||
(map #(update % :created-at dt/format-instant :rfc1123)))]
|
||||
(map #(update % :created-at ct/format-inst :rfc1123)))]
|
||||
{::yres/status 200
|
||||
::yres/body (-> (io/resource "app/templates/error-list.tmpl")
|
||||
(tmpl/render {:items items}))
|
||||
@@ -430,49 +418,49 @@
|
||||
::yres/body "OK"}))
|
||||
|
||||
|
||||
(defn- add-team-feature
|
||||
[{:keys [params] :as request}]
|
||||
(let [team-id (some-> params :team-id d/parse-uuid)
|
||||
feature (some-> params :feature str)
|
||||
(defn- handle-team-features
|
||||
[cfg {:keys [params] :as request}]
|
||||
(let [team-id (some-> params :team-id d/parse-uuid)
|
||||
feature (some-> params :feature str)
|
||||
action (some-> params :action)
|
||||
skip-check (contains? params :skip-check)]
|
||||
|
||||
(when-not (contains? params :force)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-force
|
||||
:hint "missing force checkbox"))
|
||||
|
||||
(when (nil? team-id)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-team-id
|
||||
:hint "provided invalid team id"))
|
||||
|
||||
(srepl/enable-team-feature! team-id feature :skip-check skip-check)
|
||||
(if (= action "show")
|
||||
(let [team (db/run! cfg teams/get-team-info {:id team-id})]
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body (apply str "Team features:\n"
|
||||
(->> (:features team)
|
||||
(map (fn [feature]
|
||||
(str "- " feature "\n")))))})
|
||||
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK"}))
|
||||
(do
|
||||
(when-not (contains? params :force)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-force
|
||||
:hint "missing force checkbox"))
|
||||
|
||||
(defn- remove-team-feature
|
||||
[{:keys [params] :as request}]
|
||||
(let [team-id (some-> params :team-id d/parse-uuid)
|
||||
feature (some-> params :feature str)
|
||||
skip-check (contains? params :skip-check)]
|
||||
(cond
|
||||
(= action "enable")
|
||||
(srepl/enable-team-feature! team-id feature :skip-check skip-check)
|
||||
|
||||
(when-not (contains? params :force)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-force
|
||||
:hint "missing force checkbox"))
|
||||
(= action "disable")
|
||||
(srepl/disable-team-feature! team-id feature :skip-check skip-check)
|
||||
|
||||
(when (nil? team-id)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-team-id
|
||||
:hint "provided invalid team id"))
|
||||
:else
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-action
|
||||
:hint (str "invalid action: " action)))
|
||||
|
||||
(srepl/disable-team-feature! team-id feature :skip-check skip-check)
|
||||
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK"}))
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK"}))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OTHER SMALL VIEWS/HANDLERS
|
||||
@@ -525,6 +513,25 @@
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed)))))})
|
||||
|
||||
(def errors
|
||||
(letfn [(handle-error [cause]
|
||||
(when-let [data (ex-data cause)]
|
||||
(when (= :validation (:type data))
|
||||
(str "Error: " (or (:hint data) (ex-message cause)) "\n"))))]
|
||||
{:name ::errors
|
||||
:compile
|
||||
(fn [& _params]
|
||||
(fn [handler]
|
||||
(fn [request]
|
||||
(try
|
||||
(handler request)
|
||||
(catch Throwable cause
|
||||
(let [body (or (handle-error cause)
|
||||
(ex/format-throwable cause))]
|
||||
{::yres/status 400
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body body}))))))}))
|
||||
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expected a valid database pool")
|
||||
@@ -540,15 +547,14 @@
|
||||
["/changelog" {:handler (partial changelog-handler cfg)}]
|
||||
["/error/:id" {:handler (partial error-handler cfg)}]
|
||||
["/error" {:handler (partial error-list-handler cfg)}]
|
||||
["/actions/resend-email-verification"
|
||||
{:handler (partial resend-email-notification cfg)}]
|
||||
["/actions/reset-file-version"
|
||||
{:handler (partial reset-file-version cfg)}]
|
||||
["/actions/add-team-feature"
|
||||
{:handler (partial add-team-feature)}]
|
||||
["/actions/remove-team-feature"
|
||||
{:handler (partial remove-team-feature)}]
|
||||
["/file/export" {:handler (partial export-handler cfg)}]
|
||||
["/file/import" {:handler (partial import-handler cfg)}]
|
||||
["/file/data" {:handler (partial file-data-handler cfg)}]
|
||||
["/file/changes" {:handler (partial file-changes-handler cfg)}]]])
|
||||
["/actions" {:middleware [[errors]]}
|
||||
["/resend-email-verification"
|
||||
{:handler (partial resend-email-notification cfg)}]
|
||||
["/reset-file-version"
|
||||
{:handler (partial reset-file-version cfg)}]
|
||||
["/handle-team-features"
|
||||
{:handler (partial handle-team-features cfg)}]
|
||||
["/file-export" {:handler (partial export-handler cfg)}]
|
||||
["/file-import" {:handler (partial import-handler cfg)}]
|
||||
["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}]]]])
|
||||
|
||||
|
||||
234
backend/src/app/http/management.clj
Normal file
234
backend/src/app/http/management.clj
Normal file
@@ -0,0 +1,234 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.http.management
|
||||
"Internal mangement HTTP API"
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc.commands.profile :as cmd.profile]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.worker :as-alias wrk]
|
||||
[integrant.core :as ig]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
;; ---- ROUTES
|
||||
|
||||
(declare ^:private authenticate)
|
||||
(declare ^:private get-customer)
|
||||
(declare ^:private update-customer)
|
||||
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
|
||||
|
||||
(def ^:private default-system
|
||||
{:name ::default-system
|
||||
:compile
|
||||
(fn [_ _]
|
||||
(fn [handler cfg]
|
||||
(fn [request]
|
||||
(handler cfg request))))})
|
||||
|
||||
(def ^:private transaction
|
||||
{:name ::transaction
|
||||
:compile
|
||||
(fn [data _]
|
||||
(when (:transaction data)
|
||||
(fn [handler]
|
||||
(fn [cfg request]
|
||||
(db/tx-run! cfg handler request)))))})
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
["" {:middleware [[default-system cfg]
|
||||
[transaction]]}
|
||||
["/authenticate"
|
||||
{:handler authenticate
|
||||
:allowed-methods #{:post}}]
|
||||
|
||||
["/get-customer"
|
||||
{:handler get-customer
|
||||
:transaction true
|
||||
:allowed-methods #{:post}}]
|
||||
|
||||
["/update-customer"
|
||||
{:handler update-customer
|
||||
:allowed-methods #{:post}
|
||||
:transaction true}]])
|
||||
|
||||
;; ---- HELPERS
|
||||
|
||||
(defn- coercer
|
||||
[schema & {:as opts}]
|
||||
(let [decode-fn (sm/decoder schema sm/json-transformer)
|
||||
check-fn (sm/check-fn schema opts)]
|
||||
(fn [data]
|
||||
(-> data decode-fn check-fn))))
|
||||
|
||||
;; ---- API: AUTHENTICATE
|
||||
|
||||
(defn- authenticate
|
||||
[cfg request]
|
||||
(let [token (-> request :params :token)
|
||||
props (get cfg ::setup/props)
|
||||
result (tokens/verify props {:token token :iss "authentication"})]
|
||||
{::yres/status 200
|
||||
::yres/body result}))
|
||||
|
||||
;; ---- API: GET-CUSTOMER
|
||||
|
||||
(def ^:private schema:get-customer
|
||||
[:map [:id ::sm/uuid]])
|
||||
|
||||
(def ^:private coerce-get-customer-params
|
||||
(coercer schema:get-customer
|
||||
:type :validation
|
||||
:hint "invalid data provided for `get-customer` rpc call"))
|
||||
|
||||
(def ^:private sql:get-customer-slots
|
||||
"WITH teams AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
tpr.profile_id AS profile_id
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.is_owner IS true
|
||||
AND tpr.profile_id = ?
|
||||
), teams_with_slots AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
count(*) AS total
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.team_id IN (SELECT id FROM teams)
|
||||
AND tpr.can_edit IS true
|
||||
GROUP BY 1
|
||||
ORDER BY 2
|
||||
)
|
||||
SELECT max(total) AS total FROM teams_with_slots;")
|
||||
|
||||
(defn- get-customer-slots
|
||||
[cfg profile-id]
|
||||
(let [result (db/exec-one! cfg [sql:get-customer-slots profile-id])]
|
||||
(:total result)))
|
||||
|
||||
(defn- get-customer
|
||||
[cfg request]
|
||||
(let [profile-id (-> request :params coerce-get-customer-params :id)
|
||||
profile (cmd.profile/get-profile cfg profile-id)
|
||||
result {:id (get profile :id)
|
||||
:name (get profile :fullname)
|
||||
:email (get profile :email)
|
||||
:num-editors (get-customer-slots cfg profile-id)
|
||||
:subscription (-> profile :props :subscription)}]
|
||||
{::yres/status 200
|
||||
::yres/body result}))
|
||||
|
||||
|
||||
;; ---- API: UPDATE-CUSTOMER
|
||||
|
||||
(def ^:private schema:timestamp
|
||||
(sm/type-schema
|
||||
{:type ::timestamp
|
||||
:pred ct/inst?
|
||||
:type-properties
|
||||
{:title "inst"
|
||||
:description "The same as :app.common.time/inst but encodes to epoch"
|
||||
:error/message "should be an instant"
|
||||
:gen/gen (->> (sg/small-int)
|
||||
(sg/fmap (fn [v] (ct/inst v))))
|
||||
:decode/string ct/inst
|
||||
:encode/string inst-ms
|
||||
:decode/json ct/inst
|
||||
:encode/json inst-ms}}))
|
||||
|
||||
(def ^:private schema:subscription
|
||||
[:map {:title "Subscription"}
|
||||
[:id ::sm/text]
|
||||
[:customer-id ::sm/text]
|
||||
[:type [:enum
|
||||
"unlimited"
|
||||
"professional"
|
||||
"enterprise"]]
|
||||
[:status [:enum
|
||||
"active"
|
||||
"canceled"
|
||||
"incomplete"
|
||||
"incomplete_expired"
|
||||
"past_due"
|
||||
"paused"
|
||||
"trialing"
|
||||
"unpaid"]]
|
||||
|
||||
[:billing-period [:enum
|
||||
"month"
|
||||
"day"
|
||||
"week"
|
||||
"year"]]
|
||||
[:quantity :int]
|
||||
[:description [:maybe ::sm/text]]
|
||||
[:created-at schema:timestamp]
|
||||
[:start-date [:maybe schema:timestamp]]
|
||||
[:ended-at [:maybe schema:timestamp]]
|
||||
[:trial-end [:maybe schema:timestamp]]
|
||||
[:trial-start [:maybe schema:timestamp]]
|
||||
[:cancel-at [:maybe schema:timestamp]]
|
||||
[:canceled-at [:maybe schema:timestamp]]
|
||||
[:current-period-end [:maybe schema:timestamp]]
|
||||
[:current-period-start [:maybe schema:timestamp]]
|
||||
[:cancel-at-period-end :boolean]
|
||||
|
||||
[:cancellation-details
|
||||
[:map {:title "CancellationDetails"}
|
||||
[:comment [:maybe ::sm/text]]
|
||||
[:reason [:maybe ::sm/text]]
|
||||
[:feedback [:maybe
|
||||
[:enum
|
||||
"customer_service"
|
||||
"low_quality"
|
||||
"missing_feature"
|
||||
"other"
|
||||
"switched_service"
|
||||
"too_complex"
|
||||
"too_expensive"
|
||||
"unused"]]]]]])
|
||||
|
||||
(def ^:private schema:update-customer
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:subscription [:maybe schema:subscription]]])
|
||||
|
||||
(def ^:private coerce-update-customer-params
|
||||
(coercer schema:update-customer
|
||||
:type :validation
|
||||
:hint "invalid data provided for `update-customer` rpc call"))
|
||||
|
||||
(defn- update-customer
|
||||
[cfg request]
|
||||
(let [{:keys [id subscription]}
|
||||
(-> request :params coerce-update-customer-params)
|
||||
|
||||
{:keys [props] :as profile}
|
||||
(cmd.profile/get-profile cfg id ::db/for-update true)
|
||||
|
||||
props
|
||||
(assoc props :subscription subscription)]
|
||||
|
||||
(l/dbg :hint "update customer"
|
||||
:profile-id (str id)
|
||||
:subscription-type (get subscription :type)
|
||||
:subscription-status (get subscription :status)
|
||||
:subscription-quantity (get subscription :quantity))
|
||||
|
||||
(db/update! cfg :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
|
||||
{::yres/status 201
|
||||
::yres/body nil}))
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -18,7 +19,6 @@
|
||||
[app.main :as-alias main]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[yetti.request :as yreq]))
|
||||
@@ -35,10 +35,10 @@
|
||||
(def default-auth-data-cookie-name "auth-data")
|
||||
|
||||
;; Default value for cookie max-age
|
||||
(def default-cookie-max-age (dt/duration {:days 7}))
|
||||
(def default-cookie-max-age (ct/duration {:days 7}))
|
||||
|
||||
;; Default age for automatic session renewal
|
||||
(def default-renewal-max-age (dt/duration {:hours 6}))
|
||||
(def default-renewal-max-age (ct/duration {:hours 6}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PROTOCOLS
|
||||
@@ -66,7 +66,7 @@
|
||||
[:map {:title "session-params"}
|
||||
[:user-agent ::sm/text]
|
||||
[:profile-id ::sm/uuid]
|
||||
[:created-at ::sm/inst]])
|
||||
[:created-at ::ct/inst]])
|
||||
|
||||
(def ^:private valid-params?
|
||||
(sm/validator schema:params))
|
||||
@@ -95,7 +95,7 @@
|
||||
params))
|
||||
|
||||
(update! [_ params]
|
||||
(let [updated-at (dt/now)]
|
||||
(let [updated-at (ct/now)]
|
||||
(db/update! pool :http-session
|
||||
{:updated-at updated-at}
|
||||
{:id (:id params)})
|
||||
@@ -118,7 +118,7 @@
|
||||
params))
|
||||
|
||||
(update! [_ params]
|
||||
(let [updated-at (dt/now)]
|
||||
(let [updated-at (ct/now)]
|
||||
(swap! cache update (:id params) assoc :updated-at updated-at)
|
||||
(assoc params :updated-at updated-at)))
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
(let [uagent (yreq/get-header request "user-agent")
|
||||
params {:profile-id profile-id
|
||||
:user-agent uagent
|
||||
:created-at (dt/now)}
|
||||
:created-at (ct/now)}
|
||||
token (gen-token props params)
|
||||
session (write! manager token params)]
|
||||
(l/trace :hint "create" :profile-id (str profile-id))
|
||||
@@ -203,8 +203,8 @@
|
||||
|
||||
(defn- renew-session?
|
||||
[{:keys [updated-at] :as session}]
|
||||
(and (dt/instant? updated-at)
|
||||
(let [elapsed (dt/diff updated-at (dt/now))]
|
||||
(and (ct/inst? updated-at)
|
||||
(let [elapsed (ct/diff updated-at (ct/now))]
|
||||
(neg? (compare default-renewal-max-age elapsed)))))
|
||||
|
||||
(defn- wrap-soft-auth
|
||||
@@ -256,14 +256,14 @@
|
||||
(defn- assign-auth-token-cookie
|
||||
[response {token :id updated-at :updated-at}]
|
||||
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
|
||||
created-at (or updated-at (dt/now))
|
||||
renewal (dt/plus created-at default-renewal-max-age)
|
||||
expires (dt/plus created-at max-age)
|
||||
created-at (or updated-at (ct/now))
|
||||
renewal (ct/plus created-at default-renewal-max-age)
|
||||
expires (ct/plus created-at max-age)
|
||||
secure? (contains? cf/flags :secure-session-cookies)
|
||||
strict? (contains? cf/flags :strict-session-cookies)
|
||||
cors? (contains? cf/flags :cors)
|
||||
name (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
|
||||
comment (str "Renewal at: " (ct/format-inst renewal :rfc1123))
|
||||
cookie {:path "/"
|
||||
:http-only true
|
||||
:expires expires
|
||||
@@ -279,11 +279,11 @@
|
||||
domain (cf/get :auth-data-cookie-domain)
|
||||
cname default-auth-data-cookie-name
|
||||
|
||||
created-at (or updated-at (dt/now))
|
||||
renewal (dt/plus created-at default-renewal-max-age)
|
||||
expires (dt/plus created-at max-age)
|
||||
created-at (or updated-at (ct/now))
|
||||
renewal (ct/plus created-at default-renewal-max-age)
|
||||
expires (ct/plus created-at max-age)
|
||||
|
||||
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
|
||||
comment (str "Renewal at: " (ct/format-inst renewal :rfc1123))
|
||||
secure? (contains? cf/flags :secure-session-cookies)
|
||||
strict? (contains? cf/flags :strict-session-cookies)
|
||||
cors? (contains? cf/flags :cors)
|
||||
@@ -323,7 +323,7 @@
|
||||
(defmethod ig/assert-key ::tasks/gc
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expected valid database pool")
|
||||
(assert (dt/duration? (::tasks/max-age params))))
|
||||
(assert (ct/duration? (::tasks/max-age params))))
|
||||
|
||||
(defmethod ig/expand-key ::tasks/gc
|
||||
[k v]
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
(println "event:" (d/name name))
|
||||
(println "data:" (t/encode-str data {:type :json-verbose}))
|
||||
(println))]
|
||||
(.getBytes data "UTF-8"))
|
||||
(.getBytes ^String data "UTF-8"))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "unexpected error on encoding value on sse stream"
|
||||
:cause cause)
|
||||
@@ -54,7 +54,7 @@
|
||||
::yres/body (yres/stream-body
|
||||
(fn [_ output]
|
||||
(let [channel (sp/chan :buf buf :xf (keep encode))
|
||||
listener (events/start-listener
|
||||
listener (events/spawn-listener
|
||||
channel
|
||||
(partial write! output)
|
||||
(partial pu/close! output))]
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http.session :as session]
|
||||
[app.metrics :as mtx]
|
||||
[app.msgbus :as mbus]
|
||||
[app.util.time :as dt]
|
||||
[app.util.websocket :as ws]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec.csp :as sp]
|
||||
@@ -239,7 +239,7 @@
|
||||
|
||||
(defn- on-connect
|
||||
[{:keys [::mtx/metrics]} {:keys [::ws/id] :as wsp}]
|
||||
(let [created-at (dt/now)]
|
||||
(let [created-at (ct/now)]
|
||||
(l/trace :fn "on-connect" :conn-id id)
|
||||
(swap! state assoc id wsp)
|
||||
(mtx/run! metrics
|
||||
@@ -253,7 +253,7 @@
|
||||
(mtx/run! metrics :id :websocket-active-connections :dec 1)
|
||||
(mtx/run! metrics
|
||||
:id :websocket-session-timing
|
||||
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0))))))
|
||||
:val (/ (inst-ms (ct/diff created-at (ct/now))) 1000.0))))))
|
||||
|
||||
(defn- on-rcv-message
|
||||
[{:keys [::mtx/metrics ::profile-id ::session-id]} message]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -23,7 +24,6 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.util.inet :as inet]
|
||||
[app.util.services :as-alias sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -108,9 +108,9 @@
|
||||
[::ip-addr {:optional true} ::sm/text]
|
||||
[::props {:optional true} [:map-of :keyword :any]]
|
||||
[::context {:optional true} [:map-of :keyword :any]]
|
||||
[::tracked-at {:optional true} ::sm/inst]
|
||||
[::tracked-at {:optional true} ::ct/inst]
|
||||
[::webhooks/event? {:optional true} ::sm/boolean]
|
||||
[::webhooks/batch-timeout {:optional true} ::dt/duration]
|
||||
[::webhooks/batch-timeout {:optional true} ::ct/duration]
|
||||
[::webhooks/batch-key {:optional true}
|
||||
[:or ::sm/fn ::sm/text :keyword]]])
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
(defn- handle-event!
|
||||
[cfg event]
|
||||
(let [params (event->params event)
|
||||
tnow (dt/now)]
|
||||
tnow (ct/now)]
|
||||
|
||||
(when (contains? cf/flags :audit-log)
|
||||
;; NOTE: this operation may cause primary key conflicts on inserts
|
||||
@@ -273,7 +273,7 @@
|
||||
(let [event (-> (d/without-nils event)
|
||||
(check-event))]
|
||||
(db/run! cfg (fn [cfg]
|
||||
(let [tnow (dt/now)
|
||||
(let [tnow (ct/now)
|
||||
params (-> (event->params event)
|
||||
(assoc :created-at tnow)
|
||||
(update :tracked-at #(or % tnow)))]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -16,7 +17,6 @@
|
||||
[app.http.client :as http]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.time :as dt]
|
||||
[integrant.core :as ig]
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.exec :as px]))
|
||||
@@ -55,7 +55,7 @@
|
||||
[{:keys [::uri] :as cfg} events]
|
||||
(let [token (tokens/generate (::setup/props cfg)
|
||||
{:iss "authentication"
|
||||
:iat (dt/now)
|
||||
:iat (ct/now)
|
||||
:uid uuid/zero})
|
||||
body (t/encode {:events events})
|
||||
headers {"content-type" "application/transit+json"
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uri :as uri]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.data.json :as json]
|
||||
[cuerdas.core :as str]
|
||||
@@ -124,7 +124,7 @@
|
||||
{:id (:id whook)})))
|
||||
|
||||
(db/update! pool :webhook
|
||||
{:updated-at (dt/now)
|
||||
{:updated-at (ct/now)
|
||||
:error-code nil
|
||||
:error-count 0}
|
||||
{:id (:id whook)})))
|
||||
@@ -132,7 +132,7 @@
|
||||
(report-delivery! [whook req rsp err]
|
||||
(db/insert! pool :webhook-delivery
|
||||
{:webhook-id (:id whook)
|
||||
:created-at (dt/now)
|
||||
:created-at (ct/now)
|
||||
:error-code err
|
||||
:req-data (db/tjson req)
|
||||
:rsp-data (db/tjson rsp)}))]
|
||||
@@ -155,7 +155,7 @@
|
||||
(let [req {:uri (:uri whook)
|
||||
:headers {"content-type" (:mtype whook)
|
||||
"user-agent" (str/ffmt "penpot/%" (:main cf/version))}
|
||||
:timeout (dt/duration "4s")
|
||||
:timeout (ct/duration "4s")
|
||||
:method :post
|
||||
:body body}]
|
||||
(try
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.auth.oidc.providers :as-alias oidc.providers]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
[app.email :as-alias email]
|
||||
@@ -19,6 +20,7 @@
|
||||
[app.http.awsns :as http.awsns]
|
||||
[app.http.client :as-alias http.client]
|
||||
[app.http.debug :as-alias http.debug]
|
||||
[app.http.management :as mgmt]
|
||||
[app.http.session :as-alias session]
|
||||
[app.http.session.tasks :as-alias session.tasks]
|
||||
[app.http.websocket :as http.ws]
|
||||
@@ -38,8 +40,9 @@
|
||||
[app.storage.gc-touched :as-alias sto.gc-touched]
|
||||
[app.storage.s3 :as-alias sto.s3]
|
||||
[app.svgo :as-alias svgo]
|
||||
[app.util.time :as dt]
|
||||
[app.util.cron]
|
||||
[app.worker :as-alias wrk]
|
||||
[app.worker.executor]
|
||||
[clojure.test :as test]
|
||||
[clojure.tools.namespace.repl :as repl]
|
||||
[cuerdas.core :as str]
|
||||
@@ -146,23 +149,11 @@
|
||||
::mdef/labels []
|
||||
::mdef/type :histogram}
|
||||
|
||||
:executors-active-threads
|
||||
{::mdef/name "penpot_executors_active_threads"
|
||||
::mdef/help "Current number of threads available in the executor service."
|
||||
::mdef/labels ["name"]
|
||||
::mdef/type :gauge}
|
||||
|
||||
:executors-completed-tasks
|
||||
{::mdef/name "penpot_executors_completed_tasks_total"
|
||||
::mdef/help "Approximate number of completed tasks by the executor."
|
||||
::mdef/labels ["name"]
|
||||
::mdef/type :counter}
|
||||
|
||||
:executors-running-threads
|
||||
{::mdef/name "penpot_executors_running_threads"
|
||||
::mdef/help "Current number of threads with state RUNNING."
|
||||
::mdef/labels ["name"]
|
||||
::mdef/type :gauge}})
|
||||
:http-server-dispatch-timing
|
||||
{::mdef/name "penpot_http_server_dispatch_timing"
|
||||
::mdef/help "Histogram of dispatch handler"
|
||||
::mdef/labels []
|
||||
::mdef/type :histogram}})
|
||||
|
||||
(def system-config
|
||||
{::db/pool
|
||||
@@ -174,14 +165,12 @@
|
||||
::db/max-size (cf/get :database-max-pool-size 60)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||
|
||||
;; Default thread pool for IO operations
|
||||
::wrk/executor
|
||||
{}
|
||||
;; Default netty IO pool (shared between several services)
|
||||
::wrk/netty-io-executor
|
||||
{:threads (cf/get :netty-io-threads)}
|
||||
|
||||
::wrk/monitor
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::wrk/name "default"}
|
||||
::wrk/netty-executor
|
||||
{:threads (cf/get :executor-threads)}
|
||||
|
||||
:app.migrations/migrations
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
@@ -195,14 +184,19 @@
|
||||
::rds/redis
|
||||
{::rds/uri (cf/get :redis-uri)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
::wrk/netty-executor
|
||||
(ig/ref ::wrk/netty-executor)
|
||||
|
||||
::wrk/netty-io-executor
|
||||
(ig/ref ::wrk/netty-io-executor)}
|
||||
|
||||
::mbus/msgbus
|
||||
{::wrk/executor (ig/ref ::wrk/executor)
|
||||
{::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::rds/redis (ig/ref ::rds/redis)}
|
||||
|
||||
:app.storage.tmp/cleaner
|
||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||
{::wrk/executor (ig/ref ::wrk/netty-executor)}
|
||||
|
||||
::sto.gc-deleted/handler
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
@@ -230,8 +224,10 @@
|
||||
::http/host (cf/get :http-server-host)
|
||||
::http/router (ig/ref ::http/router)
|
||||
::http/io-threads (cf/get :http-server-io-threads)
|
||||
::http/max-worker-threads (cf/get :http-server-max-worker-threads)
|
||||
::http/max-body-size (cf/get :http-server-max-body-size)
|
||||
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
||||
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||
|
||||
::ldap/provider
|
||||
{:host (cf/get :ldap-host)
|
||||
@@ -271,6 +267,10 @@
|
||||
::email/blacklist (ig/ref ::email/blacklist)
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
|
||||
::mgmt/routes
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
|
||||
:app.http/router
|
||||
{::session/manager (ig/ref ::session/manager)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
@@ -279,6 +279,7 @@
|
||||
::setup/props (ig/ref ::setup/props)
|
||||
::mtx/routes (ig/ref ::mtx/routes)
|
||||
::oidc/routes (ig/ref ::oidc/routes)
|
||||
::mgmt/routes (ig/ref ::mgmt/routes)
|
||||
::http.debug/routes (ig/ref ::http.debug/routes)
|
||||
::http.assets/routes (ig/ref ::http.assets/routes)
|
||||
::http.ws/routes (ig/ref ::http.ws/routes)
|
||||
@@ -298,23 +299,23 @@
|
||||
|
||||
:app.http.assets/routes
|
||||
{::http.assets/path (cf/get :assets-path)
|
||||
::http.assets/cache-max-age (dt/duration {:hours 24})
|
||||
::http.assets/cache-max-agesignature-max-age (dt/duration {:hours 24 :minutes 5})
|
||||
::http.assets/cache-max-age (ct/duration {:hours 24})
|
||||
::http.assets/cache-max-agesignature-max-age (ct/duration {:hours 24 :minutes 5})
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
|
||||
::rpc/climit
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::climit/config (cf/get :rpc-climit-config)
|
||||
::climit/enabled (contains? cf/flags :rpc-climit)}
|
||||
|
||||
:app.rpc/rlimit
|
||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||
{::wrk/executor (ig/ref ::wrk/netty-executor)}
|
||||
|
||||
:app.rpc/methods
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::ldap/provider (ig/ref ::ldap/provider)
|
||||
::sto/storage (ig/ref ::sto/storage)
|
||||
@@ -468,45 +469,46 @@
|
||||
(cf/get :objects-storage-s3-bucket))
|
||||
::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads)
|
||||
(cf/get :objects-storage-s3-io-threads))
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
::wrk/netty-io-executor
|
||||
(ig/ref ::wrk/netty-io-executor)}
|
||||
|
||||
:app.storage.fs/backend
|
||||
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
|
||||
(cf/get :objects-storage-fs-directory))}})
|
||||
|
||||
|
||||
(def worker-config
|
||||
{::wrk/cron
|
||||
{::wrk/registry (ig/ref ::wrk/registry)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::wrk/entries
|
||||
[{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
[{:cron #penpot/cron "0 0 0 * * ?" ;; daily
|
||||
:task :session-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
|
||||
:task :objects-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
|
||||
:task :storage-gc-deleted}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
|
||||
:task :storage-gc-touched}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
|
||||
:task :tasks-gc}
|
||||
|
||||
{:cron #app/cron "0 0 2 * * ?" ;; daily
|
||||
{:cron #penpot/cron "0 0 2 * * ?" ;; daily
|
||||
:task :file-gc-scheduler}
|
||||
|
||||
{:cron #app/cron "0 30 */3,23 * * ?"
|
||||
{:cron #penpot/cron "0 30 */3,23 * * ?"
|
||||
:task :telemetry}
|
||||
|
||||
(when (contains? cf/flags :audit-log-archive)
|
||||
{:cron #app/cron "0 */5 * * * ?" ;; every 5m
|
||||
{:cron #penpot/cron "0 */5 * * * ?" ;; every 5m
|
||||
:task :audit-log-archive})
|
||||
|
||||
(when (contains? cf/flags :audit-log-gc)
|
||||
{:cron #app/cron "30 */5 * * * ?" ;; every 5m
|
||||
{:cron #penpot/cron "30 */5 * * * ?" ;; every 5m
|
||||
:task :audit-log-gc})]}
|
||||
|
||||
::wrk/dispatcher
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
[app.common.media :as cm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.openapi :as-alias oapi]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
[app.storage :as-alias sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.time :as dt]
|
||||
[buddy.core.bytes :as bb]
|
||||
[buddy.core.codecs :as bc]
|
||||
[clojure.java.shell :as sh]
|
||||
@@ -38,15 +38,13 @@
|
||||
org.im4java.core.Info))
|
||||
|
||||
(def schema:upload
|
||||
(sm/register!
|
||||
^{::sm/type ::upload}
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size ::sm/int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
[:map-of :string :string]]]))
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size ::sm/int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
[:map-of :string :string]]])
|
||||
|
||||
(def ^:private schema:input
|
||||
[:map {:title "Input"}
|
||||
@@ -118,7 +116,7 @@
|
||||
(defn- parse-svg
|
||||
[text]
|
||||
(let [text (strip-doctype text)]
|
||||
(dm/with-open [istream (IOUtils/toInputStream text "UTF-8")]
|
||||
(dm/with-open [istream (IOUtils/toInputStream ^String text "UTF-8")]
|
||||
(xml/parse istream secure-parser-factory))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -243,7 +241,7 @@
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-svg-file
|
||||
:hint "uploaded svg does not provides dimensions"))
|
||||
(merge input info {:ts (dt/now)}))
|
||||
(merge input info {:ts (ct/now)}))
|
||||
|
||||
(let [instance (Info. (str path))
|
||||
mtype' (.getProperty instance "Mime type")]
|
||||
@@ -263,7 +261,7 @@
|
||||
(assoc input
|
||||
:width width
|
||||
:height height
|
||||
:ts (dt/now)))))))
|
||||
:ts (ct/now)))))))
|
||||
|
||||
(defmethod process-error org.im4java.core.InfoException
|
||||
[error]
|
||||
|
||||
@@ -438,7 +438,16 @@
|
||||
:fn (mg/resource "app/migrations/sql/0138-mod-file-data-fragment-table.sql")}
|
||||
|
||||
{:name "0139-mod-file-change-table.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}])
|
||||
:fn (mg/resource "app/migrations/sql/0139-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0140-mod-file-change-table.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0140-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0140-add-locked-by-column-to-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0140-add-locked-by-column-to-file-change-table.sql")}
|
||||
|
||||
{:name "0141-add-idx-to-file-library-rel"
|
||||
:fn (mg/resource "app/migrations/sql/0141-add-idx-to-file-library-rel.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Add locked_by column to file_change table for version locking feature
|
||||
-- This allows users to lock their own saved versions to prevent deletion by others
|
||||
|
||||
ALTER TABLE file_change
|
||||
ADD COLUMN locked_by uuid NULL REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE;
|
||||
|
||||
-- Create index for locked versions queries
|
||||
CREATE INDEX file_change__locked_by__idx ON file_change (locked_by) WHERE locked_by IS NOT NULL;
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN file_change.locked_by IS 'Profile ID of user who has locked this version. Only the creator can lock/unlock their own versions. Locked versions cannot be deleted by others.';
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE file_change
|
||||
ADD COLUMN migrations text[];
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX IF NOT EXISTS file_library_rel__library_file_id__idx
|
||||
ON file_library_rel (library_file_id);
|
||||
@@ -10,10 +10,10 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cfg]
|
||||
[app.redis :as rds]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
@@ -56,7 +56,7 @@
|
||||
[k v]
|
||||
{k (-> (d/without-nils v)
|
||||
(assoc ::buffer-size 128)
|
||||
(assoc ::timeout (dt/duration {:seconds 30})))})
|
||||
(assoc ::timeout (ct/duration {:seconds 30})))})
|
||||
|
||||
(def ^:private schema:params
|
||||
[:map ::rds/redis ::wrk/executor])
|
||||
@@ -216,8 +216,7 @@
|
||||
(rds/add-listener sconn (create-listener rcv-ch))
|
||||
|
||||
(px/thread
|
||||
{:name "penpot/msgbus/io-loop"
|
||||
:virtual true}
|
||||
{:name "penpot/msgbus"}
|
||||
(try
|
||||
(loop []
|
||||
(let [timeout-ch (sp/timeout-chan 1000)
|
||||
|
||||
@@ -12,17 +12,16 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.metrics :as mtx]
|
||||
[app.redis.script :as-alias rscript]
|
||||
[app.util.cache :as cache]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.core :as c]
|
||||
[clojure.java.io :as io]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px])
|
||||
[promesa.core :as p])
|
||||
(:import
|
||||
clojure.lang.MapEntry
|
||||
io.lettuce.core.KeyValue
|
||||
@@ -45,8 +44,10 @@
|
||||
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
|
||||
io.lettuce.core.resource.ClientResources
|
||||
io.lettuce.core.resource.DefaultClientResources
|
||||
io.netty.channel.nio.NioEventLoopGroup
|
||||
io.netty.util.HashedWheelTimer
|
||||
io.netty.util.Timer
|
||||
io.netty.util.concurrent.EventExecutorGroup
|
||||
java.lang.AutoCloseable
|
||||
java.time.Duration))
|
||||
|
||||
@@ -111,21 +112,16 @@
|
||||
|
||||
(defmethod ig/expand-key ::redis
|
||||
[k v]
|
||||
(let [cpus (px/get-available-processors)
|
||||
threads (max 1 (int (* cpus 0.2)))]
|
||||
{k (-> (d/without-nils v)
|
||||
(assoc ::timeout (dt/duration "10s"))
|
||||
(assoc ::io-threads (max 3 threads))
|
||||
(assoc ::worker-threads (max 3 threads)))}))
|
||||
{k (-> (d/without-nils v)
|
||||
(assoc ::timeout (ct/duration "10s")))})
|
||||
|
||||
(def ^:private schema:redis-params
|
||||
[:map {:title "redis-params"}
|
||||
::wrk/executor
|
||||
::wrk/netty-io-executor
|
||||
::wrk/netty-executor
|
||||
::mtx/metrics
|
||||
[::uri ::sm/uri]
|
||||
[::worker-threads ::sm/int]
|
||||
[::io-threads ::sm/int]
|
||||
[::timeout ::dt/duration]])
|
||||
[::timeout ::ct/duration]])
|
||||
|
||||
(defmethod ig/assert-key ::redis
|
||||
[_ params]
|
||||
@@ -141,17 +137,30 @@
|
||||
|
||||
(defn- initialize-resources
|
||||
"Initialize redis connection resources"
|
||||
[{:keys [::uri ::io-threads ::worker-threads ::wrk/executor ::mtx/metrics] :as params}]
|
||||
[{:keys [::uri ::mtx/metrics ::wrk/netty-io-executor ::wrk/netty-executor] :as params}]
|
||||
|
||||
(l/inf :hint "initialize redis resources"
|
||||
:uri (str uri)
|
||||
:io-threads io-threads
|
||||
:worker-threads worker-threads)
|
||||
:uri (str uri))
|
||||
|
||||
(let [timer (HashedWheelTimer.)
|
||||
resources (.. (DefaultClientResources/builder)
|
||||
(ioThreadPoolSize ^long io-threads)
|
||||
(computationThreadPoolSize ^long worker-threads)
|
||||
(eventExecutorGroup ^EventExecutorGroup netty-executor)
|
||||
|
||||
;; We provide lettuce with a shared event loop
|
||||
;; group instance instead of letting lettuce to
|
||||
;; create its own
|
||||
(eventLoopGroupProvider
|
||||
(reify io.lettuce.core.resource.EventLoopGroupProvider
|
||||
(allocate [_ _] netty-io-executor)
|
||||
(threadPoolSize [_]
|
||||
(.executorCount ^NioEventLoopGroup netty-io-executor))
|
||||
(release [_ _ _ _ _]
|
||||
;; Do nothing
|
||||
)
|
||||
(shutdown [_ _ _ _]
|
||||
;; Do nothing
|
||||
)))
|
||||
|
||||
(timer ^Timer timer)
|
||||
(build))
|
||||
|
||||
@@ -166,7 +175,7 @@
|
||||
(l/trace :hint "evict connection (cache)" :key key :reason cause)
|
||||
(some-> val d/close!))
|
||||
|
||||
cache (cache/create :executor executor
|
||||
cache (cache/create :executor netty-executor
|
||||
:on-remove on-remove
|
||||
:keepalive "5m")]
|
||||
(reify
|
||||
@@ -331,7 +340,7 @@
|
||||
(p/rejected cause))))
|
||||
|
||||
(eval-script [sha]
|
||||
(let [tpoint (dt/tpoint)]
|
||||
(let [tpoint (ct/tpoint)]
|
||||
(->> (.evalsha ^RedisScriptingAsyncCommands cmd
|
||||
^String sha
|
||||
^ScriptOutputType ScriptOutputType/MULTI
|
||||
@@ -346,7 +355,7 @@
|
||||
:name (name sname)
|
||||
:sha sha
|
||||
:params (str/join "," (::rscript/vals script))
|
||||
:elapsed (dt/format-duration elapsed))
|
||||
:elapsed (ct/format-duration elapsed))
|
||||
result)))
|
||||
(p/merr on-error))))
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
@@ -31,7 +32,6 @@
|
||||
[app.storage :as-alias sto]
|
||||
[app.util.inet :as inet]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
@@ -103,7 +103,7 @@
|
||||
data (-> params
|
||||
(assoc ::handler-name handler-name)
|
||||
(assoc ::ip-addr ip-addr)
|
||||
(assoc ::request-at (dt/now))
|
||||
(assoc ::request-at (ct/now))
|
||||
(assoc ::external-session-id session-id)
|
||||
(assoc ::external-event-origin event-origin)
|
||||
(assoc ::session/id (::session/id request))
|
||||
@@ -130,7 +130,7 @@
|
||||
[{:keys [::mtx/metrics ::metrics-id]} f mdata]
|
||||
(let [labels (into-array String [(::sv/name mdata)])]
|
||||
(fn [cfg params]
|
||||
(let [tp (dt/tpoint)]
|
||||
(let [tp (ct/tpoint)]
|
||||
(try
|
||||
(f cfg params)
|
||||
(finally
|
||||
|
||||
@@ -11,17 +11,16 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.util.cache :as cache]
|
||||
[app.util.services :as-alias sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.set :as set]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.bulkhead :as pbh])
|
||||
(:import
|
||||
clojure.lang.ExceptionInfo
|
||||
@@ -154,7 +153,7 @@
|
||||
:id limit-id
|
||||
:label limit-label
|
||||
:queue queue
|
||||
:elapsed (some-> elapsed dt/format-duration)
|
||||
:elapsed (some-> elapsed ct/format-duration)
|
||||
:params @limit-params)))
|
||||
|
||||
(def ^:private idseq (AtomicLong. 0))
|
||||
@@ -171,19 +170,19 @@
|
||||
mlabels (into-array String [(id->str limit-id)])
|
||||
limit-id (id->str limit-id limit-key)
|
||||
limiter (cache/get cache limit-id (partial create-limiter config))
|
||||
tpoint (dt/tpoint)
|
||||
tpoint (ct/tpoint)
|
||||
req-id (.incrementAndGet ^AtomicLong idseq)]
|
||||
(try
|
||||
(let [stats (pbh/get-stats limiter)]
|
||||
(measure metrics mlabels stats nil)
|
||||
(log "enqueued" req-id stats limit-id limit-label limit-params nil))
|
||||
|
||||
(px/invoke! limiter (fn []
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
(measure metrics mlabels stats elapsed)
|
||||
(log "acquired" req-id stats limit-id limit-label limit-params elapsed)
|
||||
(handler))))
|
||||
(pbh/invoke! limiter (fn []
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
(measure metrics mlabels stats elapsed)
|
||||
(log "acquired" req-id stats limit-id limit-label limit-params elapsed)
|
||||
(handler))))
|
||||
|
||||
(catch ExceptionInfo cause
|
||||
(let [{:keys [type code]} (ex-data cause)]
|
||||
@@ -289,13 +288,9 @@
|
||||
(get-limits cfg)))
|
||||
|
||||
(defn invoke!
|
||||
"Run a function in context of climit.
|
||||
Intended to be used in virtual threads."
|
||||
[{:keys [::executor ::rpc/climit] :as cfg} f params]
|
||||
"Run a function in context of climit."
|
||||
[{:keys [::rpc/climit] :as cfg} f params]
|
||||
(let [f (if climit
|
||||
(let [f (if (some? executor)
|
||||
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
|
||||
f)]
|
||||
(build-exec-chain cfg f))
|
||||
(build-exec-chain cfg f)
|
||||
f)]
|
||||
(f cfg params)))
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.rpc.commands.access-token
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.main :as-alias main]
|
||||
@@ -15,8 +16,7 @@
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(defn- decode-row
|
||||
[row]
|
||||
@@ -24,13 +24,13 @@
|
||||
|
||||
(defn create-access-token
|
||||
[{:keys [::db/conn ::setup/props]} profile-id name expiration]
|
||||
(let [created-at (dt/now)
|
||||
(let [created-at (ct/now)
|
||||
token-id (uuid/next)
|
||||
token (tokens/generate props {:iss "access-token"
|
||||
:tid token-id
|
||||
:iat created-at})
|
||||
|
||||
expires-at (some-> expiration dt/in-future)
|
||||
expires-at (some-> expiration ct/in-future)
|
||||
token (db/insert! conn :access-token
|
||||
{:id token-id
|
||||
:name name
|
||||
@@ -49,7 +49,7 @@
|
||||
(def ^:private schema:create-access-token
|
||||
[:map {:title "create-access-token"}
|
||||
[:name [:string {:max 250 :min 1}]]
|
||||
[:expiration {:optional true} ::dt/duration]])
|
||||
[:expiration {:optional true} ::ct/duration]])
|
||||
|
||||
(sv/defmethod ::create-access-token
|
||||
{::doc/added "1.18"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -20,8 +21,7 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.util.inet :as inet]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(def ^:private event-columns
|
||||
[:id
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
(defn- adjust-timestamp
|
||||
[{:keys [timestamp created-at] :as event}]
|
||||
(let [margin (inst-ms (dt/diff timestamp created-at))]
|
||||
(let [margin (inst-ms (ct/diff timestamp created-at))]
|
||||
(if (or (neg? margin)
|
||||
(> margin 3600000))
|
||||
;; If event is in future or lags more than 1 hour, we reasign
|
||||
@@ -63,7 +63,7 @@
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
|
||||
(let [request (-> params meta ::http/request)
|
||||
ip-addr (inet/parse-request request)
|
||||
tnow (dt/now)
|
||||
tnow (ct/now)
|
||||
xform (comp
|
||||
(map (fn [event]
|
||||
(-> event
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
|
||||
(ns app.rpc.commands.auth
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -30,7 +32,6 @@
|
||||
[app.setup.welcome-file :refer [create-welcome-file]]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -42,7 +43,7 @@
|
||||
|
||||
(defn- elapsed-verify-threshold?
|
||||
[profile]
|
||||
(let [elapsed (dt/diff (:modified-at profile) (dt/now))
|
||||
(let [elapsed (ct/diff (:modified-at profile) (ct/now))
|
||||
verify-threshold (cf/get :email-verify-threshold)]
|
||||
(pos? (compare elapsed verify-threshold))))
|
||||
|
||||
@@ -62,7 +63,7 @@
|
||||
(ex/raise :type :validation
|
||||
:code :account-without-password
|
||||
:hint "the current account does not have password")
|
||||
(let [result (profile/verify-password cfg password (:password profile))]
|
||||
(let [result (auth/verify-password password (:password profile))]
|
||||
(when (:update result)
|
||||
(l/trc :hint "updating profile password"
|
||||
:id (str (:id profile))
|
||||
@@ -85,7 +86,7 @@
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
(when-let [deleted-at (:deleted-at profile)]
|
||||
(when (dt/is-after? (dt/now) deleted-at)
|
||||
(when (ct/is-after? (ct/now) deleted-at)
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials)))
|
||||
|
||||
@@ -156,7 +157,7 @@
|
||||
(:profile-id tdata)))
|
||||
|
||||
(update-password [conn profile-id]
|
||||
(let [pwd (profile/derive-password cfg password)]
|
||||
(let [pwd (auth/derive-password password)]
|
||||
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
|
||||
nil))]
|
||||
|
||||
@@ -244,7 +245,7 @@
|
||||
:backend "penpot"
|
||||
:iss :prepared-register
|
||||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 7})
|
||||
:exp (ct/in-future {:days 7})
|
||||
:props {:newsletter-updates (or accept-newsletter-updates false)}}
|
||||
|
||||
params (d/without-nils params)
|
||||
@@ -344,7 +345,7 @@
|
||||
[{:keys [::db/conn] :as cfg} profile]
|
||||
(let [vtoken (tokens/generate (::setup/props cfg)
|
||||
{:iss :verify-email
|
||||
:exp (dt/in-future "72h")
|
||||
:exp (ct/in-future "72h")
|
||||
:profile-id (:id profile)
|
||||
:email (:email profile)})
|
||||
;; NOTE: this token is mainly used for possible complains
|
||||
@@ -352,7 +353,7 @@
|
||||
ptoken (tokens/generate (::setup/props cfg)
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 30})})]
|
||||
:exp (ct/in-future {:days 30})})]
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/register
|
||||
:public-uri (cf/get :public-uri)
|
||||
@@ -378,7 +379,7 @@
|
||||
(not (contains? cf/flags :email-verification)))
|
||||
params (-> params
|
||||
(assoc :is-active is-active)
|
||||
(update :password #(profile/derive-password cfg %)))
|
||||
(update :password auth/derive-password))
|
||||
profile (->> (create-profile! conn params)
|
||||
(create-profile-rels! conn))]
|
||||
(vary-meta profile assoc :created true))))
|
||||
@@ -466,7 +467,7 @@
|
||||
|
||||
(when (= action "resend-email-verification")
|
||||
(db/update! conn :profile
|
||||
{:modified-at (dt/now)}
|
||||
{:modified-at (ct/now)}
|
||||
{:id (:id profile)})
|
||||
(send-email-verification! cfg profile))
|
||||
|
||||
@@ -495,7 +496,7 @@
|
||||
(letfn [(create-recovery-token [{:keys [id] :as profile}]
|
||||
(let [token (tokens/generate (::setup/props cfg)
|
||||
{:iss :password-recovery
|
||||
:exp (dt/in-future "15m")
|
||||
:exp (ct/in-future "15m")
|
||||
:profile-id id})]
|
||||
(assoc profile :token token)))
|
||||
|
||||
@@ -503,7 +504,7 @@
|
||||
(let [ptoken (tokens/generate (::setup/props cfg)
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 30})})]
|
||||
:exp (ct/in-future {:days 30})})]
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/password-recovery
|
||||
:public-uri (cf/get :public-uri)
|
||||
@@ -544,7 +545,7 @@
|
||||
:else
|
||||
(do
|
||||
(db/update! conn :profile
|
||||
{:modified-at (dt/now)}
|
||||
{:modified-at (ct/now)}
|
||||
{:id (:id profile)})
|
||||
(->> profile
|
||||
(create-recovery-token)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.sse :as sse]
|
||||
@@ -26,9 +27,7 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.tasks.file-gc]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[promesa.exec :as px]
|
||||
[yetti.response :as yres]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
@@ -94,7 +93,7 @@
|
||||
;; --- Command: import-binfile
|
||||
|
||||
(defn- import-binfile
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id project-id version name file]}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id project-id version name file]}]
|
||||
(let [team (teams/get-team pool
|
||||
:profile-id profile-id
|
||||
:project-id project-id)
|
||||
@@ -105,16 +104,12 @@
|
||||
(assoc ::bfc/name name)
|
||||
(assoc ::bfc/input (:path file)))
|
||||
|
||||
;; NOTE: the importation process performs some operations that are
|
||||
;; not very friendly with virtual threads, and for avoid
|
||||
;; unexpected blocking of other concurrent operations we dispatch
|
||||
;; that operation to a dedicated executor.
|
||||
result (case (int version)
|
||||
1 (px/invoke! executor (partial bf.v1/import-files! cfg))
|
||||
3 (px/invoke! executor (partial bf.v3/import-files! cfg)))]
|
||||
1 (bf.v1/import-files! cfg)
|
||||
3 (bf.v3/import-files! cfg))]
|
||||
|
||||
(db/update! pool :project
|
||||
{:modified-at (dt/now)}
|
||||
{:modified-at (ct/now)}
|
||||
{:id project-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
@@ -125,21 +120,35 @@
|
||||
[:name [:or [:string {:max 250}]
|
||||
[:map-of ::sm/uuid [:string {:max 250}]]]]
|
||||
[:project-id ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:version {:optional true} ::sm/int]
|
||||
[:file ::media/upload]])
|
||||
[:file media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::import-binfile
|
||||
"Import a penpot file in a binary format."
|
||||
"Import a penpot file in a binary format. If `file-id` is provided,
|
||||
an in-place import will be performed instead of creating a new file.
|
||||
|
||||
The in-place imports are only supported for binfile-v3 and when a
|
||||
.penpot file only contains one penpot file.
|
||||
"
|
||||
{::doc/added "1.15"
|
||||
::doc/changes ["1.20" "Add file-id param for in-place import"
|
||||
"1.20" "Set default version to 3"]
|
||||
|
||||
::webhooks/event? true
|
||||
::sse/stream? true
|
||||
::sm/params schema:import-binfile}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id version file] :as params}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id version file-id file] :as params}]
|
||||
(projects/check-edition-permissions! pool profile-id project-id)
|
||||
(let [version (or version 1)
|
||||
(let [version (or version 3)
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :version version))
|
||||
|
||||
cfg (cond-> cfg
|
||||
(uuid? file-id)
|
||||
(assoc ::bfc/file-id file-id))
|
||||
|
||||
manifest (case (int version)
|
||||
1 nil
|
||||
3 (bf.v3/get-manifest (:path file)))]
|
||||
@@ -147,5 +156,6 @@
|
||||
(with-meta
|
||||
(sse/response (partial import-binfile cfg params))
|
||||
{::audit/props {:file nil
|
||||
:file-id file-id
|
||||
:generated-by (:generated-by manifest)
|
||||
:referer (:referer manifest)}})))
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as uri]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -29,7 +30,6 @@
|
||||
[app.rpc.retry :as rtry]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -222,7 +222,7 @@
|
||||
|
||||
(defn upsert-comment-thread-status!
|
||||
([conn profile-id thread-id]
|
||||
(upsert-comment-thread-status! conn profile-id thread-id (dt/in-future "1s")))
|
||||
(upsert-comment-thread-status! conn profile-id thread-id (ct/in-future "1s")))
|
||||
([conn profile-id thread-id mod-at]
|
||||
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id mod-at mod-at])))
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
(ns app.rpc.commands.demo
|
||||
"A demo specific mutations."
|
||||
(:require
|
||||
[app.auth :refer [derive-password]]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]))
|
||||
|
||||
@@ -45,15 +45,13 @@
|
||||
params {:email email
|
||||
:fullname fullname
|
||||
:is-active true
|
||||
:deleted-at (dt/in-future (cf/get-deletion-delay))
|
||||
:password (profile/derive-password cfg password)
|
||||
:props {}}]
|
||||
|
||||
|
||||
(let [profile (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(->> (auth/create-profile! conn params)
|
||||
(auth/create-profile-rels! conn))))]
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
{::audit/profile-id (:id profile)}))))
|
||||
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
||||
:password (derive-password password)
|
||||
:props {}}
|
||||
profile (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(->> (auth/create-profile! conn params)
|
||||
(auth/create-profile-rels! conn))))]
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
{::audit/profile-id (:id profile)})))
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.desc-js-like :as-alias smdj]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uri :as uri]
|
||||
@@ -37,10 +38,8 @@
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- FEATURES
|
||||
|
||||
@@ -52,7 +51,7 @@
|
||||
;; --- HELPERS
|
||||
|
||||
(def long-cache-duration
|
||||
(dt/duration {:days 7}))
|
||||
(ct/duration {:days 7}))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [data changes features] :as row}]
|
||||
@@ -78,6 +77,7 @@
|
||||
|
||||
;; --- FILE PERMISSIONS
|
||||
|
||||
|
||||
(def ^:private sql:file-permissions
|
||||
"select fpr.is_owner,
|
||||
fpr.is_admin,
|
||||
@@ -187,15 +187,15 @@
|
||||
[:name [:string {:max 250}]]
|
||||
[:revn [::sm/int {:min 0}]]
|
||||
[:vern [::sm/int {:min 0}]]
|
||||
[:modified-at ::dt/instant]
|
||||
[:modified-at ::ct/inst]
|
||||
[:is-shared ::sm/boolean]
|
||||
[:project-id ::sm/uuid]
|
||||
[:created-at ::dt/instant]
|
||||
[:created-at ::ct/inst]
|
||||
[:data {:optional true} ::sm/any]])
|
||||
|
||||
(def schema:permissions-mixin
|
||||
[:map {:title "PermissionsMixin"}
|
||||
[:permissions ::perms/permissions]])
|
||||
[:permissions perms/schema:permissions]])
|
||||
|
||||
(def schema:file-with-permissions
|
||||
[:merge {:title "FileWithPermissions"}
|
||||
@@ -250,7 +250,7 @@
|
||||
(feat.fmigr/resolve-applied-migrations cfg file))))))
|
||||
|
||||
(defn get-file
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg} id
|
||||
[{:keys [::db/conn] :as cfg} id
|
||||
& {:keys [project-id
|
||||
migrate?
|
||||
include-deleted?
|
||||
@@ -272,13 +272,8 @@
|
||||
::db/remove-deleted (not include-deleted?)
|
||||
::sql/for-update lock-for-update?})
|
||||
(feat.fmigr/resolve-applied-migrations cfg)
|
||||
(feat.fdata/resolve-file-data cfg))
|
||||
|
||||
;; NOTE: we perform the file decoding in a separate thread
|
||||
;; because it has heavy and synchronous operations for
|
||||
;; decoding file body that are not very friendly with virtual
|
||||
;; threads.
|
||||
file (px/invoke! executor #(decode-row file))
|
||||
(feat.fdata/resolve-file-data cfg)
|
||||
(decode-row))
|
||||
|
||||
file (if (and migrate? (fmg/need-migration? file))
|
||||
(migrate-file cfg file options)
|
||||
@@ -304,7 +299,7 @@
|
||||
(defn get-file-etag
|
||||
[{:keys [::rpc/profile-id]} {:keys [modified-at revn vern permissions]}]
|
||||
(str profile-id "/" revn "/" vern "/" (hash fmg/available-migrations) "/"
|
||||
(dt/format-instant modified-at :iso)
|
||||
(ct/format-inst modified-at :iso)
|
||||
"/"
|
||||
(uri/map->query-string permissions)))
|
||||
|
||||
@@ -341,14 +336,24 @@
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
;; This operation is needed for backward comapatibility with frontends that
|
||||
;; does not support pointer-map resolution mechanism; this just resolves the
|
||||
;; pointers on backend and return a complete file.
|
||||
(if (and (contains? (:features file) "fdata/pointer-map")
|
||||
(not (contains? (:features params) "fdata/pointer-map")))
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(update file :data feat.fdata/process-pointers deref))
|
||||
file))))
|
||||
(as-> file file
|
||||
;; This operation is needed for backward comapatibility with
|
||||
;; frontends that does not support pointer-map resolution
|
||||
;; mechanism; this just resolves the pointers on backend and
|
||||
;; return a complete file
|
||||
(if (and (contains? (:features file) "fdata/pointer-map")
|
||||
(not (contains? (:features params) "fdata/pointer-map")))
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(update file :data feat.fdata/process-pointers deref))
|
||||
file)
|
||||
|
||||
;; This operation is needed for backward comapatibility with
|
||||
;; frontends that does not support objects-map mechanism; this
|
||||
;; just converts all objects map instaces to plain maps
|
||||
(if (and (contains? (:features file) "fdata/objects-map")
|
||||
(not (contains? (:features params) "fdata/objects-map")))
|
||||
(update file :data feat.fdata/process-objects (partial into {}))
|
||||
file)))))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-fragment (by id)
|
||||
|
||||
@@ -356,8 +361,8 @@
|
||||
[:map {:title "FileFragment"}
|
||||
[:id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:created-at ::dt/instant]
|
||||
[:content any?]])
|
||||
[:created-at ::ct/inst]
|
||||
[:content ::sm/any]])
|
||||
|
||||
(def schema:get-file-fragment
|
||||
[:map {:title "get-file-fragment"}
|
||||
@@ -460,8 +465,42 @@
|
||||
(:has-libraries row)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-library-usage
|
||||
|
||||
|
||||
(declare get-library-usage)
|
||||
|
||||
(def schema:get-library-usage
|
||||
[:map {:title "get-library-usage"}
|
||||
[:file-id ::sm/uuid]])
|
||||
:sample
|
||||
(sv/defmethod ::get-library-usage
|
||||
"Gets the number of files that use the specified library."
|
||||
{::doc/added "2.10.0"
|
||||
::sm/params schema:get-library-usage
|
||||
::sm/result ::sm/int}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(get-library-usage conn file-id)))
|
||||
|
||||
(def ^:private sql:get-library-usage
|
||||
"SELECT COUNT(*) AS used
|
||||
FROM file_library_rel AS flr
|
||||
JOIN file AS fl ON (flr.library_file_id = fl.id)
|
||||
WHERE flr.library_file_id = ?::uuid
|
||||
AND (fl.deleted_at IS NULL OR
|
||||
fl.deleted_at > now())")
|
||||
|
||||
(defn- get-library-usage
|
||||
[conn file-id]
|
||||
(let [row (db/exec-one! conn [sql:get-library-usage file-id])]
|
||||
{:used-in (:used row)}))
|
||||
|
||||
|
||||
;; --- QUERY COMMAND: get-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
|
||||
@@ -551,8 +590,34 @@
|
||||
|
||||
;; --- COMMAND QUERY: get-team-shared-files
|
||||
|
||||
(defn- components-and-variants
|
||||
"Return a set with all the variant-ids, and a list of components, but with
|
||||
only one component by variant"
|
||||
[components]
|
||||
(let [{:keys [variant-ids components]}
|
||||
(reduce (fn [{:keys [variant-ids components] :as acc} {:keys [variant-id] :as component}]
|
||||
(cond
|
||||
(nil? variant-id)
|
||||
{:variant-ids variant-ids :components (conj components component)}
|
||||
(contains? variant-ids variant-id)
|
||||
acc
|
||||
:else
|
||||
{:variant-ids (conj variant-ids variant-id) :components (conj components component)}))
|
||||
{:variant-ids #{} :components []}
|
||||
components)]
|
||||
{:components components
|
||||
:variant-ids variant-ids}))
|
||||
|
||||
;;coalesce(string_agg(flr.library_file_id::text, ','), '') as library_file_ids
|
||||
(def ^:private sql:team-shared-files
|
||||
"select f.id,
|
||||
"with file_library_agg as (
|
||||
select flr.file_id,
|
||||
coalesce(array_agg(flr.library_file_id) filter (where flr.library_file_id is not null), '{}') as library_file_ids
|
||||
from file_library_rel flr
|
||||
group by flr.file_id
|
||||
)
|
||||
|
||||
select f.id,
|
||||
f.revn,
|
||||
f.vern,
|
||||
f.data,
|
||||
@@ -565,10 +630,12 @@
|
||||
f.version,
|
||||
f.is_shared,
|
||||
ft.media_id,
|
||||
p.team_id
|
||||
p.team_id,
|
||||
fla.library_file_ids
|
||||
from file as f
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn and ft.deleted_at is null)
|
||||
left join file_library_agg as fla on fla.file_id = f.id
|
||||
where f.is_shared = true
|
||||
and f.deleted_at is null
|
||||
and p.deleted_at is null
|
||||
@@ -584,10 +651,13 @@
|
||||
:sample (into [] (take limit sorted-assets))}))]
|
||||
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(let [load-objects (fn [component]
|
||||
(ctf/load-component-objects data component))
|
||||
components-sample (-> (assets-sample (ctkl/components data) 4)
|
||||
(update :sample #(mapv load-objects %)))]
|
||||
(let [load-objects (fn [component]
|
||||
(ctf/load-component-objects data component))
|
||||
comps-and-variants (components-and-variants (ctkl/components-seq data))
|
||||
components (into {} (map (juxt :id identity) (:components comps-and-variants)))
|
||||
components-sample (-> (assets-sample components 4)
|
||||
(update :sample #(mapv load-objects %))
|
||||
(assoc :variants-count (-> comps-and-variants :variant-ids count)))]
|
||||
{:components components-sample
|
||||
:media (assets-sample (:media data) 3)
|
||||
:colors (assets-sample (:colors data) 3)
|
||||
@@ -609,6 +679,8 @@
|
||||
(dissoc :media-id)
|
||||
(assoc :thumbnail-id media-id))
|
||||
(dissoc row :media-id))))
|
||||
(map (fn [row]
|
||||
(update row :library-file-ids db/decode-pgarray #{})))
|
||||
(map #(assoc % :library-summary (get-library-summary cfg %)))
|
||||
(map #(dissoc % :data))))))
|
||||
|
||||
@@ -641,6 +713,7 @@
|
||||
|
||||
;; --- COMMAND QUERY: Files that use this File library
|
||||
|
||||
|
||||
(def ^:private sql:library-using-files
|
||||
"SELECT f.id,
|
||||
f.name
|
||||
@@ -713,6 +786,7 @@
|
||||
|
||||
;; --- COMMAND QUERY: get-file-summary
|
||||
|
||||
|
||||
(defn- get-file-summary
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id id project-id] :as params}]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
@@ -730,11 +804,13 @@
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
{:name (:name file)
|
||||
:components-count (count (ctkl/components-seq (:data file)))
|
||||
:graphics-count (count (get-in file [:data :media] []))
|
||||
:colors-count (count (get-in file [:data :colors] []))
|
||||
:typography-count (count (get-in file [:data :typographies] []))})))
|
||||
(let [components-and-variants (components-and-variants (ctkl/components-seq (:data file)))]
|
||||
{:name (:name file)
|
||||
:components-count (-> components-and-variants :components count)
|
||||
:variants-count (-> components-and-variants :variant-ids count)
|
||||
:graphics-count (count (get-in file [:data :media] []))
|
||||
:colors-count (count (get-in file [:data :colors] []))
|
||||
:typography-count (count (get-in file [:data :typographies] []))}))))
|
||||
|
||||
(sv/defmethod ::get-file-summary
|
||||
"Retrieve a file summary by its ID. Only authenticated users."
|
||||
@@ -746,6 +822,7 @@
|
||||
|
||||
;; --- COMMAND QUERY: get-file-info
|
||||
|
||||
|
||||
(defn- get-file-info
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
|
||||
(db/get* conn :file
|
||||
@@ -770,7 +847,7 @@
|
||||
[conn {:keys [id name]}]
|
||||
(db/update! conn :file
|
||||
{:name name
|
||||
:modified-at (dt/now)}
|
||||
:modified-at (ct/now)}
|
||||
{:id id}
|
||||
{::db/return-keys true}))
|
||||
|
||||
@@ -783,8 +860,8 @@
|
||||
[:id ::sm/uuid]
|
||||
[:project-id ::sm/uuid]
|
||||
[:name [:string {:max 250}]]
|
||||
[:created-at ::dt/instant]
|
||||
[:modified-at ::dt/instant]]
|
||||
[:created-at ::ct/inst]
|
||||
[:modified-at ::ct/inst]]
|
||||
|
||||
::sm/params
|
||||
[:map {:title "RenameFileParams"}
|
||||
@@ -795,8 +872,8 @@
|
||||
[:map {:title "SimplifiedFile"}
|
||||
[:id ::sm/uuid]
|
||||
[:name [:string {:max 250}]]
|
||||
[:created-at ::dt/instant]
|
||||
[:modified-at ::dt/instant]]
|
||||
[:created-at ::ct/inst]
|
||||
[:modified-at ::ct/inst]]
|
||||
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
@@ -839,7 +916,7 @@
|
||||
(db/update! cfg :file
|
||||
{:revn (inc (:revn file))
|
||||
:data (blob/encode (:data file))
|
||||
:modified-at (dt/now)
|
||||
:modified-at (ct/now)
|
||||
:has-media-trimmed false}
|
||||
{:id file-id})
|
||||
|
||||
@@ -900,7 +977,7 @@
|
||||
(db/delete! conn :file-library-rel {:library-file-id id})
|
||||
(db/update! conn :file
|
||||
{:is-shared false
|
||||
:modified-at (dt/now)}
|
||||
:modified-at (ct/now)}
|
||||
{:id id})
|
||||
(select-keys file [:id :name :is-shared]))
|
||||
|
||||
@@ -909,7 +986,7 @@
|
||||
(let [file (assoc file :is-shared true)]
|
||||
(db/update! conn :file
|
||||
{:is-shared true
|
||||
:modified-at (dt/now)}
|
||||
:modified-at (ct/now)}
|
||||
{:id id})
|
||||
file)
|
||||
|
||||
@@ -945,7 +1022,7 @@
|
||||
[conn team file-id]
|
||||
(let [delay (ldel/get-deletion-delay team)
|
||||
file (db/update! conn :file
|
||||
{:deleted-at (dt/in-future delay)}
|
||||
{:deleted-at (ct/in-future delay)}
|
||||
{:id file-id}
|
||||
{::db/return-keys [:id :name :is-shared :deleted-at
|
||||
:project-id :created-at :modified-at]})]
|
||||
@@ -1000,6 +1077,7 @@
|
||||
[:library-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::link-file-to-library
|
||||
"Link a file to a library. Returns the recursive list of libraries used by that library"
|
||||
{::doc/added "1.17"
|
||||
::webhooks/event? true
|
||||
::sm/params schema:link-file-to-library}
|
||||
@@ -1013,7 +1091,8 @@
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(link-file-to-library conn params))))
|
||||
(link-file-to-library conn params)
|
||||
(bfc/get-libraries cfg [library-id]))))
|
||||
|
||||
;; --- MUTATION COMMAND: unlink-file-from-library
|
||||
|
||||
@@ -1043,7 +1122,7 @@
|
||||
(defn update-sync
|
||||
[conn {:keys [file-id library-id] :as params}]
|
||||
(db/update! conn :file-library-rel
|
||||
{:synced-at (dt/now)}
|
||||
{:synced-at (ct/now)}
|
||||
{:file-id file-id
|
||||
:library-file-id library-id}
|
||||
{::db/return-keys true}))
|
||||
@@ -1068,14 +1147,14 @@
|
||||
[conn {:keys [file-id date] :as params}]
|
||||
(db/update! conn :file
|
||||
{:ignore-sync-until date
|
||||
:modified-at (dt/now)}
|
||||
:modified-at (ct/now)}
|
||||
{:id file-id}
|
||||
{::db/return-keys true}))
|
||||
|
||||
(def ^:private schema:ignore-file-library-sync-status
|
||||
[:map {:title "ignore-file-library-sync-status"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:date ::dt/instant]])
|
||||
[:date ::ct/inst]])
|
||||
|
||||
;; TODO: improve naming
|
||||
(sv/defmethod ::ignore-file-library-sync-status
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
(ns app.rpc.commands.files-create
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -23,13 +23,13 @@
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.set :as set]))
|
||||
|
||||
(defn create-file-role!
|
||||
[conn {:keys [file-id profile-id role]}]
|
||||
(let [params {:file-id file-id
|
||||
:profile-id profile-id}]
|
||||
|
||||
(->> (perms/assign-role-flags params role)
|
||||
(db/insert! conn :file-profile-rel))))
|
||||
|
||||
@@ -41,9 +41,7 @@
|
||||
:or {is-shared false revn 0 create-page true}
|
||||
:as params}]
|
||||
|
||||
(dm/assert!
|
||||
"expected a valid connection"
|
||||
(db/connection? conn))
|
||||
(assert (db/connection? conn) "expected a valid connection")
|
||||
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||
cfeat/*current* features]
|
||||
@@ -54,18 +52,18 @@
|
||||
:is-shared is-shared
|
||||
:features features
|
||||
:ignore-sync-until ignore-sync-until
|
||||
:modified-at modified-at
|
||||
:created-at modified-at
|
||||
:deleted-at deleted-at}
|
||||
{:create-page create-page
|
||||
:page-id page-id})
|
||||
file (-> (bfc/insert-file! cfg file)
|
||||
(bfc/decode-row))]
|
||||
:page-id page-id})]
|
||||
|
||||
(bfc/insert-file! cfg file)
|
||||
|
||||
(->> (assoc params :file-id (:id file) :role :owner)
|
||||
(create-file-role! conn))
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:modified-at (ct/now)}
|
||||
{:id project-id})
|
||||
|
||||
file)))
|
||||
@@ -114,14 +112,15 @@
|
||||
;; FIXME: IMPORTANT: this code can have race conditions, because
|
||||
;; we have no locks for updating team so, creating two files
|
||||
;; concurrently can lead to lost team features updating
|
||||
|
||||
(when-let [features (-> features
|
||||
(set/difference (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(not-empty))]
|
||||
(let [features (->> features
|
||||
(set/union (:features team))
|
||||
(db/create-array conn "text"))]
|
||||
(let [features (-> features
|
||||
(set/union (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(into-array))]
|
||||
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id (:id team)}
|
||||
|
||||
@@ -8,13 +8,16 @@
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.features.file-migrations :refer [reset-migrations!]]
|
||||
[app.main :as-alias main]
|
||||
[app.msgbus :as mbus]
|
||||
[app.rpc :as-alias rpc]
|
||||
@@ -24,12 +27,18 @@
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [migrations] :as row}]
|
||||
(when row
|
||||
(cond-> row
|
||||
(some? migrations)
|
||||
(assoc :migrations (db/decode-pgarray migrations)))))
|
||||
|
||||
(def sql:get-file-snapshots
|
||||
"WITH changes AS (
|
||||
SELECT id, label, revn, created_at, created_by, profile_id
|
||||
SELECT id, label, revn, created_at, created_by, profile_id, locked_by
|
||||
FROM file_change
|
||||
WHERE file_id = ?
|
||||
AND data IS NOT NULL
|
||||
@@ -60,8 +69,8 @@
|
||||
|
||||
(defn- generate-snapshot-label
|
||||
[]
|
||||
(let [ts (-> (dt/now)
|
||||
(dt/format-instant)
|
||||
(let [ts (-> (ct/now)
|
||||
(ct/format-inst)
|
||||
(str/replace #"[T:\.]" "-")
|
||||
(str/rtrim "Z"))]
|
||||
(str "snapshot-" ts)))
|
||||
@@ -74,18 +83,15 @@
|
||||
(assert (#{:system :user :admin} created-by)
|
||||
"expected valid keyword for created-by")
|
||||
|
||||
(let [conn
|
||||
(db/get-connection cfg)
|
||||
|
||||
created-by
|
||||
(let [created-by
|
||||
(name created-by)
|
||||
|
||||
deleted-at
|
||||
(cond
|
||||
(= deleted-at :default)
|
||||
(dt/plus (dt/now) (cf/get-deletion-delay))
|
||||
(ct/plus (ct/now) (cf/get-deletion-delay))
|
||||
|
||||
(dt/instant? deleted-at)
|
||||
(ct/inst? deleted-at)
|
||||
deleted-at
|
||||
|
||||
:else
|
||||
@@ -101,12 +107,15 @@
|
||||
(blob/encode (:data file))
|
||||
|
||||
features
|
||||
(db/encode-pgarray (:features file) conn "text")]
|
||||
(into-array (:features file))
|
||||
|
||||
(l/debug :hint "creating file snapshot"
|
||||
:file-id (str (:id file))
|
||||
:id (str snapshot-id)
|
||||
:label label)
|
||||
migrations
|
||||
(into-array (:migrations file))]
|
||||
|
||||
(l/dbg :hint "creating file snapshot"
|
||||
:file-id (str (:id file))
|
||||
:id (str snapshot-id)
|
||||
:label label)
|
||||
|
||||
(db/insert! cfg :file-change
|
||||
{:id snapshot-id
|
||||
@@ -114,6 +123,7 @@
|
||||
:data data
|
||||
:version (:version file)
|
||||
:features features
|
||||
:migrations migrations
|
||||
:profile-id profile-id
|
||||
:file-id (:id file)
|
||||
:label label
|
||||
@@ -159,7 +169,17 @@
|
||||
{:file-id file-id
|
||||
:id snapshot-id}
|
||||
{::db/for-share true})
|
||||
(feat.fdata/resolve-file-data cfg))]
|
||||
(feat.fdata/resolve-file-data cfg)
|
||||
(decode-row))
|
||||
|
||||
;; If snapshot has tracked applied migrations, we reuse them,
|
||||
;; if not we take a safest set of migrations as starting
|
||||
;; point. This is because, at the time of implementing
|
||||
;; snapshots, migrations were not taken into account so we
|
||||
;; need to make this backward compatible in some way.
|
||||
file (assoc file :migrations
|
||||
(or (:migrations snapshot)
|
||||
(fmg/generate-migrations-from-version 67)))]
|
||||
|
||||
(when-not snapshot
|
||||
(ex/raise :type :not-found
|
||||
@@ -180,12 +200,16 @@
|
||||
:label (:label snapshot)
|
||||
:snapshot-id (str (:id snapshot)))
|
||||
|
||||
;; If the file was already offloaded, on restring the snapshot
|
||||
;; we are going to replace the file data, so we need to touch
|
||||
;; the old referenced storage object and avoid possible leaks
|
||||
;; If the file was already offloaded, on restoring the snapshot we
|
||||
;; are going to replace the file data, so we need to touch the old
|
||||
;; referenced storage object and avoid possible leaks
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(sto/touch-object! storage (:data-ref-id file)))
|
||||
|
||||
;; In the same way, on reseting the file data, we need to restore
|
||||
;; the applied migrations on the moment of taking the snapshot
|
||||
(reset-migrations! conn file)
|
||||
|
||||
(db/update! conn :file
|
||||
{:data (:data snapshot)
|
||||
:revn (inc (:revn file))
|
||||
@@ -253,14 +277,14 @@
|
||||
:deleted-at nil}
|
||||
{:id snapshot-id}
|
||||
{::db/return-keys true})
|
||||
(dissoc :data :features)))
|
||||
(dissoc :data :features :migrations)))
|
||||
|
||||
(defn- get-snapshot
|
||||
"Get a minimal snapshot from database and lock for update"
|
||||
[conn id]
|
||||
(db/get conn :file-change
|
||||
{:id id}
|
||||
{::sql/columns [:id :file-id :created-by :deleted-at]
|
||||
{::sql/columns [:id :file-id :created-by :deleted-at :profile-id :locked-by]
|
||||
::db/for-update true}))
|
||||
|
||||
(sv/defmethod ::update-file-snapshot
|
||||
@@ -280,7 +304,7 @@
|
||||
(defn- delete-file-snapshot!
|
||||
[conn snapshot-id]
|
||||
(db/update! conn :file-change
|
||||
{:deleted-at (dt/now)}
|
||||
{:deleted-at (ct/now)}
|
||||
{:id snapshot-id}
|
||||
{::db/return-keys false})
|
||||
nil)
|
||||
@@ -300,4 +324,111 @@
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
|
||||
;; Check if version is locked by someone else
|
||||
(when (and (:locked-by snapshot)
|
||||
(not= (:locked-by snapshot) profile-id))
|
||||
(ex/raise :type :validation
|
||||
:code :snapshot-is-locked
|
||||
:hint "Cannot delete a locked version"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:locked-by (:locked-by snapshot)))
|
||||
|
||||
(delete-file-snapshot! conn id)))))
|
||||
|
||||
;;; Lock/unlock version endpoints
|
||||
|
||||
(def ^:private schema:lock-file-snapshot
|
||||
[:map {:title "lock-file-snapshot"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(defn- lock-file-snapshot!
|
||||
[conn snapshot-id profile-id]
|
||||
(db/update! conn :file-change
|
||||
{:locked-by profile-id}
|
||||
{:id snapshot-id}
|
||||
{::db/return-keys false})
|
||||
nil)
|
||||
|
||||
(sv/defmethod ::lock-file-snapshot
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:lock-file-snapshot}
|
||||
[cfg {:keys [::rpc/profile-id id]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(let [snapshot (get-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
|
||||
(when (not= (:created-by snapshot) "user")
|
||||
(ex/raise :type :validation
|
||||
:code :system-snapshots-cant-be-locked
|
||||
:hint "Only user-created versions can be locked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
|
||||
;; Only the creator can lock their own version
|
||||
(when (not= (:profile-id snapshot) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :only-creator-can-lock
|
||||
:hint "Only the version creator can lock it"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:creator-id (:profile-id snapshot)))
|
||||
|
||||
;; Check if already locked
|
||||
(when (:locked-by snapshot)
|
||||
(ex/raise :type :validation
|
||||
:code :snapshot-already-locked
|
||||
:hint "Version is already locked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:locked-by (:locked-by snapshot)))
|
||||
|
||||
(lock-file-snapshot! conn id profile-id)))))
|
||||
|
||||
(def ^:private schema:unlock-file-snapshot
|
||||
[:map {:title "unlock-file-snapshot"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(defn- unlock-file-snapshot!
|
||||
[conn snapshot-id]
|
||||
(db/update! conn :file-change
|
||||
{:locked-by nil}
|
||||
{:id snapshot-id}
|
||||
{::db/return-keys false})
|
||||
nil)
|
||||
|
||||
(sv/defmethod ::unlock-file-snapshot
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:unlock-file-snapshot}
|
||||
[cfg {:keys [::rpc/profile-id id]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(let [snapshot (get-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
|
||||
(when (not= (:created-by snapshot) "user")
|
||||
(ex/raise :type :validation
|
||||
:code :system-snapshots-cant-be-unlocked
|
||||
:hint "Only user-created versions can be unlocked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
|
||||
;; Only the creator can unlock their own version
|
||||
(when (not= (:profile-id snapshot) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :only-creator-can-unlock
|
||||
:hint "Only the version creator can unlock it"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:creator-id (:profile-id snapshot)))
|
||||
|
||||
;; Check if not locked
|
||||
(when (not (:locked-by snapshot))
|
||||
(ex/raise :type :validation
|
||||
:code :snapshot-not-locked
|
||||
:hint "Version is not locked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
|
||||
(unlock-file-snapshot! conn id)))))
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.changes :as cpc]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -27,7 +28,6 @@
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.set :as set]))
|
||||
|
||||
;; --- MUTATION COMMAND: create-temp-file
|
||||
@@ -72,17 +72,16 @@
|
||||
params
|
||||
(-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :deleted-at (dt/in-future {:days 1}))
|
||||
(assoc :deleted-at (ct/in-future {:days 1}))
|
||||
(assoc :features features))]
|
||||
|
||||
(files.create/create-file cfg params)))
|
||||
|
||||
;; --- MUTATION COMMAND: update-temp-file
|
||||
|
||||
|
||||
(def ^:private schema:update-temp-file
|
||||
[:map {:title "update-temp-file"}
|
||||
[:changes [:vector ::cpc/change]]
|
||||
[:changes [:vector cpc/schema:change]]
|
||||
[:revn [::sm/int {:min 0}]]
|
||||
[:session-id ::sm/uuid]
|
||||
[:id ::sm/uuid]])
|
||||
@@ -97,7 +96,7 @@
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at (dt/now)
|
||||
:created-at (ct/now)
|
||||
:file-id id
|
||||
:revn revn
|
||||
:data nil
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.thumbnails :as thc]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -30,13 +31,12 @@
|
||||
[app.storage :as sto]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- FEATURES
|
||||
|
||||
(def long-cache-duration
|
||||
(dt/duration {:days 7}))
|
||||
(ct/duration {:days 7}))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-object-thumbnails
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
[:map {:title "PartialFile"}
|
||||
[:id ::sm/uuid]
|
||||
[:revn {:min 0} ::sm/int]
|
||||
[:page :any]])
|
||||
[:page [:map-of :keyword ::sm/any]]])
|
||||
|
||||
(sv/defmethod ::get-file-data-for-thumbnail
|
||||
"Retrieves the data for generate the thumbnail of the file. Used
|
||||
@@ -247,7 +247,7 @@
|
||||
(defn- create-file-object-thumbnail!
|
||||
[{:keys [::sto/storage] :as cfg} file object-id media tag]
|
||||
(let [file-id (:id file)
|
||||
timestamp (dt/now)
|
||||
timestamp (ct/now)
|
||||
media (persist-thumbnail! storage media timestamp)
|
||||
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag])
|
||||
@@ -271,7 +271,7 @@
|
||||
[:map {:title "create-file-object-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:object-id [:string {:max 250}]]
|
||||
[:media ::media/upload]
|
||||
[:media media/schema:upload]
|
||||
[:tag {:optional true} [:string {:max 50}]]])
|
||||
|
||||
(sv/defmethod ::create-file-object-thumbnail
|
||||
@@ -302,7 +302,7 @@
|
||||
{::sql/for-update true})]
|
||||
(sto/touch-object! storage media-id)
|
||||
(db/update! conn :file-tagged-object-thumbnail
|
||||
{:deleted-at (dt/now)}
|
||||
{:deleted-at (ct/now)}
|
||||
{:file-id file-id
|
||||
:object-id object-id
|
||||
:tag tag})))
|
||||
@@ -338,7 +338,7 @@
|
||||
hash (sto/calculate-hash path)
|
||||
data (-> (sto/content path)
|
||||
(sto/wrap-with-hash hash))
|
||||
tnow (dt/now)
|
||||
tnow (ct/now)
|
||||
media (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/deduplicate? true
|
||||
@@ -381,7 +381,7 @@
|
||||
[:map {:title "create-file-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:revn ::sm/int]
|
||||
[:media ::media/upload]])
|
||||
[:media media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::create-file-thumbnail
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.common.files.validate :as val]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -36,10 +37,7 @@
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.set :as set]
|
||||
[promesa.exec :as px]))
|
||||
[clojure.set :as set]))
|
||||
|
||||
(declare ^:private get-lagged-changes)
|
||||
(declare ^:private send-notifications!)
|
||||
@@ -64,10 +62,10 @@
|
||||
[:revn {:min 0} ::sm/int]
|
||||
[:vern {:min 0} ::sm/int]
|
||||
[:features {:optional true} ::cfeat/features]
|
||||
[:changes {:optional true} [:vector ::cpc/change]]
|
||||
[:changes {:optional true} [:vector cpc/schema:change]]
|
||||
[:changes-with-metadata {:optional true}
|
||||
[:vector [:map
|
||||
[:changes [:vector ::cpc/change]]
|
||||
[:changes [:vector cpc/schema:change]]
|
||||
[:hint-origin {:optional true} :keyword]
|
||||
[:hint-events {:optional true} [:vector [:string {:max 250}]]]]]]
|
||||
[:skip-validate {:optional true} ::sm/boolean]])
|
||||
@@ -76,7 +74,7 @@
|
||||
schema:update-file-result
|
||||
[:vector {:title "update-file-result"}
|
||||
[:map
|
||||
[:changes [:vector ::cpc/change]]
|
||||
[:changes [:vector cpc/schema:change]]
|
||||
[:file-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:revn {:min 0} ::sm/int]
|
||||
@@ -123,7 +121,7 @@
|
||||
[:update-file/global]]
|
||||
|
||||
::webhooks/event? true
|
||||
::webhooks/batch-timeout (dt/duration "2m")
|
||||
::webhooks/batch-timeout (ct/duration "2m")
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
|
||||
::sm/params schema:update-file
|
||||
@@ -156,10 +154,9 @@
|
||||
(assoc :file file)
|
||||
(assoc :changes changes))
|
||||
|
||||
cfg (assoc cfg ::timestamp (dt/now))
|
||||
|
||||
tpoint (dt/tpoint)]
|
||||
cfg (assoc cfg ::timestamp (ct/now))
|
||||
|
||||
tpoint (ct/tpoint)]
|
||||
|
||||
(when (not= (:vern params)
|
||||
(:vern file))
|
||||
@@ -183,15 +180,15 @@
|
||||
(set/difference (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(not-empty))]
|
||||
(let [features (->> features
|
||||
(set/union (:features team))
|
||||
(db/create-array conn "text"))]
|
||||
(let [features (-> features
|
||||
(set/union (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(into-array))]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id (:id team)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
(binding [l/*context* (some-> (meta params)
|
||||
@@ -199,7 +196,7 @@
|
||||
(errors/request->context))]
|
||||
(-> (update-file* cfg params)
|
||||
(rph/with-defer #(let [elapsed (tpoint)]
|
||||
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))))
|
||||
(l/trace :hint "update-file" :time (ct/format-duration elapsed))))))))))
|
||||
|
||||
(defn- update-file*
|
||||
"Internal function, part of the update-file process, that encapsulates
|
||||
@@ -209,7 +206,7 @@
|
||||
Follow the inner implementation to `update-file-data!` function.
|
||||
|
||||
Only intended for internal use on this module."
|
||||
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
|
||||
[{:keys [::db/conn ::timestamp] :as cfg}
|
||||
{:keys [profile-id file team features changes session-id skip-validate] :as params}]
|
||||
|
||||
(let [;; Retrieve the file data
|
||||
@@ -222,15 +219,11 @@
|
||||
|
||||
;; We create a new lexycal scope for clearly delimit the result of
|
||||
;; executing this update file operation and all its side effects
|
||||
(let [file (px/invoke! executor
|
||||
(fn []
|
||||
;; Process the file data on separated thread for avoid to do
|
||||
;; the CPU intensive operation on vthread.
|
||||
(binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(update-file-data! cfg file
|
||||
process-changes-and-validate
|
||||
changes skip-validate))))]
|
||||
(let [file (binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(update-file-data! cfg file
|
||||
process-changes-and-validate
|
||||
changes skip-validate))]
|
||||
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(persist-file! cfg file)
|
||||
@@ -244,8 +237,8 @@
|
||||
:created-at timestamp
|
||||
:updated-at timestamp
|
||||
:deleted-at (if (::snapshot-data file)
|
||||
(dt/plus timestamp (ldel/get-deletion-delay team))
|
||||
(dt/plus timestamp (dt/duration {:hours 1})))
|
||||
(ct/plus timestamp (ldel/get-deletion-delay team))
|
||||
(ct/plus timestamp (ct/duration {:hours 1})))
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:version (:version file)
|
||||
@@ -306,7 +299,7 @@
|
||||
[{:keys [::db/conn ::timestamp]} file]
|
||||
(let [;; The timestamp can be nil because this function is also
|
||||
;; intended to be used outside of this module
|
||||
modified-at (or timestamp (dt/now))]
|
||||
modified-at (or timestamp (ct/now))]
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at modified-at}
|
||||
@@ -360,7 +353,7 @@
|
||||
;; TODO: reuse operations if file is migrated
|
||||
;; TODO: move encoding to a separated thread
|
||||
file (if (take-snapshot? file)
|
||||
(let [tpoint (dt/tpoint)
|
||||
(let [tpoint (ct/tpoint)
|
||||
snapshot (-> (:data file)
|
||||
(feat.fdata/process-pointers deref)
|
||||
(feat.fdata/process-objects (partial into {}))
|
||||
@@ -372,7 +365,7 @@
|
||||
:file-id (str (:id file))
|
||||
:revn (:revn file)
|
||||
:label label
|
||||
:elapsed (dt/format-duration elapsed))
|
||||
:elapsed (ct/format-duration elapsed))
|
||||
|
||||
(-> file
|
||||
(assoc ::snapshot-data snapshot)
|
||||
@@ -408,7 +401,6 @@
|
||||
(not skip-validate))
|
||||
(bfc/get-resolved-file-libraries cfg file))
|
||||
|
||||
|
||||
;; The main purpose of this atom is provide a contextual state
|
||||
;; for the changes subsystem where optionally some hints can
|
||||
;; be provided for the changes processing. Right now we are
|
||||
@@ -452,11 +444,11 @@
|
||||
(when (contains? cf/flags :auto-file-snapshot)
|
||||
(let [freq (or (cf/get :auto-file-snapshot-every) 20)
|
||||
timeout (or (cf/get :auto-file-snapshot-timeout)
|
||||
(dt/duration {:hours 1}))]
|
||||
(ct/duration {:hours 1}))]
|
||||
|
||||
(or (= 1 freq)
|
||||
(zero? (mod revn freq))
|
||||
(> (inst-ms (dt/diff modified-at (dt/now)))
|
||||
(> (inst-ms (ct/diff modified-at (ct/now)))
|
||||
(inst-ms timeout))))))
|
||||
|
||||
(def ^:private sql:lagged-changes
|
||||
@@ -496,5 +488,5 @@
|
||||
:file-id (:id file)
|
||||
:session-id session-id
|
||||
:revn (:revn file)
|
||||
:modified-at (dt/now)
|
||||
:modified-at (ct/now)
|
||||
:changes lchanges}))))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
@@ -25,10 +26,7 @@
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[promesa.exec :as px]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
||||
(def valid-style #{"normal" "italic"})
|
||||
@@ -37,14 +35,13 @@
|
||||
|
||||
(def ^:private
|
||||
schema:get-font-variants
|
||||
[:schema {:title "get-font-variants"}
|
||||
[:and
|
||||
[:map
|
||||
[:team-id {:optional true} ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:project-id {:optional true} ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]]
|
||||
[::sm/contains-any #{:team-id :file-id :project-id}]]])
|
||||
[:and
|
||||
[:map {:title "get-font-variants"}
|
||||
[:team-id {:optional true} ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:project-id {:optional true} ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]]
|
||||
[::sm/contains-any #{:team-id :file-id :project-id}]])
|
||||
|
||||
(sv/defmethod ::get-font-variants
|
||||
{::doc/added "1.18"
|
||||
@@ -106,7 +103,7 @@
|
||||
(create-font-variant cfg (assoc params :profile-id profile-id)))))
|
||||
|
||||
(defn create-font-variant
|
||||
[{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}]
|
||||
[{:keys [::sto/storage ::db/conn]} {:keys [data] :as params}]
|
||||
(letfn [(generate-missing! [data]
|
||||
(let [data (media/run {:cmd :generate-fonts :input data})]
|
||||
(when (and (not (contains? data "font/otf"))
|
||||
@@ -124,7 +121,7 @@
|
||||
content (-> (sto/content resource)
|
||||
(sto/wrap-with-hash hash))]
|
||||
{::sto/content content
|
||||
::sto/touched-at (dt/now)
|
||||
::sto/touched-at (ct/now)
|
||||
::sto/deduplicate? true
|
||||
:content-type mtype
|
||||
:bucket "team-font-variant"})))
|
||||
@@ -158,7 +155,7 @@
|
||||
:otf-file-id (:id otf)
|
||||
:ttf-file-id (:id ttf)}))]
|
||||
|
||||
(let [data (px/invoke! executor (partial generate-missing! data))
|
||||
(let [data (generate-missing! data)
|
||||
assets (persist-fonts-files! data)
|
||||
result (insert-font-variant! assets)]
|
||||
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
|
||||
@@ -217,7 +214,7 @@
|
||||
{::sql/for-update true})
|
||||
|
||||
delay (ldel/get-deletion-delay team)
|
||||
tnow (dt/in-future delay)]
|
||||
tnow (ct/in-future delay)]
|
||||
|
||||
(teams/check-edition-permissions! (:permissions team))
|
||||
|
||||
@@ -261,7 +258,7 @@
|
||||
|
||||
(teams/check-edition-permissions! (:permissions team))
|
||||
(db/update! conn :team-font-variant
|
||||
{:deleted-at (dt/in-future delay)}
|
||||
{:deleted-at (ct/in-future delay)}
|
||||
{:id (:id variant)}
|
||||
{::db/return-keys false})
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -27,10 +28,7 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.templates :as tmpl]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[promesa.exec :as px]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
;; --- COMMAND: Duplicate File
|
||||
|
||||
@@ -104,7 +102,7 @@
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
|
||||
(binding [bfc/*state* (volatile! {:index {file-id (uuid/next)}})]
|
||||
(duplicate-file (assoc cfg ::bfc/timestamp (dt/now))
|
||||
(duplicate-file (assoc cfg ::bfc/timestamp (ct/now))
|
||||
(-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :reset-shared-flag true)))))))
|
||||
@@ -164,7 +162,7 @@
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
;; Defer all constraints
|
||||
(db/exec-one! cfg ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
(-> (assoc cfg ::bfc/timestamp (dt/now))
|
||||
(-> (assoc cfg ::bfc/timestamp (ct/now))
|
||||
(duplicate-project (assoc params :profile-id profile-id))))))
|
||||
|
||||
(defn duplicate-team
|
||||
@@ -313,15 +311,14 @@
|
||||
|
||||
;; Update the modification date of the all affected projects
|
||||
;; ensuring that the destination project is the most recent one.
|
||||
(doseq [project-id (into (list project-id) source)]
|
||||
|
||||
;; NOTE: as this is executed on virtual thread, sleeping does
|
||||
;; not causes major issues, and allows an easy way to set a
|
||||
;; trully different modification date to each file.
|
||||
(px/sleep 10)
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id}))
|
||||
(loop [project-ids (into (list project-id) source)
|
||||
modified-at (ct/now)]
|
||||
(when-let [project-id (first project-ids)]
|
||||
(db/update! conn :project
|
||||
{:modified-at modified-at}
|
||||
{:id project-id})
|
||||
(recur (rest project-ids)
|
||||
(ct/plus modified-at 10))))
|
||||
|
||||
nil))
|
||||
|
||||
@@ -396,12 +393,7 @@
|
||||
;; --- COMMAND: Clone Template
|
||||
|
||||
(defn clone-template
|
||||
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [project-id profile-id] :as params} template]
|
||||
|
||||
;; NOTE: the importation process performs some operations
|
||||
;; that are not very friendly with virtual threads, and for
|
||||
;; avoid unexpected blocking of other concurrent operations
|
||||
;; we dispatch that operation to a dedicated executor.
|
||||
[{:keys [::db/pool] :as cfg} {:keys [project-id profile-id] :as params} template]
|
||||
(let [template (tmp/tempfile-from template
|
||||
:prefix "penpot.template."
|
||||
:suffix ""
|
||||
@@ -419,13 +411,13 @@
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)))
|
||||
|
||||
result (if (= format :binfile-v3)
|
||||
(px/invoke! executor (partial bf.v3/import-files! cfg))
|
||||
(px/invoke! executor (partial bf.v1/import-files! cfg)))]
|
||||
(bf.v3/import-files! cfg)
|
||||
(bf.v1/import-files! cfg))]
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(db/update! conn :project
|
||||
{:modified-at (dt/now)}
|
||||
{:modified-at (ct/now)}
|
||||
{:id project-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -23,11 +24,8 @@
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]
|
||||
[promesa.exec :as px]))
|
||||
[datoteka.io :as io]))
|
||||
|
||||
(def default-max-file-size
|
||||
(* 1024 1024 10)) ; 10 MiB
|
||||
@@ -48,7 +46,7 @@
|
||||
[:file-id ::sm/uuid]
|
||||
[:is-local ::sm/boolean]
|
||||
[:name [:string {:max 250}]]
|
||||
[:content ::media/upload]])
|
||||
[:content media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::upload-file-media-object
|
||||
{::doc/added "1.17"
|
||||
@@ -67,7 +65,7 @@
|
||||
mobj (create-file-media-object cfg params)]
|
||||
|
||||
(db/update! conn :file
|
||||
{:modified-at (dt/now)
|
||||
{:modified-at (ct/now)
|
||||
:has-media-trimmed false}
|
||||
{:id file-id}
|
||||
{::db/return-keys false})
|
||||
@@ -153,9 +151,9 @@
|
||||
(assoc ::image (process-main-image info)))))
|
||||
|
||||
(defn- create-file-media-object
|
||||
[{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
|
||||
[{:keys [::sto/storage ::db/conn] :as cfg}
|
||||
{:keys [id file-id is-local name content]}]
|
||||
(let [result (px/invoke! executor (partial process-image content))
|
||||
(let [result (process-image content)
|
||||
image (sto/put-object! storage (::image result))
|
||||
thumb (when-let [params (::thumb result)]
|
||||
(sto/put-object! storage params))]
|
||||
@@ -192,7 +190,7 @@
|
||||
mobj (create-file-media-object-from-url cfg (assoc params :profile-id profile-id))]
|
||||
|
||||
(db/update! pool :file
|
||||
{:modified-at (dt/now)
|
||||
{:modified-at (ct/now)
|
||||
:has-media-trimmed false}
|
||||
{:id file-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.plugins :refer [schema:plugin-registry]]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -28,18 +29,14 @@
|
||||
[app.storage :as sto]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(declare check-profile-existence!)
|
||||
(declare decode-row)
|
||||
(declare derive-password)
|
||||
(declare filter-props)
|
||||
(declare get-profile)
|
||||
(declare strip-private-attrs)
|
||||
(declare verify-password)
|
||||
|
||||
(def schema:props-notifications
|
||||
[:map {:title "props-notifications"}
|
||||
@@ -70,8 +67,8 @@
|
||||
[:is-blocked {:optional true} ::sm/boolean]
|
||||
[:is-demo {:optional true} ::sm/boolean]
|
||||
[:is-muted {:optional true} ::sm/boolean]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:created-at {:optional true} ::ct/inst]
|
||||
[:modified-at {:optional true} ::ct/inst]
|
||||
[:default-project-id {:optional true} ::sm/uuid]
|
||||
[:default-team-id {:optional true} ::sm/uuid]
|
||||
[:props {:optional true} schema:props]])
|
||||
@@ -131,9 +128,7 @@
|
||||
;; 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 ::sql/for-update true)
|
||||
(decode-row))
|
||||
|
||||
(let [profile (get-profile conn profile-id ::db/for-update true)
|
||||
;; Update the profile map with direct params
|
||||
profile (-> profile
|
||||
(assoc :fullname fullname)
|
||||
@@ -143,9 +138,9 @@
|
||||
(db/update! conn :profile
|
||||
{:fullname fullname
|
||||
:lang lang
|
||||
:theme theme
|
||||
:props (db/tjson (:props profile))}
|
||||
{:id profile-id})
|
||||
:theme theme}
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(-> profile
|
||||
(strip-private-attrs)
|
||||
@@ -194,7 +189,7 @@
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
|
||||
(let [profile (db/get-by-id conn :profile profile-id ::sql/for-update true)]
|
||||
(when (and (not= (:password profile) "!")
|
||||
(not (:valid (verify-password cfg old-password (:password profile)))))
|
||||
(not (:valid (auth/verify-password old-password (:password profile)))))
|
||||
(ex/raise :type :validation
|
||||
:code :old-password-not-match))
|
||||
profile))
|
||||
@@ -203,7 +198,7 @@
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}]
|
||||
(when-not (db/read-only? conn)
|
||||
(db/update! conn :profile
|
||||
{:password (derive-password cfg password)}
|
||||
{:password (auth/derive-password password)}
|
||||
{:id id})
|
||||
nil))
|
||||
|
||||
@@ -228,21 +223,22 @@
|
||||
|
||||
(defn- update-notifications!
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id dashboard-comments email-comments email-invites]}]
|
||||
(let [profile (get-profile conn profile-id)
|
||||
(let [profile
|
||||
(get-profile conn profile-id ::db/for-update true)
|
||||
|
||||
notifications
|
||||
{:dashboard-comments dashboard-comments
|
||||
:email-comments email-comments
|
||||
:email-invites email-invites}]
|
||||
:email-invites email-invites}
|
||||
|
||||
(db/update!
|
||||
conn :profile
|
||||
{:props
|
||||
(-> (:props profile)
|
||||
(assoc :notifications notifications)
|
||||
(db/tjson))}
|
||||
{:id (:id profile)})
|
||||
props
|
||||
(-> (get profile :props)
|
||||
(assoc :notifications notifications))]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
nil))
|
||||
|
||||
;; --- MUTATION: Update Photo
|
||||
@@ -253,7 +249,7 @@
|
||||
(def ^:private
|
||||
schema:update-profile-photo
|
||||
[:map {:title "update-profile-photo"}
|
||||
[:file ::media/upload]])
|
||||
[:file media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::update-profile-photo
|
||||
{:doc/added "1.1"
|
||||
@@ -304,12 +300,11 @@
|
||||
:content-type (:mtype thumb)}))
|
||||
|
||||
(defn upload-photo
|
||||
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file] :as params}]
|
||||
[{:keys [::sto/storage] :as cfg} {:keys [file] :as params}]
|
||||
(let [params (-> cfg
|
||||
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
|
||||
[:process-image/global]])
|
||||
(assoc ::climit/label "upload-photo")
|
||||
(assoc ::climit/executor executor)
|
||||
(climit/invoke! generate-thumbnail! file))]
|
||||
(sto/put-object! storage params)))
|
||||
|
||||
@@ -352,13 +347,13 @@
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile email] :as params}]
|
||||
(let [token (tokens/generate (::setup/props cfg)
|
||||
{:iss :change-email
|
||||
:exp (dt/in-future "15m")
|
||||
:exp (ct/in-future "15m")
|
||||
:profile-id (:id profile)
|
||||
:email email})
|
||||
ptoken (tokens/generate (::setup/props cfg)
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 30})})]
|
||||
:exp (ct/in-future {:days 30})})]
|
||||
|
||||
(when (not= email (:email profile))
|
||||
(check-profile-existence! conn params))
|
||||
@@ -411,7 +406,7 @@
|
||||
|
||||
(defn update-profile-props
|
||||
[{:keys [::db/conn] :as cfg} profile-id props]
|
||||
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
||||
(let [profile (get-profile conn profile-id ::db/for-update true)
|
||||
props (reduce-kv (fn [props k v]
|
||||
;; We don't accept namespaced keys
|
||||
(if (simple-ident? k)
|
||||
@@ -424,16 +419,17 @@
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id})
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(filter-props props)))
|
||||
|
||||
(sv/defmethod ::update-profile-props
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile-props}
|
||||
::sm/params schema:update-profile-props
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id props]}]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(update-profile-props cfg profile-id props))))
|
||||
(update-profile-props cfg profile-id props))
|
||||
|
||||
;; --- MUTATION: Delete Profile
|
||||
|
||||
@@ -444,7 +440,7 @@
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(let [teams (get-owned-teams conn profile-id)
|
||||
deleted-at (dt/now)]
|
||||
deleted-at (ct/now)]
|
||||
|
||||
;; If we found owned teams with participants, we don't allow
|
||||
;; delete profile until the user properly transfer ownership or
|
||||
@@ -471,6 +467,26 @@
|
||||
(-> (rph/wrap nil)
|
||||
(rph/with-transform (session/delete-fn cfg)))))
|
||||
|
||||
(def sql:get-subscription-editors
|
||||
"SELECT DISTINCT
|
||||
p.id,
|
||||
p.fullname AS name,
|
||||
p.email AS email
|
||||
FROM team_profile_rel AS tpr1
|
||||
JOIN team_profile_rel AS tpr2
|
||||
ON (tpr1.team_id = tpr2.team_id)
|
||||
JOIN profile AS p
|
||||
ON (tpr2.profile_id = p.id)
|
||||
WHERE tpr1.profile_id = ?
|
||||
AND tpr1.is_owner IS true
|
||||
AND tpr2.can_edit IS true")
|
||||
|
||||
(sv/defmethod ::get-subscription-usage
|
||||
{::doc/added "2.9"}
|
||||
[cfg {:keys [::rpc/profile-id]}]
|
||||
(let [editors (db/exec! cfg [sql:get-subscription-editors profile-id])]
|
||||
{:editors editors}))
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(def sql:owned-teams
|
||||
@@ -528,15 +544,6 @@
|
||||
[props]
|
||||
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
|
||||
|
||||
(defn derive-password
|
||||
[{:keys [::wrk/executor]} password]
|
||||
(when password
|
||||
(px/invoke! executor (partial auth/derive-password password))))
|
||||
|
||||
(defn verify-password
|
||||
[{:keys [::wrk/executor]} password password-data]
|
||||
(px/invoke! executor (partial auth/verify-password password password-data)))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [props] :as row}]
|
||||
(cond-> row
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.logical-deletion :as ldel]
|
||||
@@ -21,7 +22,6 @@
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]))
|
||||
|
||||
;; --- Check Project Permissions
|
||||
@@ -218,7 +218,7 @@
|
||||
(sv/defmethod ::update-project-pin
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:update-project-pin
|
||||
::webhooks/batch-timeout (dt/duration "5s")
|
||||
::webhooks/batch-timeout (ct/duration "5s")
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
@@ -257,7 +257,7 @@
|
||||
[conn team project-id]
|
||||
(let [delay (ldel/get-deletion-delay team)
|
||||
project (db/update! conn :project
|
||||
{:deleted-at (dt/in-future delay)}
|
||||
{:deleted-at (ct/in-future delay)}
|
||||
{:id project-id}
|
||||
{::db/return-keys true})]
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.team :as tt]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.team :as types.team]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -30,7 +31,6 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.set :as set]))
|
||||
|
||||
@@ -78,9 +78,10 @@
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [features subscription] :as row}]
|
||||
(cond-> row
|
||||
(some? features) (assoc :features (db/decode-pgarray features #{}))
|
||||
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription))))
|
||||
(when row
|
||||
(cond-> row
|
||||
(some? features) (assoc :features (db/decode-pgarray features #{}))
|
||||
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription)))))
|
||||
|
||||
;; FIXME: move
|
||||
|
||||
@@ -461,11 +462,12 @@
|
||||
|
||||
;; --- COMMAND QUERY: get-team-info
|
||||
|
||||
(defn- get-team-info
|
||||
(defn get-team-info
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
|
||||
(db/get* conn :team
|
||||
{:id id}
|
||||
{::sql/columns [:id :is-default]}))
|
||||
(-> (db/get* conn :team
|
||||
{:id id}
|
||||
{::sql/columns [:id :is-default :features]})
|
||||
(decode-row)))
|
||||
|
||||
(sv/defmethod ::get-team-info
|
||||
"Retrieve minimal team info by its ID."
|
||||
@@ -501,7 +503,7 @@
|
||||
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
(set/difference cfeat/no-team-inheritable-features))
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))
|
||||
@@ -627,7 +629,7 @@
|
||||
|
||||
;; assign owner role to new profile
|
||||
(db/update! conn :team-profile-rel
|
||||
(get tt/permissions-for-role :owner)
|
||||
(get types.team/permissions-for-role :owner)
|
||||
{:team-id id :profile-id reassign-to}))
|
||||
|
||||
;; and finally, if all other conditions does not match and the
|
||||
@@ -664,7 +666,7 @@
|
||||
|
||||
(let [delay (ldel/get-deletion-delay team)
|
||||
team (db/update! conn :team
|
||||
{:deleted-at (dt/in-future delay)}
|
||||
{:deleted-at (ct/in-future delay)}
|
||||
{:id id}
|
||||
{::db/return-keys true})]
|
||||
|
||||
@@ -740,7 +742,7 @@
|
||||
:team-id team-id
|
||||
:role role})
|
||||
|
||||
(let [params (get tt/permissions-for-role role)]
|
||||
(let [params (get types.team/permissions-for-role role)]
|
||||
;; Only allow single owner on team
|
||||
(when (= role :owner)
|
||||
(db/update! conn :team-profile-rel
|
||||
@@ -758,7 +760,7 @@
|
||||
[:map {:title "update-team-member-role"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:member-id ::sm/uuid]
|
||||
[:role ::tt/role]])
|
||||
[:role types.team/schema:role]])
|
||||
|
||||
(sv/defmethod ::update-team-member-role
|
||||
{::doc/added "1.17"
|
||||
@@ -808,7 +810,7 @@
|
||||
(def ^:private schema:update-team-photo
|
||||
[:map {:title "update-team-photo"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:file ::media/upload]])
|
||||
[:file media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::update-team-photo
|
||||
{::doc/added "1.17"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.team :as types.team]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -29,7 +30,6 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- Mutation: Create Team Invitation
|
||||
@@ -62,7 +62,7 @@
|
||||
(tokens/generate (::setup/props cfg)
|
||||
{:iss :profile-identity
|
||||
:profile-id profile-id
|
||||
:exp (dt/in-future {:days 30})}))
|
||||
:exp (ct/in-future {:days 30})}))
|
||||
|
||||
(def ^:private schema:create-invitation
|
||||
[:map {:title "params:create-invitation"}
|
||||
@@ -75,7 +75,7 @@
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:fullname :string]]]
|
||||
[:role ::types.team/role]
|
||||
[:role types.team/schema:role]
|
||||
[:email ::sm/email]])
|
||||
|
||||
(def ^:private check-create-invitation-params
|
||||
@@ -126,7 +126,7 @@
|
||||
(teams/check-email-spam conn email true)
|
||||
|
||||
(let [id (uuid/next)
|
||||
expire (dt/in-future "168h") ;; 7 days
|
||||
expire (ct/in-future "168h") ;; 7 days
|
||||
invitation (db/exec-one! conn [sql:upsert-team-invitation id
|
||||
(:id team) (str/lower email)
|
||||
(:id profile)
|
||||
@@ -224,62 +224,112 @@
|
||||
(def ^:private xf:map-email (map :email))
|
||||
|
||||
(defn- create-team-invitations
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
|
||||
(let [emails (set emails)
|
||||
"Unified function to handle both create and resend team invitations.
|
||||
Accepts either:
|
||||
- emails (set) + role (single role for all emails)
|
||||
- invitations (vector of {:email :role} maps)"
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails invitations] :as params}]
|
||||
(let [;; Normalize input to a consistent format: [{:email :role}]
|
||||
invitation-data (cond
|
||||
;; Case 1: emails + single role (create invitations style)
|
||||
(and emails role)
|
||||
(map (fn [email] {:email email :role role}) emails)
|
||||
|
||||
join-requests (->> (get-valid-access-request-profiles conn (:id team))
|
||||
(d/index-by :email))
|
||||
;; Case 2: invitations with individual roles (resend invitations style)
|
||||
(some? invitations)
|
||||
invitations
|
||||
|
||||
team-members (into #{} xf:map-email
|
||||
(teams/get-team-members conn (:id team)))
|
||||
:else
|
||||
(throw (ex-info "Invalid parameters: must provide either emails+role or invitations" {})))
|
||||
|
||||
invitations (into #{}
|
||||
(comp
|
||||
;; We don't re-send inviation to
|
||||
;; already existing members
|
||||
(remove team-members)
|
||||
invitation-emails (into #{} (map :email) invitation-data)
|
||||
|
||||
join-requests (->> (get-valid-access-request-profiles conn (:id team))
|
||||
(d/index-by :email))
|
||||
|
||||
team-members (into #{} xf:map-email
|
||||
(teams/get-team-members conn (:id team)))
|
||||
|
||||
invitations (into #{}
|
||||
(comp
|
||||
;; We don't re-send invitations to
|
||||
;; already existing members
|
||||
(remove #(contains? team-members (:email %)))
|
||||
;; We don't send invitations to
|
||||
;; join-requested members
|
||||
(remove join-requests)
|
||||
(map (fn [email] (assoc params :email email)))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
(remove #(contains? join-requests (:email %)))
|
||||
(map (fn [{:keys [email role]}]
|
||||
(create-invitation cfg
|
||||
(-> params
|
||||
(assoc :email email)
|
||||
(assoc :role role)))))
|
||||
(remove nil?))
|
||||
invitation-data)]
|
||||
|
||||
;; For requested invitations, do not send invitation emails, add
|
||||
;; the user directly to the team
|
||||
(->> join-requests
|
||||
(filter #(contains? emails (key %)))
|
||||
(map val)
|
||||
(run! (partial add-member-to-team conn profile team role)))
|
||||
(filter #(contains? invitation-emails (key %)))
|
||||
(map (fn [[email member]]
|
||||
(let [role (:role (first (filter #(= (:email %) email) invitation-data)))]
|
||||
(add-member-to-team conn profile team role member))))
|
||||
(doall))
|
||||
|
||||
invitations))
|
||||
|
||||
(def ^:private schema:create-team-invitations
|
||||
[:map {:title "create-team-invitations"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:role ::types.team/role]
|
||||
[:emails [::sm/set ::sm/email]]])
|
||||
[:and
|
||||
[:map {:title "create-team-invitations"}
|
||||
[:team-id ::sm/uuid]
|
||||
;; Support both formats:
|
||||
;; 1. emails (set) + role (single role for all)
|
||||
;; 2. invitations (vector of {:email :role} maps)
|
||||
[:emails {:optional true} [::sm/set ::sm/email]]
|
||||
[:role {:optional true} types.team/schema:role]
|
||||
[:invitations {:optional true} [:vector [:map
|
||||
[:email ::sm/email]
|
||||
[:role types.team/schema:role]]]]]
|
||||
|
||||
;; Ensure exactly one format is provided
|
||||
[:fn (fn [params]
|
||||
(let [has-emails-role (and (contains? params :emails)
|
||||
(contains? params :role))
|
||||
has-invitations (contains? params :invitations)]
|
||||
(and (or has-emails-role has-invitations)
|
||||
(not (and has-emails-role has-invitations)))))]])
|
||||
|
||||
(def ^:private max-invitations-by-request-threshold
|
||||
"The number of invitations can be sent in a single rpc request"
|
||||
25)
|
||||
|
||||
(sv/defmethod ::create-team-invitations
|
||||
"A rpc call that allow to send a single or multiple invitations to
|
||||
join the team."
|
||||
"A rpc call that allows to send single or multiple invitations to join the team.
|
||||
|
||||
Supports two parameter formats:
|
||||
1. emails (set) + role (single role for all emails)
|
||||
2. invitations (vector of {:email :role} maps for individual roles)"
|
||||
{::doc/added "1.17"
|
||||
::doc/module :teams
|
||||
::sm/params schema:create-team-invitations}
|
||||
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
|
||||
[cfg {:keys [::rpc/profile-id team-id role emails] :as params}]
|
||||
(let [perms (teams/get-permissions cfg profile-id team-id)
|
||||
profile (db/get-by-id cfg :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
;; Determine which format is being used
|
||||
using-emails-format? (and emails role)
|
||||
;; Handle both parameter formats
|
||||
emails (if using-emails-format?
|
||||
(into #{} (map profile/clean-email) emails)
|
||||
#{})
|
||||
;; Calculate total invitation count for both formats
|
||||
invitation-count (if using-emails-format?
|
||||
(count emails)
|
||||
(count (:invitations params)))]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(when (> invitation-count max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
@@ -288,7 +338,7 @@
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id team-id)
|
||||
(assoc ::quotes/incr (count emails))
|
||||
(assoc ::quotes/incr invitation-count)
|
||||
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
|
||||
{::quotes/id ::quotes/profiles-per-team}))
|
||||
|
||||
@@ -304,7 +354,12 @@
|
||||
(-> params
|
||||
(assoc :profile profile)
|
||||
(assoc :team team)
|
||||
(assoc :emails emails)))]
|
||||
;; Pass parameters in the correct format for the unified function
|
||||
(cond-> using-emails-format?
|
||||
;; If using emails+role format, ensure both are present
|
||||
(assoc :emails emails :role role)
|
||||
;; If using invitations format, the :invitations key is already in params
|
||||
(not using-emails-format?) identity)))]
|
||||
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
@@ -318,7 +373,7 @@
|
||||
[:features {:optional true} ::cfeat/features]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:emails [::sm/set ::sm/email]]
|
||||
[:role ::types.team/role]])
|
||||
[:role types.team/schema:role]])
|
||||
|
||||
(sv/defmethod ::create-team-with-invitations
|
||||
{::doc/added "1.17"
|
||||
@@ -403,7 +458,7 @@
|
||||
[:map {:title "update-team-invitation-role"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:email ::sm/email]
|
||||
[:role ::types.team/role]])
|
||||
[:role types.team/schema:role]])
|
||||
|
||||
(sv/defmethod ::update-team-invitation-role
|
||||
{::doc/added "1.17"
|
||||
@@ -418,7 +473,7 @@
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(db/update! conn :team-invitation
|
||||
{:role (name role) :updated-at (dt/now)}
|
||||
{:role (name role) :updated-at (ct/now)}
|
||||
{:team-id team-id :email-to (profile/clean-email email)})
|
||||
|
||||
nil))
|
||||
@@ -471,7 +526,7 @@
|
||||
(when-let [request (db/get* conn :team-access-request
|
||||
{:team-id team-id
|
||||
:requester-id profile-id})]
|
||||
(when (dt/is-after? (:valid-until request) (dt/now))
|
||||
(when (ct/is-after? (:valid-until request) (ct/now))
|
||||
(ex/raise :type :validation
|
||||
:code :request-already-sent
|
||||
:hint "you have already made a request to join this team less than 24 hours ago"))))
|
||||
@@ -487,8 +542,8 @@
|
||||
"Create or update team access request for provided team and profile-id"
|
||||
[conn team-id requester-id]
|
||||
(check-existing-team-access-request conn team-id requester-id)
|
||||
(let [valid-until (dt/in-future {:hours 24})
|
||||
auto-join-until (dt/in-future {:days 7})
|
||||
(let [valid-until (ct/in-future {:hours 24})
|
||||
auto-join-until (ct/in-future {:days 7})
|
||||
request-id (uuid/next)]
|
||||
(db/exec-one! conn [sql:upsert-team-access-request
|
||||
request-id team-id requester-id
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.team :as types.team]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -23,8 +24,7 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(defmulti process-token (fn [_ _ claims] (:iss claims)))
|
||||
|
||||
@@ -126,9 +126,9 @@
|
||||
(def schema:team-invitation-claims
|
||||
[:map {:title "TeamInvitationClaims"}
|
||||
[:iss :keyword]
|
||||
[:exp ::dt/instant]
|
||||
[:exp ::ct/inst]
|
||||
[:profile-id ::sm/uuid]
|
||||
[:role ::types.team/role]
|
||||
[:role types.team/schema:role]
|
||||
[:team-id ::sm/uuid]
|
||||
[:member-email ::sm/email]
|
||||
[:member-id {:optional true} ::sm/uuid]])
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
@@ -19,7 +20,6 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn get-webhooks-permissions
|
||||
@@ -54,7 +54,7 @@
|
||||
(http/req! cfg
|
||||
{:method :head
|
||||
:uri (str (:uri params))
|
||||
:timeout (dt/duration "3s")}
|
||||
:timeout (ct/duration "3s")}
|
||||
{:sync? true}))]
|
||||
(if (ex/exception? response)
|
||||
(if-let [hint (webhooks/interpret-exception response)]
|
||||
|
||||
@@ -166,9 +166,6 @@
|
||||
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
|
||||
;; :description "penpot backend"
|
||||
}]
|
||||
:security
|
||||
{:api_key []}
|
||||
|
||||
:paths paths
|
||||
:components {:schemas @definitions}}))
|
||||
|
||||
|
||||
@@ -10,15 +10,14 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]))
|
||||
|
||||
(sm/register!
|
||||
^{::sm/type ::permissions}
|
||||
[:map {:title "Permissions"}
|
||||
[:type {:gen/elements [:membership :share-link]} :keyword]
|
||||
[:is-owner ::sm/boolean]
|
||||
[:is-admin ::sm/boolean]
|
||||
[:can-edit ::sm/boolean]
|
||||
[:can-read ::sm/boolean]
|
||||
[:is-logged ::sm/boolean]])
|
||||
(def schema:permissions
|
||||
[:map {:title "Permissions"}
|
||||
[:type {:gen/elements [:membership :share-link]} :keyword]
|
||||
[:is-owner ::sm/boolean]
|
||||
[:is-admin ::sm/boolean]
|
||||
[:can-edit ::sm/boolean]
|
||||
[:can-read ::sm/boolean]
|
||||
[:is-logged ::sm/boolean]])
|
||||
|
||||
(def valid-roles
|
||||
#{:admin :owner :editor :viewer})
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"- Total: ~(::total params) (INCR ~(::incr params 1))\n")]
|
||||
(wrk/submit! {::db/conn conn
|
||||
::wrk/task :sendmail
|
||||
::wrk/delay (dt/duration "30s")
|
||||
::wrk/delay (ct/duration "30s")
|
||||
::wrk/max-retries 4
|
||||
::wrk/priority 200
|
||||
::wrk/dedupe true
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as uri]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -58,7 +59,6 @@
|
||||
[app.rpc.rlimit.result :as-alias lresult]
|
||||
[app.util.inet :as inet]
|
||||
[app.util.services :as-alias sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.edn :as edn]
|
||||
[cuerdas.core :as str]
|
||||
@@ -67,7 +67,7 @@
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def ^:private default-timeout
|
||||
(dt/duration 400))
|
||||
(ct/duration 400))
|
||||
|
||||
(def ^:private default-options
|
||||
{:codec rds/string-codec
|
||||
@@ -94,6 +94,10 @@
|
||||
(defmulti parse-limit (fn [[_ strategy _]] strategy))
|
||||
(defmulti process-limit (fn [_ _ _ o] (::strategy o)))
|
||||
|
||||
(defn- ->seconds
|
||||
[d]
|
||||
(-> d inst-ms (/ 1000) int))
|
||||
|
||||
(sm/register!
|
||||
{:type ::rpc/rlimit
|
||||
:pred #(instance? clojure.lang.Agent %)})
|
||||
@@ -115,7 +119,7 @@
|
||||
[:map
|
||||
[::capacity ::sm/int]
|
||||
[::rate ::sm/int]
|
||||
[::internal ::dt/duration]
|
||||
[::internal ::ct/duration]
|
||||
[::params [::sm/vec :any]]]
|
||||
[:map
|
||||
[::nreq ::sm/int]
|
||||
@@ -157,7 +161,7 @@
|
||||
(assert (valid-limit-tuple? vlimit) "expected valid limit tuple")
|
||||
|
||||
(if-let [[_ capacity rate interval] (re-find bucket-opts-re opts)]
|
||||
(let [interval (dt/duration interval)
|
||||
(let [interval (ct/duration interval)
|
||||
rate (parse-long rate)
|
||||
capacity (parse-long capacity)]
|
||||
{::name name
|
||||
@@ -166,7 +170,7 @@
|
||||
::rate rate
|
||||
::interval interval
|
||||
::opts opts
|
||||
::params [(dt/->seconds interval) rate capacity]
|
||||
::params [(->seconds interval) rate capacity]
|
||||
::key (str "ratelimit.bucket." (d/name name))})
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-bucket-limit-opts
|
||||
@@ -176,7 +180,7 @@
|
||||
[redis user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
|
||||
(let [script (-> bucket-rate-limit-script
|
||||
(assoc ::rscript/keys [(str key "." service "." user-id)])
|
||||
(assoc ::rscript/vals (conj params (dt/->seconds now))))
|
||||
(assoc ::rscript/vals (conj params (->seconds now))))
|
||||
result (rds/eval redis script)
|
||||
allowed? (boolean (nth result 0))
|
||||
remaining (nth result 1)
|
||||
@@ -191,16 +195,16 @@
|
||||
:remaining remaining)
|
||||
(-> limit
|
||||
(assoc ::lresult/allowed allowed?)
|
||||
(assoc ::lresult/reset (dt/plus now reset))
|
||||
(assoc ::lresult/reset (ct/plus now reset))
|
||||
(assoc ::lresult/remaining remaining))))
|
||||
|
||||
(defmethod process-limit :window
|
||||
[redis user-id now {:keys [::nreq ::unit ::key ::service] :as limit}]
|
||||
(let [ts (dt/truncate now unit)
|
||||
ttl (dt/diff now (dt/plus ts {unit 1}))
|
||||
(let [ts (ct/truncate now unit)
|
||||
ttl (ct/diff now (ct/plus ts {unit 1}))
|
||||
script (-> window-rate-limit-script
|
||||
(assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))])
|
||||
(assoc ::rscript/vals [nreq (dt/->seconds ttl)]))
|
||||
(assoc ::rscript/keys [(str key "." service "." user-id "." (ct/format-inst ts))])
|
||||
(assoc ::rscript/vals [nreq (->seconds ttl)]))
|
||||
result (rds/eval redis script)
|
||||
allowed? (boolean (nth result 0))
|
||||
remaining (nth result 1)]
|
||||
@@ -214,7 +218,7 @@
|
||||
(-> limit
|
||||
(assoc ::lresult/allowed allowed?)
|
||||
(assoc ::lresult/remaining remaining)
|
||||
(assoc ::lresult/reset (dt/plus ts {unit 1})))))
|
||||
(assoc ::lresult/reset (ct/plus ts {unit 1})))))
|
||||
|
||||
(defn- process-limits!
|
||||
[redis user-id limits now]
|
||||
@@ -223,7 +227,7 @@
|
||||
(d/index-by ::name ::lresult/remaining)
|
||||
(uri/map->query-string))
|
||||
reset (->> results
|
||||
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
|
||||
(d/index-by ::name (comp ->seconds ::lresult/reset))
|
||||
(uri/map->query-string))
|
||||
|
||||
rejected (d/seek (complement ::lresult/allowed) results)]
|
||||
@@ -261,7 +265,7 @@
|
||||
(let [redis (rds/get-or-connect redis ::rpc/rlimit default-options)
|
||||
uid (get-uid params)
|
||||
;; FIXME: why not clasic try/catch?
|
||||
result (ex/try! (process-limits! redis uid limits (dt/now)))]
|
||||
result (ex/try! (process-limits! redis uid limits (ct/now)))]
|
||||
|
||||
(l/trc :hint "process-limits"
|
||||
:service sname
|
||||
@@ -321,7 +325,7 @@
|
||||
(sm/check-fn schema:config))
|
||||
|
||||
(def ^:private check-refresh
|
||||
(sm/check-fn ::dt/duration))
|
||||
(sm/check-fn ::ct/duration))
|
||||
|
||||
(def ^:private check-limits
|
||||
(sm/check-fn schema:limits))
|
||||
@@ -351,7 +355,7 @@
|
||||
config)))]
|
||||
|
||||
(when-let [config (some->> path slurp edn/read-string check-config)]
|
||||
(let [refresh (->> config meta :refresh dt/duration check-refresh)
|
||||
(let [refresh (->> config meta :refresh ct/duration check-refresh)
|
||||
limits (->> config compile-pass-1 compile-pass-2 check-limits)]
|
||||
|
||||
{::refresh refresh
|
||||
@@ -410,7 +414,7 @@
|
||||
(l/info :hint "initializing rlimit config reader" :path (str path))
|
||||
|
||||
;; Initialize the state with initial refresh value
|
||||
(send-via executor state (constantly {::refresh (dt/duration "5s")}))
|
||||
(send-via executor state (constantly {::refresh (ct/duration "5s")}))
|
||||
|
||||
;; Force a refresh
|
||||
(refresh-config (assoc cfg ::path path ::state state)))
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.srepl.cli :as cli]
|
||||
[app.srepl.main]
|
||||
[app.util.locks :as locks]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core :as c]
|
||||
[clojure.core.server :as ccs]
|
||||
[clojure.main :as cm]
|
||||
@@ -77,7 +77,7 @@
|
||||
(loop []
|
||||
(when (try
|
||||
(let [data (read-line)
|
||||
tpoint (dt/tpoint)]
|
||||
tpoint (ct/tpoint)]
|
||||
|
||||
(l/dbg :hint "received" :data (if (= data ::eof) "EOF" data))
|
||||
|
||||
|
||||
@@ -7,16 +7,17 @@
|
||||
(ns app.srepl.cli
|
||||
"PREPL API for external usage (CLI or ADMIN)"
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.auth :refer [derive-password]]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.auth :as cmd.auth]
|
||||
[app.rpc.commands.profile :as cmd.profile]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn coercer
|
||||
@@ -53,7 +54,7 @@
|
||||
(some-> (get-current-system)
|
||||
(db/tx-run!
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [password (cmd.profile/derive-password system password)
|
||||
(let [password (derive-password password)
|
||||
params {:id (uuid/next)
|
||||
:email email
|
||||
:fullname fullname
|
||||
@@ -73,7 +74,7 @@
|
||||
(assoc :fullname fullname)
|
||||
|
||||
(some? password)
|
||||
(assoc :password (auth/derive-password password))
|
||||
(assoc :password (derive-password password))
|
||||
|
||||
(some? is-active)
|
||||
(assoc :is-active is-active))]
|
||||
@@ -101,7 +102,7 @@
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(let [res (if soft
|
||||
(db/update! conn :profile
|
||||
{:deleted-at (dt/now)}
|
||||
{:deleted-at (ct/now)}
|
||||
{:email email :deleted-at nil})
|
||||
(db/delete! conn :profile
|
||||
{:email email}))]
|
||||
@@ -123,7 +124,7 @@
|
||||
|
||||
(defmethod exec-command "derive-password"
|
||||
[{:keys [password]}]
|
||||
(auth/derive-password password))
|
||||
(derive-password password))
|
||||
|
||||
(defmethod exec-command "authenticate"
|
||||
[{:keys [token]}]
|
||||
@@ -173,6 +174,21 @@
|
||||
:num-editors (get-customer-slots system id)
|
||||
:subscription (get props :subscription)})))
|
||||
|
||||
(def ^:private schema:timestamp
|
||||
(sm/type-schema
|
||||
{:type ::timestamp
|
||||
:pred ct/inst?
|
||||
:type-properties
|
||||
{:title "inst"
|
||||
:description "The same as :app.common.time/inst but encodes to epoch"
|
||||
:error/message "should be an instant"
|
||||
:gen/gen (->> (sg/small-int)
|
||||
(sg/fmap (fn [v] (ct/inst v))))
|
||||
:decode/string ct/inst
|
||||
:encode/string inst-ms
|
||||
:decode/json ct/inst
|
||||
:encode/json inst-ms}}))
|
||||
|
||||
(def ^:private schema:customer-subscription
|
||||
[:map {:title "CustomerSubscription"}
|
||||
[:id ::sm/text]
|
||||
@@ -186,7 +202,7 @@
|
||||
"canceled"
|
||||
"incomplete"
|
||||
"incomplete_expired"
|
||||
"pass_due"
|
||||
"past_due"
|
||||
"paused"
|
||||
"trialing"
|
||||
"unpaid"]]
|
||||
@@ -198,16 +214,15 @@
|
||||
"year"]]
|
||||
[:quantity :int]
|
||||
[:description [:maybe ::sm/text]]
|
||||
[:created-at ::sm/timestamp]
|
||||
[:start-date [:maybe ::sm/timestamp]]
|
||||
[:ended-at [:maybe ::sm/timestamp]]
|
||||
[:trial-end [:maybe ::sm/timestamp]]
|
||||
[:trial-start [:maybe ::sm/timestamp]]
|
||||
[:cancel-at [:maybe ::sm/timestamp]]
|
||||
[:canceled-at [:maybe ::sm/timestamp]]
|
||||
|
||||
[:current-period-end ::sm/timestamp]
|
||||
[:current-period-start ::sm/timestamp]
|
||||
[:created-at schema:timestamp]
|
||||
[:start-date [:maybe schema:timestamp]]
|
||||
[:ended-at [:maybe schema:timestamp]]
|
||||
[:trial-end [:maybe schema:timestamp]]
|
||||
[:trial-start [:maybe schema:timestamp]]
|
||||
[:cancel-at [:maybe schema:timestamp]]
|
||||
[:canceled-at [:maybe schema:timestamp]]
|
||||
[:current-period-end [:maybe schema:timestamp]]
|
||||
[:current-period-start [:maybe schema:timestamp]]
|
||||
[:cancel-at-period-end :boolean]
|
||||
|
||||
[:cancellation-details
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.files.validate :as cfv]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.main :as main]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.files-snapshot :as fsnap]
|
||||
[app.util.time :as dt]))
|
||||
[app.rpc.commands.files-snapshot :as fsnap]))
|
||||
|
||||
(def ^:dynamic *system* nil)
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
(when (string? label)
|
||||
(fsnap/create-file-snapshot! system file
|
||||
{:label label
|
||||
:deleted-at (dt/in-future {:days 30})
|
||||
:deleted-at (ct/in-future {:days 30})
|
||||
:created-by :admin}))
|
||||
|
||||
(let [file' (update file' :revn inc)]
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
[app.common.pprint :as p]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -38,8 +39,8 @@
|
||||
[app.srepl.helpers :as h]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.datafy :refer [datafy]]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.pprint :refer [print-table]]
|
||||
[clojure.stacktrace :as strace]
|
||||
@@ -476,7 +477,7 @@
|
||||
:max-jobs max-jobs
|
||||
:max-items max-items)
|
||||
|
||||
(let [tpoint (dt/tpoint)
|
||||
(let [tpoint (ct/tpoint)
|
||||
factory (px/thread-factory :virtual false :prefix "penpot/file-process/")
|
||||
executor (px/cached-executor :factory factory)
|
||||
sjobs (ps/create :permits max-jobs)
|
||||
@@ -506,7 +507,7 @@
|
||||
(Thread/sleep (int pause)))
|
||||
|
||||
(ps/release! sjobs)
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(let [elapsed (ct/format-duration (tpoint))]
|
||||
(l/trc :hint "process:file:end"
|
||||
:tid thread-id
|
||||
:file-id (str file-id)
|
||||
@@ -516,7 +517,7 @@
|
||||
process-file*
|
||||
(fn [idx file-id]
|
||||
(ps/acquire! sjobs)
|
||||
(px/run! executor (partial process-file file-id idx (dt/tpoint)))
|
||||
(px/run! executor (partial process-file file-id idx (ct/tpoint)))
|
||||
(inc idx))
|
||||
|
||||
process-files
|
||||
@@ -542,7 +543,7 @@
|
||||
(l/dbg :hint "process:error" :cause cause))
|
||||
|
||||
(finally
|
||||
(let [elapsed (dt/format-duration (tpoint))]
|
||||
(let [elapsed (ct/format-duration (tpoint))]
|
||||
(l/dbg :hint "process:end"
|
||||
:rollback rollback?
|
||||
:elapsed elapsed))))))
|
||||
@@ -556,7 +557,7 @@
|
||||
"Mark a project for deletion"
|
||||
[file-id]
|
||||
(let [file-id (h/parse-uuid file-id)
|
||||
tnow (dt/now)]
|
||||
tnow (ct/now)]
|
||||
|
||||
(audit/insert! main/system
|
||||
{::audit/name "delete-file"
|
||||
@@ -618,7 +619,7 @@
|
||||
::audit/props file
|
||||
::audit/context {:triggered-by "srepl"
|
||||
:cause "explicit call to restore-file!"}
|
||||
::audit/tracked-at (dt/now)})
|
||||
::audit/tracked-at (ct/now)})
|
||||
|
||||
(restore-file* system file-id))))))
|
||||
|
||||
@@ -626,7 +627,7 @@
|
||||
"Mark a project for deletion"
|
||||
[project-id]
|
||||
(let [project-id (h/parse-uuid project-id)
|
||||
tnow (dt/now)]
|
||||
tnow (ct/now)]
|
||||
|
||||
(audit/insert! main/system
|
||||
{::audit/name "delete-project"
|
||||
@@ -673,7 +674,7 @@
|
||||
::audit/props project
|
||||
::audit/context {:triggered-by "srepl"
|
||||
:cause "explicit call to restore-team!"}
|
||||
::audit/tracked-at (dt/now)})
|
||||
::audit/tracked-at (ct/now)})
|
||||
|
||||
(restore-project* system project-id))))))
|
||||
|
||||
@@ -681,7 +682,7 @@
|
||||
"Mark a team for deletion"
|
||||
[team-id]
|
||||
(let [team-id (h/parse-uuid team-id)
|
||||
tnow (dt/now)]
|
||||
tnow (ct/now)]
|
||||
|
||||
(audit/insert! main/system
|
||||
{::audit/name "delete-team"
|
||||
@@ -733,7 +734,7 @@
|
||||
::audit/props team
|
||||
::audit/context {:triggered-by "srepl"
|
||||
:cause "explicit call to restore-team!"}
|
||||
::audit/tracked-at (dt/now)})
|
||||
::audit/tracked-at (ct/now)})
|
||||
|
||||
(restore-team* system team-id))))))
|
||||
|
||||
@@ -741,7 +742,7 @@
|
||||
"Mark a profile for deletion."
|
||||
[profile-id]
|
||||
(let [profile-id (h/parse-uuid profile-id)
|
||||
tnow (dt/now)]
|
||||
tnow (ct/now)]
|
||||
|
||||
(audit/insert! main/system
|
||||
{::audit/name "delete-profile"
|
||||
@@ -775,7 +776,7 @@
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/context {:triggered-by "srepl"
|
||||
:cause "explicit call to restore-profile!"}
|
||||
::audit/tracked-at (dt/now)})
|
||||
::audit/tracked-at (ct/now)})
|
||||
|
||||
(db/update! system :profile
|
||||
{:deleted-at nil}
|
||||
@@ -821,7 +822,7 @@
|
||||
{:deleted deleted :total total})))]
|
||||
|
||||
(let [path (fs/path path)
|
||||
deleted-at (dt/minus (dt/now) (cf/get-deletion-delay))]
|
||||
deleted-at (ct/minus (ct/now) (cf/get-deletion-delay))]
|
||||
|
||||
(when-not (fs/exists? path)
|
||||
(throw (ex-info "path does not exists" {:path path})))
|
||||
@@ -905,7 +906,7 @@
|
||||
(db/tx-run! main/system
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
(let [team (-> (assoc cfg ::bfc/timestamp (dt/now))
|
||||
(let [team (-> (assoc cfg ::bfc/timestamp (ct/now))
|
||||
(mgmt/duplicate-team :team-id team-id :name name))
|
||||
rels (db/query conn :team-profile-rel {:team-id team-id})]
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.storage.fs :as sfs]
|
||||
[app.storage.impl :as impl]
|
||||
[app.storage.s3 :as ss3]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig])
|
||||
@@ -27,7 +27,9 @@
|
||||
|
||||
(defn get-legacy-backend
|
||||
[]
|
||||
(let [name (cf/get :assets-storage-backend)]
|
||||
(when-let [name (cf/get :assets-storage-backend)]
|
||||
(l/wrn :hint "using deprecated configuration, please read 2.11 release notes"
|
||||
:href "https://github.com/penpot/penpot/releases/tag/2.11.0")
|
||||
(case name
|
||||
:assets-fs :fs
|
||||
:assets-s3 :s3
|
||||
@@ -122,7 +124,7 @@
|
||||
(dissoc :id))
|
||||
|
||||
touched-at (if touch
|
||||
(or touched-at (dt/now))
|
||||
(or touched-at (ct/now))
|
||||
touched-at)
|
||||
|
||||
;; NOTE: for now we don't reuse the deleted objects, but in
|
||||
@@ -224,7 +226,7 @@
|
||||
(assert (valid-storage? storage))
|
||||
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)]
|
||||
(-> (db/update! connectable :storage-object
|
||||
{:touched-at (dt/now)}
|
||||
{:touched-at (ct/now)}
|
||||
{:id id})
|
||||
(db/get-update-count)
|
||||
(pos?))))
|
||||
@@ -235,7 +237,7 @@
|
||||
[storage object]
|
||||
(assert (valid-storage? storage))
|
||||
(when (or (nil? (:expired-at object))
|
||||
(dt/is-after? (:expired-at object) (dt/now)))
|
||||
(ct/is-after? (:expired-at object) (ct/now)))
|
||||
(-> (impl/resolve-backend storage (:backend object))
|
||||
(impl/get-object-data object))))
|
||||
|
||||
@@ -244,7 +246,7 @@
|
||||
[storage object]
|
||||
(assert (valid-storage? storage))
|
||||
(when (or (nil? (:expired-at object))
|
||||
(dt/is-after? (:expired-at object) (dt/now)))
|
||||
(ct/is-after? (:expired-at object) (ct/now)))
|
||||
(-> (impl/resolve-backend storage (:backend object))
|
||||
(impl/get-object-bytes object))))
|
||||
|
||||
@@ -254,7 +256,7 @@
|
||||
([storage object options]
|
||||
(assert (valid-storage? storage))
|
||||
(when (or (nil? (:expired-at object))
|
||||
(dt/is-after? (:expired-at object) (dt/now)))
|
||||
(ct/is-after? (:expired-at object) (ct/now)))
|
||||
(-> (impl/resolve-backend storage (:backend object))
|
||||
(impl/get-object-url object options)))))
|
||||
|
||||
@@ -266,7 +268,7 @@
|
||||
(let [backend (impl/resolve-backend storage (:backend object))]
|
||||
(when (and (= :fs (::type backend))
|
||||
(or (nil? (:expired-at object))
|
||||
(dt/is-after? (:expired-at object) (dt/now))))
|
||||
(ct/is-after? (:expired-at object) (ct/now))))
|
||||
(-> (impl/get-object-url backend object nil) file-url->path))))
|
||||
|
||||
(defn del-object!
|
||||
@@ -274,7 +276,7 @@
|
||||
(assert (valid-storage? storage))
|
||||
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
|
||||
res (db/update! connectable :storage-object
|
||||
{:deleted-at (dt/now)}
|
||||
{:deleted-at (ct/now)}
|
||||
{:id id})]
|
||||
(pos? (db/get-update-count res))))
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.storage.impl :as impl]
|
||||
[app.util.time :as dt]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private sql:lock-sobjects
|
||||
@@ -106,18 +106,18 @@
|
||||
|
||||
(defmethod ig/expand-key ::handler
|
||||
[k v]
|
||||
{k (assoc v ::min-age (dt/duration {:hours 2}))})
|
||||
{k (assoc v ::min-age (ct/duration {:hours 2}))})
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [::min-age] :as cfg}]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [min-age (dt/duration (or (:min-age props) min-age))]
|
||||
(let [min-age (ct/duration (or (:min-age props) min-age))]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(let [cfg (assoc cfg ::min-age min-age)
|
||||
total (clean-deleted! cfg)]
|
||||
|
||||
(l/inf :hint "task finished"
|
||||
:min-age (dt/format-duration min-age)
|
||||
:min-age (ct/format-duration min-age)
|
||||
:total total)
|
||||
|
||||
{:deleted total}))))))
|
||||
|
||||
@@ -212,8 +212,8 @@
|
||||
deleted 0]
|
||||
(if-let [chunk (get-chunk pool)]
|
||||
(let [[nfo ndo] (db/tx-run! cfg process-chunk! chunk)]
|
||||
(recur (+ freezed nfo)
|
||||
(+ deleted ndo)))
|
||||
(recur (long (+ freezed nfo))
|
||||
(long (+ deleted ndo))))
|
||||
(do
|
||||
(l/inf :hint "task finished"
|
||||
:to-freeze freezed
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.storage :as-alias sto]
|
||||
[app.storage.impl :as impl]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.java.io :as io]
|
||||
[datoteka.fs :as fs]
|
||||
@@ -31,13 +31,13 @@
|
||||
java.time.Duration
|
||||
java.util.Collection
|
||||
java.util.Optional
|
||||
java.util.concurrent.atomic.AtomicLong
|
||||
org.reactivestreams.Subscriber
|
||||
software.amazon.awssdk.core.ResponseBytes
|
||||
software.amazon.awssdk.core.async.AsyncRequestBody
|
||||
software.amazon.awssdk.core.async.AsyncResponseTransformer
|
||||
software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody
|
||||
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
|
||||
@@ -69,7 +69,7 @@
|
||||
20000)
|
||||
|
||||
(def default-timeout
|
||||
(dt/duration {:seconds 30}))
|
||||
(ct/duration {:seconds 30}))
|
||||
|
||||
(declare put-object)
|
||||
(declare get-object-bytes)
|
||||
@@ -87,12 +87,11 @@
|
||||
|
||||
(def ^:private schema:config
|
||||
[:map {:title "s3-backend-config"}
|
||||
::wrk/executor
|
||||
::wrk/netty-io-executor
|
||||
[::region {:optional true} :keyword]
|
||||
[::bucket {:optional true} ::sm/text]
|
||||
[::prefix {:optional true} ::sm/text]
|
||||
[::endpoint {:optional true} ::sm/uri]
|
||||
[::io-threads {:optional true} ::sm/int]])
|
||||
[::endpoint {:optional true} ::sm/uri]])
|
||||
|
||||
(defmethod ig/expand-key ::backend
|
||||
[k v]
|
||||
@@ -110,6 +109,7 @@
|
||||
presigner (build-s3-presigner params)]
|
||||
(assoc params
|
||||
::sto/type :s3
|
||||
::counter (AtomicLong. 0)
|
||||
::client @client
|
||||
::presigner presigner
|
||||
::close-fn #(.close ^java.lang.AutoCloseable client)))))
|
||||
@@ -121,7 +121,7 @@
|
||||
(defmethod ig/halt-key! ::backend
|
||||
[_ {:keys [::close-fn]}]
|
||||
(when (fn? close-fn)
|
||||
(px/run! close-fn)))
|
||||
(close-fn)))
|
||||
|
||||
(def ^:private schema:backend
|
||||
[:map {:title "s3-backend"}
|
||||
@@ -198,19 +198,16 @@
|
||||
(Region/of (name region)))
|
||||
|
||||
(defn- build-s3-client
|
||||
[{:keys [::region ::endpoint ::io-threads ::wrk/executor]}]
|
||||
[{:keys [::region ::endpoint ::wrk/netty-io-executor]}]
|
||||
(let [aconfig (-> (ClientAsyncConfiguration/builder)
|
||||
(.advancedOption SdkAdvancedAsyncClientOption/FUTURE_COMPLETION_EXECUTOR executor)
|
||||
(.build))
|
||||
|
||||
sconfig (-> (S3Configuration/builder)
|
||||
(cond-> (some? endpoint) (.pathStyleAccessEnabled true))
|
||||
(.build))
|
||||
|
||||
thr-num (or io-threads (min 16 (px/get-available-processors)))
|
||||
hclient (-> (NettyNioAsyncHttpClient/builder)
|
||||
(.eventLoopGroupBuilder (-> (SdkEventLoopGroup/builder)
|
||||
(.numberOfThreads (int thr-num))))
|
||||
(.eventLoopGroup (SdkEventLoopGroup/create netty-io-executor))
|
||||
(.connectionAcquisitionTimeout default-timeout)
|
||||
(.connectionTimeout default-timeout)
|
||||
(.readTimeout default-timeout)
|
||||
@@ -262,7 +259,7 @@
|
||||
(.close ^InputStream input))))
|
||||
|
||||
(defn- make-request-body
|
||||
[executor content]
|
||||
[counter content]
|
||||
(let [size (impl/get-size content)]
|
||||
(reify
|
||||
AsyncRequestBody
|
||||
@@ -272,16 +269,19 @@
|
||||
(^void subscribe [_ ^Subscriber subscriber]
|
||||
(let [delegate (AsyncRequestBody/forBlockingInputStream (long size))
|
||||
input (io/input-stream content)]
|
||||
(px/run! executor (partial write-input-stream delegate input))
|
||||
|
||||
(px/thread-call (partial write-input-stream delegate input)
|
||||
{:name (str "penpot/storage/" (.getAndIncrement ^AtomicLong counter))})
|
||||
|
||||
(.subscribe ^BlockingInputStreamAsyncRequestBody delegate
|
||||
^Subscriber subscriber))))))
|
||||
|
||||
(defn- put-object
|
||||
[{:keys [::client ::bucket ::prefix ::wrk/executor]} {:keys [id] :as object} content]
|
||||
[{:keys [::client ::bucket ::prefix ::counter]} {:keys [id] :as object} content]
|
||||
(let [path (dm/str prefix (impl/id->path id))
|
||||
mdata (meta object)
|
||||
mtype (:content-type mdata "application/octet-stream")
|
||||
rbody (make-request-body executor content)
|
||||
rbody (make-request-body counter content)
|
||||
request (.. (PutObjectRequest/builder)
|
||||
(bucket bucket)
|
||||
(contentType mtype)
|
||||
@@ -338,11 +338,11 @@
|
||||
(p/fmap #(.asByteArray ^ResponseBytes %)))))
|
||||
|
||||
(def default-max-age
|
||||
(dt/duration {:minutes 10}))
|
||||
(ct/duration {:minutes 10}))
|
||||
|
||||
(defn- get-object-url
|
||||
[{:keys [::presigner ::bucket ::prefix]} {:keys [id]} {:keys [max-age] :or {max-age default-max-age}}]
|
||||
(assert (dt/duration? max-age) "expected valid duration instance")
|
||||
(assert (ct/duration? max-age) "expected valid duration instance")
|
||||
|
||||
(let [gor (.. (GetObjectRequest/builder)
|
||||
(bucket bucket)
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io]
|
||||
@@ -38,13 +38,13 @@
|
||||
|
||||
(defmethod ig/expand-key ::cleaner
|
||||
[k v]
|
||||
{k (assoc v ::min-age (dt/duration "60m"))})
|
||||
{k (assoc v ::min-age (ct/duration "60m"))})
|
||||
|
||||
(defmethod ig/init-key ::cleaner
|
||||
[_ cfg]
|
||||
(fs/create-dir default-tmp-dir)
|
||||
(px/fn->thread (partial io-loop cfg)
|
||||
{:name "penpot/storage/tmp-cleaner" :virtual true}))
|
||||
{:name "penpot/storage/tmp-cleaner"}))
|
||||
|
||||
(defmethod ig/halt-key! ::cleaner
|
||||
[_ thread]
|
||||
@@ -52,13 +52,13 @@
|
||||
|
||||
(defn- io-loop
|
||||
[{:keys [::min-age] :as cfg}]
|
||||
(l/inf :hint "started tmp cleaner" :default-min-age (dt/format-duration min-age))
|
||||
(l/inf :hint "started tmp cleaner" :default-min-age (ct/format-duration min-age))
|
||||
(try
|
||||
(loop []
|
||||
(when-let [[path min-age'] (sp/take! queue)]
|
||||
(let [min-age (or min-age' min-age)]
|
||||
(l/dbg :hint "schedule tempfile deletion" :path path
|
||||
:expires-at (dt/plus (dt/now) min-age))
|
||||
:expires-at (ct/plus (ct/now) min-age))
|
||||
(px/schedule! (inst-ms min-age) (partial remove-temp-file cfg path))
|
||||
(recur))))
|
||||
(catch InterruptedException _
|
||||
@@ -87,7 +87,7 @@
|
||||
path (fs/join default-tmp-dir (str prefix (uuid/next) suffix))
|
||||
path (Files/createFile path attrs)]
|
||||
(fs/delete-on-exit! path)
|
||||
(sp/offer! queue [path (some-> min-age dt/duration)])
|
||||
(sp/offer! queue [path (some-> min-age ct/duration)])
|
||||
path))
|
||||
|
||||
(defn tempfile-from
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
"A generic task for object deletion cascade handling"
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.util.time :as dt]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:dynamic *team-deletion* false)
|
||||
@@ -23,7 +23,7 @@
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||
(when-let [file (db/get* conn :file {:id id} {::db/remove-deleted false})]
|
||||
(l/trc :hint "marking for deletion" :rel "file" :id (str id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
(db/update! conn :file
|
||||
{:deleted-at deleted-at}
|
||||
@@ -62,7 +62,7 @@
|
||||
(defmethod delete-object :project
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||
(l/trc :hint "marking for deletion" :rel "project" :id (str id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
(db/update! conn :project
|
||||
{:deleted-at deleted-at}
|
||||
@@ -79,7 +79,7 @@
|
||||
(defmethod delete-object :team
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||
(l/trc :hint "marking for deletion" :rel "team" :id (str id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
(db/update! conn :team
|
||||
{:deleted-at deleted-at}
|
||||
{:id id}
|
||||
@@ -101,7 +101,7 @@
|
||||
(defmethod delete-object :profile
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||
(l/trc :hint "marking for deletion" :rel "profile" :id (str id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:deleted-at deleted-at}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.common.files.validate :as cfv]
|
||||
[app.common.logging :as l]
|
||||
[app.common.thumbnails :as thc]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
@@ -23,7 +24,6 @@
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.storage :as sto]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [min-age (dt/duration (or (:min-age props)
|
||||
(let [min-age (ct/duration (or (:min-age props)
|
||||
(cf/get-deletion-delay)))
|
||||
file-id (get props :file-id)
|
||||
cfg (-> cfg
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
"A maintenance task that is responsible of properly scheduling the
|
||||
file-gc task for all files that matches the eligibility threshold."
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [min-age (dt/duration (or (:min-age props) (::min-age cfg)))]
|
||||
(let [min-age (ct/duration (or (:min-age props) (::min-age cfg)))]
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(assoc ::min-age min-age)
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
of deleted or unreachable objects."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.util.time :as dt]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private sql:get-profiles
|
||||
@@ -53,7 +53,7 @@
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "team"
|
||||
:id (str id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
;; Mark as deleted the storage object
|
||||
(some->> photo-id (sto/touch-object! storage))
|
||||
@@ -82,7 +82,7 @@
|
||||
:rel "team-font-variant"
|
||||
:id (str id)
|
||||
:team-id (str team-id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
;; Mark as deleted the all related storage objects
|
||||
(some->> (:woff1-file-id font) (sto/touch-object! storage))
|
||||
@@ -114,7 +114,7 @@
|
||||
:rel "project"
|
||||
:id (str id)
|
||||
:team-id (str team-id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
;; And finally, permanently delete the project.
|
||||
(db/delete! conn :project {:id id})
|
||||
@@ -140,7 +140,7 @@
|
||||
:rel "file"
|
||||
:id (str id)
|
||||
:project-id (str project-id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
(when (= "objects-storage" (:data-backend file))
|
||||
(sto/touch-object! storage (:data-ref-id file)))
|
||||
@@ -169,7 +169,7 @@
|
||||
:rel "file-thumbnail"
|
||||
:file-id (str file-id)
|
||||
:revn revn
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
;; Mark as deleted the storage object
|
||||
(some->> media-id (sto/touch-object! storage))
|
||||
@@ -198,7 +198,7 @@
|
||||
:rel "file-tagged-object-thumbnail"
|
||||
:file-id (str file-id)
|
||||
:object-id object-id
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
;; Mark as deleted the storage object
|
||||
(some->> media-id (sto/touch-object! storage))
|
||||
@@ -227,7 +227,7 @@
|
||||
:rel "file-data-fragment"
|
||||
:id (str id)
|
||||
:file-id (str file-id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
(some->> data-ref-id (sto/touch-object! storage))
|
||||
(db/delete! conn :file-data-fragment {:file-id file-id :id id})
|
||||
@@ -253,7 +253,7 @@
|
||||
:rel "file-media-object"
|
||||
:id (str id)
|
||||
:file-id (str file-id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
;; Mark as deleted the all related storage objects
|
||||
(some->> (:media-id fmo) (sto/touch-object! storage))
|
||||
@@ -282,7 +282,7 @@
|
||||
:rel "file-change"
|
||||
:id (str id)
|
||||
:file-id (str file-id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
(when (= "objects-storage" (:data-backend xlog))
|
||||
(sto/touch-object! storage (:data-ref-id xlog)))
|
||||
@@ -313,7 +313,7 @@
|
||||
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
||||
(proc-fn cfg)))]
|
||||
(if (pos? result)
|
||||
(recur (+ total result))
|
||||
(recur (long (+ total result)))
|
||||
total))))
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
@@ -328,14 +328,14 @@
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [threshold (dt/duration (get props :deletion-threshold 0))
|
||||
(let [threshold (ct/duration (get props :deletion-threshold 0))
|
||||
cfg (assoc cfg ::deletion-threshold (db/interval threshold))]
|
||||
(loop [procs (map deref deletion-proc-vars)
|
||||
total 0]
|
||||
(if-let [proc-fn (first procs)]
|
||||
(let [result (execute-proc! cfg proc-fn)]
|
||||
(recur (rest procs)
|
||||
(+ total result)))
|
||||
(long (+ total result))))
|
||||
(do
|
||||
(l/inf :hint "task finished" :deleted total)
|
||||
{:processed total}))))))
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.util.time :as dt]
|
||||
[buddy.sign.jwe :as jwe]))
|
||||
|
||||
(defn generate
|
||||
@@ -22,7 +22,7 @@
|
||||
(bytes? tokens-key))
|
||||
|
||||
(let [payload (-> claims
|
||||
(assoc :iat (dt/now))
|
||||
(assoc :iat (ct/now))
|
||||
(d/without-nils)
|
||||
(t/encode))]
|
||||
(jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm})))
|
||||
@@ -35,8 +35,8 @@
|
||||
(defn verify
|
||||
[sprops {:keys [token] :as params}]
|
||||
(let [claims (decode sprops token)]
|
||||
(when (and (dt/instant? (:exp claims))
|
||||
(dt/is-before? (:exp claims) (dt/now)))
|
||||
(when (and (ct/inst? (:exp claims))
|
||||
(ct/is-before? (:exp claims) (ct/now)))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token
|
||||
:reason :token-expired
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(:refer-clojure :exclude [get])
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.util.time :as dt]
|
||||
[app.common.time :as ct]
|
||||
[promesa.exec :as px])
|
||||
(:import
|
||||
com.github.benmanes.caffeine.cache.AsyncCache
|
||||
@@ -51,7 +51,7 @@
|
||||
(let [cache (as-> (Caffeine/newBuilder) builder
|
||||
(if (fn? on-remove) (.removalListener builder (create-listener on-remove)) builder)
|
||||
(if executor (.executor builder ^Executor (px/resolve-executor executor)) builder)
|
||||
(if keepalive (.expireAfterAccess builder ^Duration (dt/duration keepalive)) builder)
|
||||
(if keepalive (.expireAfterAccess builder ^Duration (ct/duration keepalive)) builder)
|
||||
(if (int? max-size) (.maximumSize builder (long max-size)) builder)
|
||||
(.recordStats builder)
|
||||
(.buildAsync builder))
|
||||
|
||||
138
backend/src/app/util/cron.clj
Normal file
138
backend/src/app/util/cron.clj
Normal file
@@ -0,0 +1,138 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.util.cron
|
||||
(:require
|
||||
[app.common.exceptions :as ex])
|
||||
(:import
|
||||
java.time.Instant
|
||||
java.util.Date
|
||||
org.apache.logging.log4j.core.util.CronExpression))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Cron Expression
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Cron expressions are comprised of 6 required fields and one
|
||||
;; optional field separated by white space. The fields respectively
|
||||
;; are described as follows:
|
||||
;;
|
||||
;; Field Name Allowed Values Allowed Special Characters
|
||||
;; Seconds 0-59 , - * /
|
||||
;; Minutes 0-59 , - * /
|
||||
;; Hours 0-23 , - * /
|
||||
;; Day-of-month 1-31 , - * ? / L W
|
||||
;; Month 0-11 or JAN-DEC , - * /
|
||||
;; Day-of-Week 1-7 or SUN-SAT , - * ? / L #
|
||||
;; Year (Optional) empty, 1970-2199 , - * /
|
||||
;;
|
||||
;; The '*' character is used to specify all values. For example, "*"
|
||||
;; in the minute field means "every minute".
|
||||
;;
|
||||
;; The '?' character is allowed for the day-of-month and day-of-week
|
||||
;; fields. It is used to specify 'no specific value'. This is useful
|
||||
;; when you need to specify something in one of the two fields, but
|
||||
;; not the other.
|
||||
;;
|
||||
;; The '-' character is used to specify ranges For example "10-12" in
|
||||
;; the hour field means "the hours 10, 11 and 12".
|
||||
;;
|
||||
;; The ',' character is used to specify additional values. For
|
||||
;; example "MON,WED,FRI" in the day-of-week field means "the days
|
||||
;; Monday, Wednesday, and Friday".
|
||||
;;
|
||||
;; The '/' character is used to specify increments. For example "0/15"
|
||||
;; in the seconds field means "the seconds 0, 15, 30, and
|
||||
;; 45". And "5/15" in the seconds field means "the seconds 5, 20, 35,
|
||||
;; and 50". Specifying '*' before the '/' is equivalent to specifying
|
||||
;; 0 is the value to start with. Essentially, for each field in the
|
||||
;; expression, there is a set of numbers that can be turned on or
|
||||
;; off. For seconds and minutes, the numbers range from 0 to 59. For
|
||||
;; hours 0 to 23, for days of the month 0 to 31, and for months 0 to
|
||||
;; 11 (JAN to DEC). The "/" character simply helps you turn on
|
||||
;; every "nth" value in the given set. Thus "7/6" in the month field
|
||||
;; only turns on month "7", it does NOT mean every 6th month, please
|
||||
;; note that subtlety.
|
||||
;;
|
||||
;; The 'L' character is allowed for the day-of-month and day-of-week
|
||||
;; fields. This character is short-hand for "last", but it has
|
||||
;; different meaning in each of the two fields. For example, the
|
||||
;; value "L" in the day-of-month field means "the last day of the
|
||||
;; month" - day 31 for January, day 28 for February on non-leap
|
||||
;; years. If used in the day-of-week field by itself, it simply
|
||||
;; means "7" or "SAT". But if used in the day-of-week field after
|
||||
;; another value, it means "the last xxx day of the month" - for
|
||||
;; example "6L" means "the last friday of the month". You can also
|
||||
;; specify an offset from the last day of the month, such as "L-3"
|
||||
;; which would mean the third-to-last day of the calendar month. When
|
||||
;; using the 'L' option, it is important not to specify lists, or
|
||||
;; ranges of values, as you'll get confusing/unexpected results.
|
||||
;;
|
||||
;; The 'W' character is allowed for the day-of-month field. This
|
||||
;; character is used to specify the weekday (Monday-Friday) nearest
|
||||
;; the given day. As an example, if you were to specify "15W" as the
|
||||
;; value for the day-of-month field, the meaning is: "the nearest
|
||||
;; weekday to the 15th of the month". So if the 15th is a Saturday,
|
||||
;; the trigger will fire on Friday the 14th. If the 15th is a Sunday,
|
||||
;; the trigger will fire on Monday the 16th. If the 15th is a Tuesday,
|
||||
;; then it will fire on Tuesday the 15th. However if you specify "1W"
|
||||
;; as the value for day-of-month, and the 1st is a Saturday, the
|
||||
;; trigger will fire on Monday the 3rd, as it will not 'jump' over the
|
||||
;; boundary of a month's days. The 'W' character can only be specified
|
||||
;; when the day-of-month is a single day, not a range or list of days.
|
||||
;;
|
||||
;; The 'L' and 'W' characters can also be combined for the
|
||||
;; day-of-month expression to yield 'LW', which translates to "last
|
||||
;; weekday of the month".
|
||||
;;
|
||||
;; The '#' character is allowed for the day-of-week field. This
|
||||
;; character is used to specify "the nth" XXX day of the month. For
|
||||
;; example, the value of "6#3" in the day-of-week field means the
|
||||
;; third Friday of the month (day 6 = Friday and "#3" = the 3rd one in
|
||||
;; the month). Other examples: "2#1" = the first Monday of the month
|
||||
;; and "4#5" = the fifth Wednesday of the month. Note that if you
|
||||
;; specify "#5" and there is not 5 of the given day-of-week in the
|
||||
;; month, then no firing will occur that month. If the '#' character
|
||||
;; is used, there can only be one expression in the day-of-week
|
||||
;; field ("3#1,6#3" is not valid, since there are two expressions).
|
||||
;;
|
||||
;; The legal characters and the names of months and days of the week
|
||||
;; are not case sensitive.
|
||||
|
||||
(defn cron
|
||||
"Creates an instance of CronExpression from string."
|
||||
[s]
|
||||
(try
|
||||
(CronExpression. s)
|
||||
(catch java.text.ParseException e
|
||||
(ex/raise :type :parse
|
||||
:code :invalid-cron-expression
|
||||
:cause e
|
||||
:context {:expr s}))))
|
||||
|
||||
(defn cron-expr?
|
||||
[v]
|
||||
(instance? CronExpression v))
|
||||
|
||||
(defn next-valid-instant-from
|
||||
[^CronExpression cron ^Instant now]
|
||||
(assert (cron-expr? cron))
|
||||
(.toInstant (.getNextValidTimeAfter cron (Date/from now))))
|
||||
|
||||
(defn get-next
|
||||
[cron tnow]
|
||||
(let [nt (next-valid-instant-from cron tnow)]
|
||||
(cons nt (lazy-seq (get-next cron nt)))))
|
||||
|
||||
(defmethod print-method CronExpression
|
||||
[o w]
|
||||
(print-dup o w))
|
||||
|
||||
(defmethod print-dup CronExpression
|
||||
[mv ^java.io.Writer writer]
|
||||
;; Do not delete this comment
|
||||
;; (print-ctor o (fn [o w] (print-dup (.toString ^CronExpression o) w)) w)
|
||||
(.write writer (str "#penpot/cron \"" (.toString ^CronExpression mv) "\"")))
|
||||
@@ -27,7 +27,7 @@
|
||||
(sp/put! channel [type data])
|
||||
nil)))
|
||||
|
||||
(defn start-listener
|
||||
(defn spawn-listener
|
||||
[channel on-event on-close]
|
||||
(assert (sp/chan? channel) "expected active events channel")
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
[f on-event]
|
||||
|
||||
(binding [*channel* (sp/chan :buf 32)]
|
||||
(let [listener (start-listener *channel* on-event (constantly nil))]
|
||||
(let [listener (spawn-listener *channel* on-event (constantly nil))]
|
||||
(try
|
||||
(f)
|
||||
(finally
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
|
||||
(:require
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core :as c]
|
||||
[clojure.data.json :as json])
|
||||
(:import
|
||||
@@ -61,8 +61,10 @@
|
||||
(declare create)
|
||||
|
||||
(defn create-tracked
|
||||
[]
|
||||
(atom {}))
|
||||
[& {:keys [inherit]}]
|
||||
(if inherit
|
||||
(atom (if *tracked* @*tracked* {}))
|
||||
(atom {})))
|
||||
|
||||
(defprotocol IPointerMap
|
||||
(get-id [_])
|
||||
@@ -102,7 +104,7 @@
|
||||
|
||||
(clone [this]
|
||||
(when-not loaded? (load! this))
|
||||
(let [mdata (assoc mdata :created-at (dt/now))
|
||||
(let [mdata (assoc mdata :created-at (ct/now))
|
||||
id (uuid/next)
|
||||
pmap (PointerMap. id
|
||||
mdata
|
||||
@@ -177,7 +179,7 @@
|
||||
(let [odata' (assoc odata key val)]
|
||||
(if (identical? odata odata')
|
||||
this
|
||||
(let [mdata (assoc mdata :created-at (dt/now))
|
||||
(let [mdata (assoc mdata :created-at (ct/now))
|
||||
id (if modified? id (uuid/next))
|
||||
pmap (PointerMap. id
|
||||
mdata
|
||||
@@ -195,7 +197,7 @@
|
||||
(let [odata' (dissoc odata key)]
|
||||
(if (identical? odata odata')
|
||||
this
|
||||
(let [mdata (assoc mdata :created-at (dt/now))
|
||||
(let [mdata (assoc mdata :created-at (ct/now))
|
||||
id (if modified? id (uuid/next))
|
||||
pmap (PointerMap. id
|
||||
mdata
|
||||
@@ -218,7 +220,7 @@
|
||||
(defn create
|
||||
([]
|
||||
(let [id (uuid/next)
|
||||
mdata (assoc *metadata* :created-at (dt/now))
|
||||
mdata (assoc *metadata* :created-at (ct/now))
|
||||
pmap (PointerMap. id mdata {} true true)]
|
||||
(some-> *tracked* (swap! assoc id pmap))
|
||||
pmap))
|
||||
@@ -237,7 +239,7 @@
|
||||
(do
|
||||
(some-> *tracked* (swap! assoc (get-id data) data))
|
||||
data)
|
||||
(let [mdata (assoc (meta data) :created-at (dt/now))
|
||||
(let [mdata (assoc (meta data) :created-at (ct/now))
|
||||
id (uuid/next)
|
||||
pmap (PointerMap. id
|
||||
mdata
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user