mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
986 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b0eb06991 | ||
|
|
410c1dfd7f | ||
|
|
b98378311f | ||
|
|
e6306e7c2f | ||
|
|
0663e5f92b | ||
|
|
21e300fcec | ||
|
|
7fb4c89832 | ||
|
|
1e26fdd35b | ||
|
|
19db80bc2e | ||
|
|
4ac493250c | ||
|
|
ec9d960538 | ||
|
|
5fa6603fa4 | ||
|
|
d81517bf26 | ||
|
|
9af0f2896c | ||
|
|
f619ce09d6 | ||
|
|
58c41dd01f | ||
|
|
ec3e415bf0 | ||
|
|
230cb8da2c | ||
|
|
59f96358c4 | ||
|
|
874e78c6cd | ||
|
|
caea81501d | ||
|
|
1578253fb1 | ||
|
|
fec6437c5d | ||
|
|
edff24913a | ||
|
|
5120b49639 | ||
|
|
0e13cc3ed8 | ||
|
|
cb49e8e4d1 | ||
|
|
3113e034f6 | ||
|
|
85291fdec3 | ||
|
|
57e1b7efda | ||
|
|
be78a71bb7 | ||
|
|
2baa59cb3a | ||
|
|
aea7c5952a | ||
|
|
cbde321d3b | ||
|
|
aa5de988cf | ||
|
|
fd485a0d3e | ||
|
|
aea06a98b7 | ||
|
|
82401bdfdd | ||
|
|
12c25a2d21 | ||
|
|
241d5077c9 | ||
|
|
37757315d0 | ||
|
|
1195c37508 | ||
|
|
3d9b2deb59 | ||
|
|
32d948554f | ||
|
|
b7a6b7aba2 | ||
|
|
05e15a1d06 | ||
|
|
f57ecb6ef2 | ||
|
|
45318497d2 | ||
|
|
b762143d75 | ||
|
|
ba1a730d3a | ||
|
|
463bb2f91c | ||
|
|
e14cab0c8b | ||
|
|
3220ca4140 | ||
|
|
92de0cf6fe | ||
|
|
d111591c8b | ||
|
|
935686b9e3 | ||
|
|
b1ad21c7d3 | ||
|
|
4f800c8a76 | ||
|
|
5c498219ed | ||
|
|
0b211c39eb | ||
|
|
77b9776d9c | ||
|
|
04e596b616 | ||
|
|
9fba76fe5c | ||
|
|
8b9858aa66 | ||
|
|
2f7218b09c | ||
|
|
0a05091241 | ||
|
|
507d0cb2e2 | ||
|
|
fb61948244 | ||
|
|
15fdc99ec0 | ||
|
|
dd07c73c28 | ||
|
|
43354cafb1 | ||
|
|
b2c3187690 | ||
|
|
4a788ce9d8 | ||
|
|
f9ad8bd2d5 | ||
|
|
4429aea8a4 | ||
|
|
7a60e82d03 | ||
|
|
3544e9c355 | ||
|
|
ca4f0141c6 | ||
|
|
fcaa26a84d | ||
|
|
ea103d3070 | ||
|
|
286efbe548 | ||
|
|
a0ee800002 | ||
|
|
f4d3fba306 | ||
|
|
d5a58cfcb5 | ||
|
|
0e6bb27ecf | ||
|
|
83fdc01df1 | ||
|
|
c7ccabc6cb | ||
|
|
3eefe33e64 | ||
|
|
c44dc7f0ab | ||
|
|
1639d82348 | ||
|
|
b31e520731 | ||
|
|
e85d4143c0 | ||
|
|
da20f04e25 | ||
|
|
9a49ea426e | ||
|
|
c0c4c21682 | ||
|
|
c7424dfcb3 | ||
|
|
e33ca6af12 | ||
|
|
9dc6e308b6 | ||
|
|
4fe2ff5553 | ||
|
|
e5ccb82db8 | ||
|
|
63ae987010 | ||
|
|
efa0d3fd75 | ||
|
|
fd5930bc56 | ||
|
|
a057c6253b | ||
|
|
ed5d1fa891 | ||
|
|
76d36d18be | ||
|
|
a78c433a60 | ||
|
|
aff7530cb0 | ||
|
|
e7e7267fc2 | ||
|
|
9aaab2de3b | ||
|
|
8f0121a92d | ||
|
|
beb1e386c2 | ||
|
|
5b84a5e0e9 | ||
|
|
a9613651a5 | ||
|
|
d6ebc7f230 | ||
|
|
84ca118524 | ||
|
|
0ad6623e24 | ||
|
|
ba944f9dc2 | ||
|
|
99307befdd | ||
|
|
638c4e6b23 | ||
|
|
18bb7ef00d | ||
|
|
3dd9ce1fcd | ||
|
|
fd4814194c | ||
|
|
4d526fe419 | ||
|
|
50d1b187bb | ||
|
|
f54f78d186 | ||
|
|
97ec4949d7 | ||
|
|
f08b9d389b | ||
|
|
4fc94cae24 | ||
|
|
8cab8230e6 | ||
|
|
b7afa49c9f | ||
|
|
e0b5ccb41d | ||
|
|
fb63ffb919 | ||
|
|
cb98b0c937 | ||
|
|
bee56cfd99 | ||
|
|
9e130614ce | ||
|
|
2f487d2e83 | ||
|
|
1ff4b3a4fc | ||
|
|
e86d38c52c | ||
|
|
d371ae8ceb | ||
|
|
a9d10b2bf3 | ||
|
|
d6ffd193d3 | ||
|
|
2635213947 | ||
|
|
d496cbfb53 | ||
|
|
db8f0ad9ee | ||
|
|
8d795a4317 | ||
|
|
1a9ae456dd | ||
|
|
4e9d7a3f8c | ||
|
|
087967befb | ||
|
|
02ad6c7834 | ||
|
|
fc11f557fa | ||
|
|
f299f3ed20 | ||
|
|
94c831cd05 | ||
|
|
53b4c70e4b | ||
|
|
6ff5109b00 | ||
|
|
339afe614c | ||
|
|
911fdd3067 | ||
|
|
588e27e6d1 | ||
|
|
3a71032d62 | ||
|
|
59a091e7dc | ||
|
|
0e250a99ed | ||
|
|
f73b72753f | ||
|
|
00a85b0d90 | ||
|
|
e79d7e6fb5 | ||
|
|
ef745dc378 | ||
|
|
ac5dfb7ff0 | ||
|
|
51b0841171 | ||
|
|
57264da21b | ||
|
|
16f740b318 | ||
|
|
2ba70141cc | ||
|
|
e945329d91 | ||
|
|
ffeb4703fd | ||
|
|
fcf41187fa | ||
|
|
f0fc704efc | ||
|
|
aa58615650 | ||
|
|
b0a84bcbf7 | ||
|
|
cf2d413a28 | ||
|
|
9226b78b0c | ||
|
|
a635fd8a7b | ||
|
|
6f714277f7 | ||
|
|
4aff4982b0 | ||
|
|
8634989497 | ||
|
|
a50e60b2a0 | ||
|
|
8875f4dfc4 | ||
|
|
61c8217ed5 | ||
|
|
f75912033e | ||
|
|
1d8630810b | ||
|
|
ce155fb43e | ||
|
|
67ebe17fcb | ||
|
|
f13a6c0a3b | ||
|
|
e1870429b9 | ||
|
|
5aa1cd7f40 | ||
|
|
de1b887d29 | ||
|
|
e662f5ce0b | ||
|
|
790b69a32e | ||
|
|
9aaab97d6a | ||
|
|
66317197b6 | ||
|
|
91757335ac | ||
|
|
be3145aae9 | ||
|
|
79425af848 | ||
|
|
45b47a1071 | ||
|
|
4b30567c39 | ||
|
|
1ed33a7400 | ||
|
|
ca1d11006c | ||
|
|
71f1f3dd22 | ||
|
|
64add9a542 | ||
|
|
a7c02438e4 | ||
|
|
910af7a44c | ||
|
|
05f9ac3745 | ||
|
|
5c43eefe46 | ||
|
|
b63f4c53d2 | ||
|
|
3d652bc829 | ||
|
|
c78ef8771d | ||
|
|
9a38f16219 | ||
|
|
e5363205a7 | ||
|
|
088b3274f2 | ||
|
|
fabe323e34 | ||
|
|
a731bf034e | ||
|
|
3cbf772acf | ||
|
|
9701f88ae3 | ||
|
|
7ca1d118a1 | ||
|
|
a7dfa21cbf | ||
|
|
ab81444773 | ||
|
|
02c4343dfd | ||
|
|
1bfe86738f | ||
|
|
7fe0e05050 | ||
|
|
10588efe25 | ||
|
|
bf4607d368 | ||
|
|
45f04ae8d4 | ||
|
|
674f1de183 | ||
|
|
368c4fa1a7 | ||
|
|
c737579b7f | ||
|
|
0b7c51a323 | ||
|
|
e7c77b7bf3 | ||
|
|
6c727b7e51 | ||
|
|
a9754d6a1c | ||
|
|
218052d5dd | ||
|
|
f112cb748f | ||
|
|
67a65c66b6 | ||
|
|
cd6add14a0 | ||
|
|
33f9f1ddf4 | ||
|
|
2131de77b0 | ||
|
|
0febb60a3f | ||
|
|
820b914291 | ||
|
|
536d3ee70d | ||
|
|
2544d82486 | ||
|
|
ea6854fadc | ||
|
|
147e3b1173 | ||
|
|
8252142e65 | ||
|
|
6e2270632e | ||
|
|
7d14546d3b | ||
|
|
b31024c362 | ||
|
|
85eaef05ad | ||
|
|
16e4c10862 | ||
|
|
904e0f61a8 | ||
|
|
70953aa66e | ||
|
|
d41de5d270 | ||
|
|
d8603f4f34 | ||
|
|
b13a8094ea | ||
|
|
28e07bad54 | ||
|
|
81cc483166 | ||
|
|
8d7cc55c28 | ||
|
|
083e0e307a | ||
|
|
80f3f89a89 | ||
|
|
3dc8c18849 | ||
|
|
0a6a97cf07 | ||
|
|
7676d7a239 | ||
|
|
679715f896 | ||
|
|
d5aa880336 | ||
|
|
08daa86766 | ||
|
|
f0c1952dfe | ||
|
|
198d9115cc | ||
|
|
6b0c01bd3e | ||
|
|
a1848d3c75 | ||
|
|
a1596f7c62 | ||
|
|
740e701ad3 | ||
|
|
71d571a25c | ||
|
|
2293300182 | ||
|
|
85c7f2806f | ||
|
|
27ecb4fe49 | ||
|
|
7afeefe84f | ||
|
|
651922194f | ||
|
|
1d1b4fb04d | ||
|
|
b418f1eb05 | ||
|
|
a9b44ee39e | ||
|
|
cb48355898 | ||
|
|
6ce47e3a20 | ||
|
|
09e511bc49 | ||
|
|
66ade27383 | ||
|
|
d96058d589 | ||
|
|
8631a15933 | ||
|
|
c0bf81ad7c | ||
|
|
8fd1fa203e | ||
|
|
51b907bcf5 | ||
|
|
ecb5d974c5 | ||
|
|
19c91aff19 | ||
|
|
8e0cc7dd2f | ||
|
|
6478528ebc | ||
|
|
3e3b24f611 | ||
|
|
6dc7d70a1f | ||
|
|
a6f4c5651c | ||
|
|
291a4d39fe | ||
|
|
e639a1dd73 | ||
|
|
e3a961623b | ||
|
|
1ca27fdba2 | ||
|
|
f72428056a | ||
|
|
bdb3f11ac7 | ||
|
|
2f69af1a52 | ||
|
|
984cf77ee0 | ||
|
|
1762596fce | ||
|
|
910eb9b770 | ||
|
|
16807bd57f | ||
|
|
8bc29ad92d | ||
|
|
51a913a5e4 | ||
|
|
c30da805b3 | ||
|
|
42ee593708 | ||
|
|
1b49b35365 | ||
|
|
333ab7066c | ||
|
|
66dbdc09b5 | ||
|
|
79f396f6ff | ||
|
|
62ba4877ac | ||
|
|
e65a5e6d22 | ||
|
|
711b43070d | ||
|
|
b2f957d1d8 | ||
|
|
249745f256 | ||
|
|
8ff9d3bcf1 | ||
|
|
99c328c302 | ||
|
|
8a9f0a245e | ||
|
|
9daecc127a | ||
|
|
119bba4f04 | ||
|
|
bf205c579d | ||
|
|
aa57b17254 | ||
|
|
7df786c5c6 | ||
|
|
b315464e98 | ||
|
|
1252ea076b | ||
|
|
d970e446a8 | ||
|
|
fe40f3727b | ||
|
|
10a290b065 | ||
|
|
0abb11b42a | ||
|
|
1eb6eb729a | ||
|
|
e371a4a3c3 | ||
|
|
f2b6bd3d4b | ||
|
|
d21f87b543 | ||
|
|
b8cdee4f45 | ||
|
|
b06d7445d9 | ||
|
|
2aa9cceb32 | ||
|
|
ae2a2a8a07 | ||
|
|
286555411b | ||
|
|
3c42aec180 | ||
|
|
a203ae00c6 | ||
|
|
68784616a4 | ||
|
|
5cd8c6a5da | ||
|
|
a3647fea8a | ||
|
|
300e73f07d | ||
|
|
03ced23f05 | ||
|
|
da8eeb5d8f | ||
|
|
16cf02896f | ||
|
|
c9f10d248e | ||
|
|
3297e41289 | ||
|
|
4e1b347797 | ||
|
|
4602332dbb | ||
|
|
e1f69ee533 | ||
|
|
d90e431b5b | ||
|
|
45a9eca0ce | ||
|
|
af202f4871 | ||
|
|
7b32fa35f3 | ||
|
|
0c40b230dc | ||
|
|
2baba67bfc | ||
|
|
b1e69a9059 | ||
|
|
f87362ec1b | ||
|
|
84dc57373a | ||
|
|
b3f4a02658 | ||
|
|
50304dbefe | ||
|
|
bf01af0bf3 | ||
|
|
0e3fe3463b | ||
|
|
3956763825 | ||
|
|
d6fe895f6e | ||
|
|
697fa08d8b | ||
|
|
669a35afef | ||
|
|
b1d94a3845 | ||
|
|
06869cf491 | ||
|
|
ed67d8514c | ||
|
|
07ec2f3d7b | ||
|
|
73d02e85c4 | ||
|
|
c1cf1eb37a | ||
|
|
7aecc94049 | ||
|
|
b55fbfeb8e | ||
|
|
20b8991fa4 | ||
|
|
e50de4201d | ||
|
|
ba2b876ee4 | ||
|
|
9b1399c9d5 | ||
|
|
020a2f00c2 | ||
|
|
8713946f35 | ||
|
|
bed8c25e40 | ||
|
|
7c51a09068 | ||
|
|
0f0a6ce042 | ||
|
|
a890968ee4 | ||
|
|
2267c5be00 | ||
|
|
e853cfb98a | ||
|
|
f89a0db471 | ||
|
|
1d66b43da1 | ||
|
|
75c4a4f253 | ||
|
|
6a9c95b3b1 | ||
|
|
c11a2d0081 | ||
|
|
43a13ff2c5 | ||
|
|
8454d163eb | ||
|
|
4a11f6a39f | ||
|
|
c63235cd3d | ||
|
|
f4ce196a34 | ||
|
|
e644b27201 | ||
|
|
d2b14582ee | ||
|
|
ca9194fd96 | ||
|
|
773d3ab13c | ||
|
|
a02a4a07a5 | ||
|
|
0a8625227f | ||
|
|
dc613aa2bf | ||
|
|
2008ac5663 | ||
|
|
da326f1925 | ||
|
|
71911cb1a7 | ||
|
|
31e7582216 | ||
|
|
91f63047e0 | ||
|
|
c9aa4938f4 | ||
|
|
b9e6a38037 | ||
|
|
eb054146d2 | ||
|
|
a780373cfc | ||
|
|
5ad90ccdb8 | ||
|
|
ef97d81ca8 | ||
|
|
c9f7b642c5 | ||
|
|
770962be3f | ||
|
|
d6c857f4a8 | ||
|
|
7b7ffb427d | ||
|
|
b80318d1b6 | ||
|
|
3997d69e49 | ||
|
|
4af41b33c0 | ||
|
|
0f8bbead66 | ||
|
|
804fcff5b6 | ||
|
|
d18e6920fc | ||
|
|
c95174ff11 | ||
|
|
c70329c22b | ||
|
|
34f69263a2 | ||
|
|
bf5056f8fe | ||
|
|
c47f5b06ba | ||
|
|
63055e3df2 | ||
|
|
8ee555140b | ||
|
|
859f78e986 | ||
|
|
366eebb68c | ||
|
|
25ce0b9aee | ||
|
|
376a7db545 | ||
|
|
f906ed741d | ||
|
|
ad152e62e5 | ||
|
|
7f3a61ef43 | ||
|
|
a7ba8c8213 | ||
|
|
b1f3d6d4c0 | ||
|
|
5707ae027f | ||
|
|
770208024d | ||
|
|
975a9f5344 | ||
|
|
a45008411e | ||
|
|
9091ffd1b3 | ||
|
|
089f161d1e | ||
|
|
e6d7131701 | ||
|
|
ed82170c2c | ||
|
|
3472d42cdd | ||
|
|
b8926350b9 | ||
|
|
e5194b313d | ||
|
|
94228375ad | ||
|
|
0e8385a5a7 | ||
|
|
afd3763d96 | ||
|
|
1122f09093 | ||
|
|
1123af15c9 | ||
|
|
b2f6e865a7 | ||
|
|
93b9ac2d13 | ||
|
|
64af65b0e7 | ||
|
|
390c8437c5 | ||
|
|
cc65ca9b6a | ||
|
|
4bd7cba247 | ||
|
|
92e2f7f8b7 | ||
|
|
98054ddff6 | ||
|
|
4e5c1aba2e | ||
|
|
5eb44320ae | ||
|
|
f751269289 | ||
|
|
0e836622ec | ||
|
|
a6ac0a813d | ||
|
|
21c47f60f2 | ||
|
|
66142b1dd8 | ||
|
|
3a28f4a0cb | ||
|
|
71eeca96ee | ||
|
|
b56cde47ca | ||
|
|
793e65bac0 | ||
|
|
4d1a569269 | ||
|
|
5c56317d50 | ||
|
|
32491ae2ad | ||
|
|
28aeb11565 | ||
|
|
61eb3ff908 | ||
|
|
ff5455881e | ||
|
|
143b7ce678 | ||
|
|
9c3dfc78df | ||
|
|
017872c5ef | ||
|
|
b9ad67d147 | ||
|
|
62ea3b1a1a | ||
|
|
325b668d00 | ||
|
|
9ce6bf8e14 | ||
|
|
052448c59a | ||
|
|
c8b318b315 | ||
|
|
54daac329e | ||
|
|
b5a59e4de0 | ||
|
|
258876d13f | ||
|
|
9cc8e16d64 | ||
|
|
85d1254638 | ||
|
|
45eb261dcd | ||
|
|
8b2ff5f269 | ||
|
|
79ab568c80 | ||
|
|
382b1737cb | ||
|
|
c9b3a83592 | ||
|
|
0b21ce7ce9 | ||
|
|
523bf8c689 | ||
|
|
8ead5a9d1f | ||
|
|
46e3199438 | ||
|
|
d94e614a54 | ||
|
|
9f459867bf | ||
|
|
f5ce319c24 | ||
|
|
6b9c02d42a | ||
|
|
e2e06a8f83 | ||
|
|
8402f73d52 | ||
|
|
84334ace6a | ||
|
|
43e2518fbf | ||
|
|
13098713e9 | ||
|
|
77c6e541d6 | ||
|
|
bbd49f9372 | ||
|
|
015be517bf | ||
|
|
8faa936a68 | ||
|
|
39922cca5e | ||
|
|
63dd68081a | ||
|
|
3694b19a97 | ||
|
|
c58b2a5e45 | ||
|
|
5b8562f5bd | ||
|
|
67020426f6 | ||
|
|
3840894201 | ||
|
|
888f3feaff | ||
|
|
2e339d7457 | ||
|
|
fd0ff633e6 | ||
|
|
02923fe9ce | ||
|
|
875f97813e | ||
|
|
6a29385762 | ||
|
|
fbc9f9dec9 | ||
|
|
4607ade8e9 | ||
|
|
e4a553c022 | ||
|
|
bc733bab0c | ||
|
|
1a522ddf48 | ||
|
|
1de0f4c4fa | ||
|
|
71eb5c25b5 | ||
|
|
ea34979a29 | ||
|
|
1f784ad44c | ||
|
|
f89726752f | ||
|
|
921b6b7ba4 | ||
|
|
6c5cc7a0ce | ||
|
|
f851e2f2cf | ||
|
|
b73207f29a | ||
|
|
8d8f0b55ea | ||
|
|
0e1e9b9cf3 | ||
|
|
ab4eed78af | ||
|
|
8a36c15d5c | ||
|
|
bd895286bc | ||
|
|
b76990ec16 | ||
|
|
9bc93c5cf5 | ||
|
|
1d8ae79f84 | ||
|
|
4269aa2422 | ||
|
|
c887dc1ec8 | ||
|
|
8bbacd691d | ||
|
|
d8f3f85fb7 | ||
|
|
159c0b3350 | ||
|
|
45788f3235 | ||
|
|
4c85c6ccdf | ||
|
|
08408ae005 | ||
|
|
d470279fb7 | ||
|
|
8260458f8f | ||
|
|
0512dd1e86 | ||
|
|
4972019cb9 | ||
|
|
5e615cab52 | ||
|
|
64ea2b6d3b | ||
|
|
27018fa8e2 | ||
|
|
263d634801 | ||
|
|
c61c33f71c | ||
|
|
bfb30b8fed | ||
|
|
ca3e27937e | ||
|
|
50c34a4186 | ||
|
|
84875aaf82 | ||
|
|
b7a8b95820 | ||
|
|
5cad5ca11a | ||
|
|
7598de29dd | ||
|
|
af6ffe67c8 | ||
|
|
6944c4b3bf | ||
|
|
24817d5707 | ||
|
|
3d855712ee | ||
|
|
872263d253 | ||
|
|
1c491fb1c5 | ||
|
|
29374c579a | ||
|
|
206d7b6ca9 | ||
|
|
ab6ca8c894 | ||
|
|
e7f5fde051 | ||
|
|
030b358b0e | ||
|
|
4b1791d35c | ||
|
|
b788cf823d | ||
|
|
11cbc0288f | ||
|
|
2269c80b02 | ||
|
|
c922cc042e | ||
|
|
9cb6f44f72 | ||
|
|
f376cd5ab1 | ||
|
|
8b8a02b68c | ||
|
|
5759b1336a | ||
|
|
76f17cb6d7 | ||
|
|
7149c421cf | ||
|
|
353c599767 | ||
|
|
285c43dab7 | ||
|
|
bd5933f4c2 | ||
|
|
0faac08237 | ||
|
|
14f94b1566 | ||
|
|
415e2ff265 | ||
|
|
26e97358d1 | ||
|
|
0ed6fb33c5 | ||
|
|
df98e7512b | ||
|
|
99ede6477f | ||
|
|
ee0debe14a | ||
|
|
dd1e11f102 | ||
|
|
ed47493459 | ||
|
|
5b8391067d | ||
|
|
4f41a59ceb | ||
|
|
4525cb38e4 | ||
|
|
704dfc25e6 | ||
|
|
7022ebe4ca | ||
|
|
094863f0ea | ||
|
|
a6282125cf | ||
|
|
63387a09af | ||
|
|
d4c47cecf2 | ||
|
|
b839069af1 | ||
|
|
e3c98100cc | ||
|
|
afcce04650 | ||
|
|
5fb35c2d48 | ||
|
|
dfe89f0d83 | ||
|
|
c500ec071e | ||
|
|
6e47f16fe1 | ||
|
|
13e6c2dbeb | ||
|
|
9d27dfb41c | ||
|
|
9529905875 | ||
|
|
eef7fc4b51 | ||
|
|
a88435f6b6 | ||
|
|
0b22598783 | ||
|
|
40cc019a51 | ||
|
|
781094ab05 | ||
|
|
21cfe2e535 | ||
|
|
4f72684080 | ||
|
|
5ac514366e | ||
|
|
d5d6285eef | ||
|
|
0e34b41bce | ||
|
|
e157066bcc | ||
|
|
f43a63ac6e | ||
|
|
a5828fa01a | ||
|
|
662d3ff795 | ||
|
|
a97f05db3b | ||
|
|
5a439ca70c | ||
|
|
551faca3a3 | ||
|
|
c25ffe9639 | ||
|
|
900021ec1b | ||
|
|
cc1ad16688 | ||
|
|
fabac9fcd9 | ||
|
|
da170b9b16 | ||
|
|
0e417430b8 | ||
|
|
0cfac31bca | ||
|
|
0f5e8fd314 | ||
|
|
e457155950 | ||
|
|
5a98790dd3 | ||
|
|
e188f65e7c | ||
|
|
6bf3ea3c46 | ||
|
|
fa33ac57fb | ||
|
|
0bed9eea09 | ||
|
|
36e9f7a8cb | ||
|
|
b155dbef6a | ||
|
|
e48b428725 | ||
|
|
68884c9126 | ||
|
|
a713976f9c | ||
|
|
51d7306380 | ||
|
|
0b83f75ec8 | ||
|
|
0a80635943 | ||
|
|
e6a5f4b203 | ||
|
|
061b2fdbcb | ||
|
|
7922e32618 | ||
|
|
0949d10da6 | ||
|
|
0c8ad9262e | ||
|
|
f841aef82d | ||
|
|
d42e51315d | ||
|
|
e761b73cfd | ||
|
|
1a9c9a7767 | ||
|
|
eb7ce7cef1 | ||
|
|
68fd8dcd76 | ||
|
|
ce8bb2567c | ||
|
|
ab3d9bc120 | ||
|
|
432ec161c1 | ||
|
|
fcaf4a9b51 | ||
|
|
cbb7e6599b | ||
|
|
8f89a144ca | ||
|
|
50bd05b351 | ||
|
|
4e7f5401ec | ||
|
|
3fd56e8c36 | ||
|
|
daf028ab2b | ||
|
|
5467620b32 | ||
|
|
a3cae0f094 | ||
|
|
f74eea4314 | ||
|
|
04e930a766 | ||
|
|
89ac13e805 | ||
|
|
0b86d38517 | ||
|
|
7e76809860 | ||
|
|
4d46b98e7f | ||
|
|
6c1a46aee5 | ||
|
|
a7cef5a1c6 | ||
|
|
8a218a6626 | ||
|
|
542fe18dcd | ||
|
|
25f18ac951 | ||
|
|
5540438595 | ||
|
|
b25456835a | ||
|
|
f86091382f | ||
|
|
dbdf8871cc | ||
|
|
1e6926ee2d | ||
|
|
24cbe98ac2 | ||
|
|
c590b026d6 | ||
|
|
d28221aec9 | ||
|
|
df0c6c8c2d | ||
|
|
876f0f9a14 | ||
|
|
f9f0fc1d67 | ||
|
|
42e357341a | ||
|
|
edc5402aad | ||
|
|
3f61ffa6fb | ||
|
|
1c22b889fe | ||
|
|
e80f5939b2 | ||
|
|
c068088cc1 | ||
|
|
1767f371f7 | ||
|
|
5d5caf64fa | ||
|
|
e095b9419a | ||
|
|
a5ef072f56 | ||
|
|
5f6a2456d4 | ||
|
|
b0f24889f4 | ||
|
|
0a69f0fd51 | ||
|
|
43c0142682 | ||
|
|
1ff16cc54b | ||
|
|
3609cf54c0 | ||
|
|
198038f5ea | ||
|
|
b5a3df85a8 | ||
|
|
6e56c6e0d4 | ||
|
|
568f3d6007 | ||
|
|
6b505a1d06 | ||
|
|
acc7e91c55 | ||
|
|
8e43df8d95 | ||
|
|
ca81b382d3 | ||
|
|
a60d7976f5 | ||
|
|
46cf63da37 | ||
|
|
099079b962 | ||
|
|
5b8121516f | ||
|
|
23eacaa78a | ||
|
|
659292a3f1 | ||
|
|
f88756d3a8 | ||
|
|
81e7bab22a | ||
|
|
1eeb5fc2b0 | ||
|
|
126cdd73e4 | ||
|
|
2251267ae4 | ||
|
|
f3e4b6d9a2 | ||
|
|
b22379537b | ||
|
|
feb5bb0653 | ||
|
|
ebfe99be79 | ||
|
|
49c1b939d5 | ||
|
|
84a37f124b | ||
|
|
f0f48f4ddf | ||
|
|
cf15e3a9a1 | ||
|
|
927f98c66b | ||
|
|
782f6ac27e | ||
|
|
5140e4ec9a | ||
|
|
e255f9bfc5 | ||
|
|
9e9b369dc2 | ||
|
|
765dc734a4 | ||
|
|
3abe8a1103 | ||
|
|
73333074fd | ||
|
|
a415786e1c | ||
|
|
a3a1077dea | ||
|
|
58a812c2c9 | ||
|
|
7e69ec4367 | ||
|
|
4623c01896 | ||
|
|
e85322de62 | ||
|
|
090ff3da13 | ||
|
|
fe068b1bfb | ||
|
|
d3b0a685b9 | ||
|
|
245c04fc5a | ||
|
|
94acb0c6ae | ||
|
|
ad4a16cc4e | ||
|
|
dae39113f6 | ||
|
|
1e1cb8fe57 | ||
|
|
606254d941 | ||
|
|
8fd51b9e9e | ||
|
|
f90442cfb1 | ||
|
|
2a44c0f65a | ||
|
|
cd8d2b5e95 | ||
|
|
8d89b2e80a | ||
|
|
3255f8af0c | ||
|
|
ca0784fa59 | ||
|
|
827060b6ce | ||
|
|
60c069cb1f | ||
|
|
70dfea5876 | ||
|
|
ab475d65ee | ||
|
|
fa9963a737 | ||
|
|
d65d9c9a4c | ||
|
|
6f8c7a613f | ||
|
|
1b45c927b2 | ||
|
|
ab1cc896de | ||
|
|
4b91600e35 | ||
|
|
69e43e40eb | ||
|
|
930622ea3c | ||
|
|
7ac104c3c6 | ||
|
|
c47dc5ecdb | ||
|
|
a3f05c1146 | ||
|
|
04b1fa378d | ||
|
|
cbc9d9ab8c | ||
|
|
7f61c63425 | ||
|
|
3d379dec58 | ||
|
|
9968a816db | ||
|
|
8f721cd12a | ||
|
|
d45f2e1ec7 | ||
|
|
7e0f1794a5 | ||
|
|
20959fc68f | ||
|
|
a942224f46 | ||
|
|
fef16f6b28 | ||
|
|
1abc4d0365 | ||
|
|
862761d46f | ||
|
|
94743a16be | ||
|
|
f6729834b6 | ||
|
|
1b4c435900 | ||
|
|
24e6a86bf5 | ||
|
|
88f74d5e9c | ||
|
|
171f93662f | ||
|
|
c5429f7efa | ||
|
|
36847e6a06 | ||
|
|
438fd7d825 | ||
|
|
767b61a3bb | ||
|
|
dd2542dcd6 | ||
|
|
f8b63d487a | ||
|
|
fcd0feb7f6 | ||
|
|
8287cf386b | ||
|
|
8415bee5da | ||
|
|
30eb48321c | ||
|
|
7cff2e01a4 | ||
|
|
fa94341b60 | ||
|
|
a1b34d5e7b | ||
|
|
14ef3cf07c | ||
|
|
db6cddefd4 | ||
|
|
95c6ecb724 | ||
|
|
fc27f6b3f0 | ||
|
|
2ce0bb2caa | ||
|
|
94e1c006ab | ||
|
|
fc2e3586a2 | ||
|
|
bca8b6b606 | ||
|
|
bb6a3471d5 | ||
|
|
09d07e4890 | ||
|
|
8826ad8990 | ||
|
|
b350edc250 | ||
|
|
af326c0b1e | ||
|
|
edc346a6b3 | ||
|
|
1e3fc97f2b | ||
|
|
51b78ef5f3 | ||
|
|
4e4036382b | ||
|
|
daf7cb9d6b | ||
|
|
08c97f280f | ||
|
|
9c836a34ee | ||
|
|
e839430270 | ||
|
|
cf5e530ed9 | ||
|
|
49ac5e647f | ||
|
|
5a7b3ded20 | ||
|
|
bcf06f0d69 | ||
|
|
4f3fe28905 | ||
|
|
f695861731 | ||
|
|
2fee966fc8 | ||
|
|
6964124c1b | ||
|
|
f9e2d91e48 | ||
|
|
2c483e7f7c | ||
|
|
3d078108a3 | ||
|
|
794967bd9f | ||
|
|
554474776c | ||
|
|
81c5f9cd4e | ||
|
|
e4735401a7 | ||
|
|
758c3be3c5 | ||
|
|
2be9ae7868 | ||
|
|
3ee4540f7d | ||
|
|
84c1175379 | ||
|
|
ad4d87a138 | ||
|
|
5feff0d0ec | ||
|
|
08e7a2c065 | ||
|
|
a524521fdb | ||
|
|
0b2312a407 | ||
|
|
c7d9e24c00 | ||
|
|
0d827e490e | ||
|
|
6402125233 | ||
|
|
7bfbda4490 | ||
|
|
f6b357691c | ||
|
|
0c1e4e6888 | ||
|
|
c7ecdcf625 | ||
|
|
957b9b1cf4 | ||
|
|
1b4eed1a6e | ||
|
|
acd47b870f | ||
|
|
9ab8d03697 | ||
|
|
9c6f23d7cc | ||
|
|
9af00d6d58 | ||
|
|
186c09f29b | ||
|
|
70b185a37a | ||
|
|
5af01505c2 | ||
|
|
5f6ab2fc08 | ||
|
|
c8f7ea934c | ||
|
|
a4aa8dafac | ||
|
|
8ca9866440 | ||
|
|
97eff7f21d | ||
|
|
e30fd8eb9b | ||
|
|
6fa48c9222 | ||
|
|
c36c7144cb | ||
|
|
bbd0a893b1 | ||
|
|
cfea2b8d9d | ||
|
|
0fcbe821cd | ||
|
|
ca41164980 | ||
|
|
cf20e2a510 | ||
|
|
02f1231e7d | ||
|
|
6846e7941f | ||
|
|
719287c3a6 | ||
|
|
194670c3bf | ||
|
|
c4222e0981 | ||
|
|
61dc4b8bb8 | ||
|
|
df357c1008 | ||
|
|
871b52a288 | ||
|
|
8a4a043e9f | ||
|
|
c0297aa061 | ||
|
|
2aaa2eeaff | ||
|
|
5f62e5bacf | ||
|
|
ab78559710 | ||
|
|
0d75bb1d3e | ||
|
|
6441062e7f | ||
|
|
4827a45f22 | ||
|
|
40eef457ab | ||
|
|
c22a4d109b | ||
|
|
571c3e04c4 | ||
|
|
f9fb97a572 | ||
|
|
e6484bb2d5 | ||
|
|
19f51fdc1f | ||
|
|
25f7ba3ee6 | ||
|
|
a3cd1ff4e1 | ||
|
|
f62c717f79 | ||
|
|
3ed99133d4 | ||
|
|
ab25cc331d | ||
|
|
2648992a9b | ||
|
|
7bcc3d081e | ||
|
|
affd1c45ca | ||
|
|
8c7458812d | ||
|
|
053908c772 | ||
|
|
d623736f9f | ||
|
|
a3e416ff68 | ||
|
|
8bf207b9f7 | ||
|
|
e1520d5e7e | ||
|
|
4f89c9f7c6 | ||
|
|
d34fb92211 | ||
|
|
3f7b006d16 | ||
|
|
a15cf33691 | ||
|
|
4c1699df6c | ||
|
|
404535c512 | ||
|
|
2cedcecc38 | ||
|
|
ba7b02d8d4 | ||
|
|
6aa3c16646 | ||
|
|
5a03e17e00 | ||
|
|
417d145b43 | ||
|
|
e5636a2afa | ||
|
|
5291d2d129 | ||
|
|
c6f6a87499 | ||
|
|
1ef21c6ed6 | ||
|
|
6ee1932cbf | ||
|
|
83cf43a314 | ||
|
|
52c14a79a1 | ||
|
|
152db0551d | ||
|
|
3a2467ccfe | ||
|
|
5a28f3597c | ||
|
|
9ce179f91b | ||
|
|
7aeb072cc3 | ||
|
|
8a652761a3 | ||
|
|
be667575d5 | ||
|
|
59ca8a44a0 | ||
|
|
c9f0d3bfd8 | ||
|
|
06e7a1fda1 | ||
|
|
495203c112 |
@@ -1,64 +0,0 @@
|
|||||||
FROM {{ if eq "CI" .Orbit.Image }}golang:1.10-stretch{{ else }}debian:stretch-slim{{ end }}
|
|
||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Libraries
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs all required libraries.
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y xvfb wkhtmltopdf pdftk unoconv ttf-mscorefonts-installer &&\
|
|
||||||
ln -s /usr/bin/xvfb-run /usr/local/bin/xvfb-run &&\
|
|
||||||
ln -s /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf &&\
|
|
||||||
ln -s /usr/bin/pdftk /usr/local/bin/pdftk &&\
|
|
||||||
ln -s /usr/bin/unoconv /usr/local/bin/unoconv
|
|
||||||
|
|
||||||
RUN apt-get install -y curl gnupg &&\
|
|
||||||
curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y nodejs
|
|
||||||
|
|
||||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y yarn bzip2 &&\
|
|
||||||
yarn global add markdown-pdf --prefix /usr/local
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Gotenberg
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | All Gotenberg related stuff.
|
|
||||||
# |
|
|
||||||
|
|
||||||
{{- if eq "CI" .Orbit.Image }}
|
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/thecodingmachine/gotenberg
|
|
||||||
|
|
||||||
# Installs lint dependencies.
|
|
||||||
RUN go get -u gopkg.in/alecthomas/gometalinter.v2 &&\
|
|
||||||
gometalinter.v2 --install
|
|
||||||
|
|
||||||
# Copies our Go source.
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Installs project dependencies.
|
|
||||||
RUN go get -d -v ./...
|
|
||||||
|
|
||||||
ENTRYPOINT [".ci/docker-entrypoint.sh"]
|
|
||||||
{{- else }}
|
|
||||||
|
|
||||||
COPY .ci/gotenberg /usr/bin/gotenberg
|
|
||||||
RUN ln -s /usr/bin/gotenberg /usr/local/bin/gotenberg
|
|
||||||
|
|
||||||
COPY .ci/gotenberg.yml /gotenberg/gotenberg.yml
|
|
||||||
|
|
||||||
WORKDIR /gotenberg
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
CMD ["gotenberg"]
|
|
||||||
{{- end }}
|
|
||||||
72
.bruno/Chromium/Convert/HTML to PDF.bru
Normal file
72
.bruno/Chromium/Convert/HTML to PDF.bru
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
meta {
|
||||||
|
name: HTML to PDF
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/chromium/convert/html
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page-1-html/index.html)
|
||||||
|
~landscape: false
|
||||||
|
~printBackground: false
|
||||||
|
~scale: 1.0
|
||||||
|
~singlePage: false
|
||||||
|
~paperWidth: 8.5
|
||||||
|
~paperHeight: 11
|
||||||
|
~marginTop: 0.39
|
||||||
|
~marginBottom: 0.39
|
||||||
|
~marginLeft: 0.39
|
||||||
|
~marginRight: 0.39
|
||||||
|
~nativePageRanges: 1-2
|
||||||
|
~preferCssPageSize: false
|
||||||
|
~generateDocumentOutline: false
|
||||||
|
~generateTaggedPdf: false
|
||||||
|
~skipNetworkIdleEvent: false
|
||||||
|
~failOnHttpStatusCodes: [499,599]
|
||||||
|
~failOnResourceHttpStatusCodes: []
|
||||||
|
~ignoreResourceHttpStatusDomains: []
|
||||||
|
~failOnResourceLoadingFailed: false
|
||||||
|
~failOnConsoleExceptions: false
|
||||||
|
~waitDelay: 0s
|
||||||
|
~waitWindowStatus:
|
||||||
|
~waitForExpression:
|
||||||
|
~waitForSelector:
|
||||||
|
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||||
|
~userAgent:
|
||||||
|
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||||
|
~emulatedMediaType: print
|
||||||
|
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||||
|
~omitBackground: false
|
||||||
|
~splitMode: intervals
|
||||||
|
~splitSpan: 1
|
||||||
|
~splitUnify: false
|
||||||
|
~pdfa: PDF/A-1b
|
||||||
|
~pdfua: true
|
||||||
|
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||||
|
~userPassword:
|
||||||
|
~ownerPassword:
|
||||||
|
~watermarkSource: text
|
||||||
|
~watermarkExpression: CONFIDENTIAL
|
||||||
|
~watermarkPages:
|
||||||
|
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||||
|
~stampSource: text
|
||||||
|
~stampExpression: DRAFT
|
||||||
|
~stampPages:
|
||||||
|
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||||
|
~rotateAngle: 90
|
||||||
|
~rotatePages:
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: my-file
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
73
.bruno/Chromium/Convert/Markdown to PDF.bru
Normal file
73
.bruno/Chromium/Convert/Markdown to PDF.bru
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
meta {
|
||||||
|
name: Markdown to PDF
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/chromium/convert/markdown
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page-1-markdown/index.html)
|
||||||
|
files: @file(../../test/integration/testdata/page-1-markdown/page_1.md)
|
||||||
|
~landscape: false
|
||||||
|
~printBackground: false
|
||||||
|
~scale: 1.0
|
||||||
|
~singlePage: false
|
||||||
|
~paperWidth: 8.5
|
||||||
|
~paperHeight: 11
|
||||||
|
~marginTop: 0.39
|
||||||
|
~marginBottom: 0.39
|
||||||
|
~marginLeft: 0.39
|
||||||
|
~marginRight: 0.39
|
||||||
|
~nativePageRanges: 1-2
|
||||||
|
~preferCssPageSize: false
|
||||||
|
~generateDocumentOutline: false
|
||||||
|
~generateTaggedPdf: false
|
||||||
|
~skipNetworkIdleEvent: false
|
||||||
|
~failOnHttpStatusCodes: [499,599]
|
||||||
|
~failOnResourceHttpStatusCodes: []
|
||||||
|
~ignoreResourceHttpStatusDomains: []
|
||||||
|
~failOnResourceLoadingFailed: false
|
||||||
|
~failOnConsoleExceptions: false
|
||||||
|
~waitDelay: 0s
|
||||||
|
~waitWindowStatus:
|
||||||
|
~waitForExpression:
|
||||||
|
~waitForSelector:
|
||||||
|
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||||
|
~userAgent:
|
||||||
|
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||||
|
~emulatedMediaType: print
|
||||||
|
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||||
|
~omitBackground: false
|
||||||
|
~splitMode: intervals
|
||||||
|
~splitSpan: 1
|
||||||
|
~splitUnify: false
|
||||||
|
~pdfa: PDF/A-1b
|
||||||
|
~pdfua: true
|
||||||
|
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||||
|
~userPassword:
|
||||||
|
~ownerPassword:
|
||||||
|
~watermarkSource: text
|
||||||
|
~watermarkExpression: CONFIDENTIAL
|
||||||
|
~watermarkPages:
|
||||||
|
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||||
|
~stampSource: text
|
||||||
|
~stampExpression: DRAFT
|
||||||
|
~stampPages:
|
||||||
|
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||||
|
~rotateAngle: 90
|
||||||
|
~rotatePages:
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: my-file
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
72
.bruno/Chromium/Convert/URL to PDF.bru
Normal file
72
.bruno/Chromium/Convert/URL to PDF.bru
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
meta {
|
||||||
|
name: URL to PDF
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/chromium/convert/url
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
url: https://example.com
|
||||||
|
~landscape: false
|
||||||
|
~printBackground: false
|
||||||
|
~scale: 1.0
|
||||||
|
~singlePage: false
|
||||||
|
~paperWidth: 8.5
|
||||||
|
~paperHeight: 11
|
||||||
|
~marginTop: 0.39
|
||||||
|
~marginBottom: 0.39
|
||||||
|
~marginLeft: 0.39
|
||||||
|
~marginRight: 0.39
|
||||||
|
~nativePageRanges: 1-2
|
||||||
|
~preferCssPageSize: false
|
||||||
|
~generateDocumentOutline: false
|
||||||
|
~generateTaggedPdf: false
|
||||||
|
~skipNetworkIdleEvent: false
|
||||||
|
~failOnHttpStatusCodes: [499,599]
|
||||||
|
~failOnResourceHttpStatusCodes: []
|
||||||
|
~ignoreResourceHttpStatusDomains: []
|
||||||
|
~failOnResourceLoadingFailed: false
|
||||||
|
~failOnConsoleExceptions: false
|
||||||
|
~waitDelay: 0s
|
||||||
|
~waitWindowStatus:
|
||||||
|
~waitForExpression:
|
||||||
|
~waitForSelector:
|
||||||
|
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||||
|
~userAgent:
|
||||||
|
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||||
|
~emulatedMediaType: print
|
||||||
|
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||||
|
~omitBackground: false
|
||||||
|
~splitMode: intervals
|
||||||
|
~splitSpan: 1
|
||||||
|
~splitUnify: false
|
||||||
|
~pdfa: PDF/A-1b
|
||||||
|
~pdfua: true
|
||||||
|
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||||
|
~userPassword:
|
||||||
|
~ownerPassword:
|
||||||
|
~watermarkSource: text
|
||||||
|
~watermarkExpression: CONFIDENTIAL
|
||||||
|
~watermarkPages:
|
||||||
|
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||||
|
~stampSource: text
|
||||||
|
~stampExpression: DRAFT
|
||||||
|
~stampPages:
|
||||||
|
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||||
|
~rotateAngle: 90
|
||||||
|
~rotatePages:
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: my-file
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
46
.bruno/Chromium/Screenshot/HTML Screenshot.bru
Normal file
46
.bruno/Chromium/Screenshot/HTML Screenshot.bru
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
meta {
|
||||||
|
name: HTML Screenshot
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/chromium/screenshot/html
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page-1-html/index.html)
|
||||||
|
~width: 800
|
||||||
|
~height: 600
|
||||||
|
~clip: false
|
||||||
|
~format: png
|
||||||
|
~quality: 100
|
||||||
|
~optimizeForSpeed: false
|
||||||
|
~skipNetworkIdleEvent: false
|
||||||
|
~failOnHttpStatusCodes: [499,599]
|
||||||
|
~failOnResourceHttpStatusCodes: []
|
||||||
|
~ignoreResourceHttpStatusDomains: []
|
||||||
|
~failOnResourceLoadingFailed: false
|
||||||
|
~failOnConsoleExceptions: false
|
||||||
|
~waitDelay: 0s
|
||||||
|
~waitWindowStatus:
|
||||||
|
~waitForExpression:
|
||||||
|
~waitForSelector:
|
||||||
|
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||||
|
~userAgent:
|
||||||
|
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||||
|
~emulatedMediaType: screen
|
||||||
|
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||||
|
~omitBackground: false
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: my-screenshot
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
47
.bruno/Chromium/Screenshot/Markdown Screenshot.bru
Normal file
47
.bruno/Chromium/Screenshot/Markdown Screenshot.bru
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
meta {
|
||||||
|
name: Markdown Screenshot
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/chromium/screenshot/markdown
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page-1-markdown/index.html)
|
||||||
|
files: @file(../../test/integration/testdata/page-1-markdown/page_1.md)
|
||||||
|
~width: 800
|
||||||
|
~height: 600
|
||||||
|
~clip: false
|
||||||
|
~format: png
|
||||||
|
~quality: 100
|
||||||
|
~optimizeForSpeed: false
|
||||||
|
~skipNetworkIdleEvent: false
|
||||||
|
~failOnHttpStatusCodes: [499,599]
|
||||||
|
~failOnResourceHttpStatusCodes: []
|
||||||
|
~ignoreResourceHttpStatusDomains: []
|
||||||
|
~failOnResourceLoadingFailed: false
|
||||||
|
~failOnConsoleExceptions: false
|
||||||
|
~waitDelay: 0s
|
||||||
|
~waitWindowStatus:
|
||||||
|
~waitForExpression:
|
||||||
|
~waitForSelector:
|
||||||
|
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||||
|
~userAgent:
|
||||||
|
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||||
|
~emulatedMediaType: screen
|
||||||
|
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||||
|
~omitBackground: false
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: my-screenshot
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
46
.bruno/Chromium/Screenshot/URL Screenshot.bru
Normal file
46
.bruno/Chromium/Screenshot/URL Screenshot.bru
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
meta {
|
||||||
|
name: URL Screenshot
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/chromium/screenshot/url
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
url: https://example.com
|
||||||
|
~width: 800
|
||||||
|
~height: 600
|
||||||
|
~clip: false
|
||||||
|
~format: png
|
||||||
|
~quality: 100
|
||||||
|
~optimizeForSpeed: false
|
||||||
|
~skipNetworkIdleEvent: false
|
||||||
|
~failOnHttpStatusCodes: [499,599]
|
||||||
|
~failOnResourceHttpStatusCodes: []
|
||||||
|
~ignoreResourceHttpStatusDomains: []
|
||||||
|
~failOnResourceLoadingFailed: false
|
||||||
|
~failOnConsoleExceptions: false
|
||||||
|
~waitDelay: 0s
|
||||||
|
~waitWindowStatus:
|
||||||
|
~waitForExpression:
|
||||||
|
~waitForSelector:
|
||||||
|
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||||
|
~userAgent:
|
||||||
|
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||||
|
~emulatedMediaType: screen
|
||||||
|
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||||
|
~omitBackground: false
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: my-screenshot
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
11
.bruno/Health & Info/Debug.bru
Normal file
11
.bruno/Health & Info/Debug.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Debug
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/debug
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
11
.bruno/Health & Info/Health.bru
Normal file
11
.bruno/Health & Info/Health.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Health
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/health
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
11
.bruno/Health & Info/Prometheus Metrics.bru
Normal file
11
.bruno/Health & Info/Prometheus Metrics.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Prometheus Metrics
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/prometheus/metrics
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
11
.bruno/Health & Info/Version.bru
Normal file
11
.bruno/Health & Info/Version.bru
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
meta {
|
||||||
|
name: Version
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/version
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
74
.bruno/LibreOffice/Convert to PDF.bru
Normal file
74
.bruno/LibreOffice/Convert to PDF.bru
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
meta {
|
||||||
|
name: Convert to PDF
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/libreoffice/convert
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.docx)
|
||||||
|
~password:
|
||||||
|
~landscape: false
|
||||||
|
~nativePageRanges:
|
||||||
|
~updateIndexes: false
|
||||||
|
~exportFormFields: true
|
||||||
|
~allowDuplicateFieldNames: false
|
||||||
|
~exportBookmarks: true
|
||||||
|
~exportBookmarksToPdfDestination: false
|
||||||
|
~exportPlaceholders: false
|
||||||
|
~exportNotes: false
|
||||||
|
~exportNotesPages: false
|
||||||
|
~exportOnlyNotesPages: false
|
||||||
|
~exportNotesInMargin: false
|
||||||
|
~convertOooTargetToPdfTarget: false
|
||||||
|
~exportLinksRelativeFsys: false
|
||||||
|
~exportHiddenSlides: false
|
||||||
|
~skipEmptyPages: false
|
||||||
|
~addOriginalDocumentAsStream: false
|
||||||
|
~singlePageSheets: false
|
||||||
|
~losslessImageCompression: false
|
||||||
|
~quality: 90
|
||||||
|
~reduceImageResolution: false
|
||||||
|
~maxImageResolution: 300
|
||||||
|
~nativeWatermarkText:
|
||||||
|
~nativeWatermarkColor: 0
|
||||||
|
~nativeWatermarkFontHeight: 0
|
||||||
|
~nativeWatermarkRotateAngle: 0
|
||||||
|
~nativeWatermarkFontName:
|
||||||
|
~nativeTiledWatermarkText:
|
||||||
|
~nativePdfFormats: true
|
||||||
|
~merge: false
|
||||||
|
~flatten: false
|
||||||
|
~splitMode: intervals
|
||||||
|
~splitSpan: 1
|
||||||
|
~splitUnify: false
|
||||||
|
~pdfa: PDF/A-1b
|
||||||
|
~pdfua: true
|
||||||
|
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||||
|
~userPassword:
|
||||||
|
~ownerPassword:
|
||||||
|
~watermarkSource: text
|
||||||
|
~watermarkExpression: CONFIDENTIAL
|
||||||
|
~watermarkPages:
|
||||||
|
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||||
|
~stampSource: text
|
||||||
|
~stampExpression: DRAFT
|
||||||
|
~stampPages:
|
||||||
|
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||||
|
~rotateAngle: 90
|
||||||
|
~rotatePages:
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: my-file
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
23
.bruno/PDF Engines/Bookmarks/Read Bookmarks.bru
Normal file
23
.bruno/PDF Engines/Bookmarks/Read Bookmarks.bru
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
meta {
|
||||||
|
name: Read Bookmarks
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/bookmarks/read
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
25
.bruno/PDF Engines/Bookmarks/Write Bookmarks.bru
Normal file
25
.bruno/PDF Engines/Bookmarks/Write Bookmarks.bru
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
meta {
|
||||||
|
name: Write Bookmarks
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/bookmarks/write
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
bookmarks: [{"title":"Chapter 1","page":1,"children":[{"title":"Section 1.1","page":1}]}]
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: with-bookmarks
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
26
.bruno/PDF Engines/Convert/Convert PDF.bru
Normal file
26
.bruno/PDF Engines/Convert/Convert PDF.bru
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Convert PDF
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/convert
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
pdfa: PDF/A-1b
|
||||||
|
~pdfua: true
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: converted
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
26
.bruno/PDF Engines/Embed/Embed Files.bru
Normal file
26
.bruno/PDF Engines/Embed/Embed Files.bru
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Embed Files
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/embed
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
embeds: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
~downloadFrom: [{"url":"https://example.com/attachment.xml","embedded":true}]
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: with-embeds
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
26
.bruno/PDF Engines/Encrypt/Encrypt PDF.bru
Normal file
26
.bruno/PDF Engines/Encrypt/Encrypt PDF.bru
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Encrypt PDF
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/encrypt
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
userPassword: secret123
|
||||||
|
~ownerPassword: owner456
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: encrypted
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
24
.bruno/PDF Engines/Flatten/Flatten PDF.bru
Normal file
24
.bruno/PDF Engines/Flatten/Flatten PDF.bru
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
meta {
|
||||||
|
name: Flatten PDF
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/flatten
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: flattened
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
43
.bruno/PDF Engines/Merge/Merge PDFs.bru
Normal file
43
.bruno/PDF Engines/Merge/Merge PDFs.bru
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
meta {
|
||||||
|
name: Merge PDFs
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/merge
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
files: @file(../../test/integration/testdata/page_2.pdf)
|
||||||
|
~flatten: false
|
||||||
|
~autoIndexBookmarks: false
|
||||||
|
~pdfa: PDF/A-1b
|
||||||
|
~pdfua: true
|
||||||
|
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||||
|
~bookmarks: [{"title":"Page 1","page":1},{"title":"Page 2","page":2}]
|
||||||
|
~userPassword:
|
||||||
|
~ownerPassword:
|
||||||
|
~watermarkSource: text
|
||||||
|
~watermarkExpression: CONFIDENTIAL
|
||||||
|
~watermarkPages:
|
||||||
|
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||||
|
~stampSource: text
|
||||||
|
~stampExpression: DRAFT
|
||||||
|
~stampPages:
|
||||||
|
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||||
|
~rotateAngle: 90
|
||||||
|
~rotatePages:
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: merged
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
23
.bruno/PDF Engines/Metadata/Read Metadata.bru
Normal file
23
.bruno/PDF Engines/Metadata/Read Metadata.bru
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
meta {
|
||||||
|
name: Read Metadata
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/metadata/read
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
25
.bruno/PDF Engines/Metadata/Write Metadata.bru
Normal file
25
.bruno/PDF Engines/Metadata/Write Metadata.bru
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
meta {
|
||||||
|
name: Write Metadata
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/metadata/write
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
metadata: {"Author":"Bruno","Copyright":"Bruno","Creator":"Gotenberg","Keywords":["test"],"Marked":true,"Producer":"Gotenberg","Subject":"Test","Title":"Test Document","Trapped":"Unknown"}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: with-metadata
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
26
.bruno/PDF Engines/Rotate/Rotate PDF.bru
Normal file
26
.bruno/PDF Engines/Rotate/Rotate PDF.bru
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Rotate PDF
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/rotate
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
rotateAngle: 90
|
||||||
|
~rotatePages: 1-2
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: rotated
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
43
.bruno/PDF Engines/Split/Split PDF.bru
Normal file
43
.bruno/PDF Engines/Split/Split PDF.bru
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
meta {
|
||||||
|
name: Split PDF
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/split
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/pages_3.pdf)
|
||||||
|
splitMode: intervals
|
||||||
|
splitSpan: 1
|
||||||
|
~splitUnify: false
|
||||||
|
~flatten: false
|
||||||
|
~pdfa: PDF/A-1b
|
||||||
|
~pdfua: true
|
||||||
|
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||||
|
~userPassword:
|
||||||
|
~ownerPassword:
|
||||||
|
~watermarkSource: text
|
||||||
|
~watermarkExpression: CONFIDENTIAL
|
||||||
|
~watermarkPages:
|
||||||
|
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||||
|
~stampSource: text
|
||||||
|
~stampExpression: DRAFT
|
||||||
|
~stampPages:
|
||||||
|
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||||
|
~rotateAngle: 90
|
||||||
|
~rotatePages:
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: split
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
30
.bruno/PDF Engines/Stamp/Stamp PDF.bru
Normal file
30
.bruno/PDF Engines/Stamp/Stamp PDF.bru
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
meta {
|
||||||
|
name: Stamp PDF
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/stamp
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
stampSource: text
|
||||||
|
stampExpression: APPROVED
|
||||||
|
~stampPages: 1-2
|
||||||
|
~stampOptions: {"font":"Helvetica","fontSize":"48","color":"#00FF00","opacity":"0.5","rotation":"0"}
|
||||||
|
~stamp: @file(../../test/integration/testdata/watermark.png)
|
||||||
|
~downloadFrom: [{"url":"https://example.com/stamp.png","field":"stamp"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: stamped
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
30
.bruno/PDF Engines/Watermark/Watermark PDF.bru
Normal file
30
.bruno/PDF Engines/Watermark/Watermark PDF.bru
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
meta {
|
||||||
|
name: Watermark PDF
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/pdfengines/watermark
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||||
|
watermarkSource: text
|
||||||
|
watermarkExpression: CONFIDENTIAL
|
||||||
|
~watermarkPages: 1-2
|
||||||
|
~watermarkOptions: {"font":"Helvetica","fontSize":"48","color":"#FF0000","opacity":"0.3","rotation":"45"}
|
||||||
|
~watermark: @file(../../test/integration/testdata/watermark.png)
|
||||||
|
~downloadFrom: [{"url":"https://example.com/watermark.png","field":"watermark"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: watermarked
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
6
.bruno/bruno.json
Normal file
6
.bruno/bruno.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "Gotenberg",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": ["node_modules", ".git"]
|
||||||
|
}
|
||||||
3
.bruno/collection.bru
Normal file
3
.bruno/collection.bru
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
headers {
|
||||||
|
Gotenberg-Trace: {{traceId}}
|
||||||
|
}
|
||||||
4
.bruno/environments/Demo.bru
Normal file
4
.bruno/environments/Demo.bru
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
vars {
|
||||||
|
baseUrl: https://demo.gotenberg.dev
|
||||||
|
traceId: bruno-demo
|
||||||
|
}
|
||||||
4
.bruno/environments/Local.bru
Normal file
4
.bruno/environments/Local.bru
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
vars {
|
||||||
|
baseUrl: http://localhost:3000
|
||||||
|
traceId: bruno-local
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
# Statically checking Go source for errors and warnings.
|
|
||||||
gometalinter.v2 --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode -E gocyclo --vendor ./...;
|
|
||||||
|
|
||||||
# Running tests according to current Gotenberg version.
|
|
||||||
if [[ "$VERSION" == "snapshot" ]]; then
|
|
||||||
for d in $(go list ./... | grep -v vendor); do
|
|
||||||
go test -race -cover -covermode=atomic $d;
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "" > .ci/coverage.txt;
|
|
||||||
for d in $(go list ./... | grep -v vendor); do
|
|
||||||
go test -race -coverprofile=profile.out -covermode=atomic $d;
|
|
||||||
if [ -f profile.out ]; then
|
|
||||||
cat profile.out >> .ci/coverage.txt;
|
|
||||||
rm profile.out;
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Builds the Linux binary.
|
|
||||||
if [ -f .ci/gotenberg ]; then
|
|
||||||
rm .ci/gotenberg
|
|
||||||
fi
|
|
||||||
|
|
||||||
env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X main.version=${VERSION}" && mv gotenberg .ci/;
|
|
||||||
|
|
||||||
# Bye!
|
|
||||||
exit 0;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# The port the application will listen to.
|
|
||||||
port: 3000
|
|
||||||
|
|
||||||
logs:
|
|
||||||
# Accepted values, in order of severity: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
|
|
||||||
# Messages at and above the selected level will be logged.
|
|
||||||
level: "INFO"
|
|
||||||
|
|
||||||
# Accepted values: text, json.
|
|
||||||
# When a TTY is not attached, the output will be in the defined format.
|
|
||||||
formatter: "text"
|
|
||||||
|
|
||||||
# You don't like a library which is used for a conversion? You want to handle a new file type?
|
|
||||||
# You may provide here your own implementation!
|
|
||||||
commands:
|
|
||||||
|
|
||||||
# Some libraries like unoconv cannot perform concurrent conversions. That's why the API does only one conversion at a time.
|
|
||||||
# If your current implementation uses libraries which are able to perform concurrent conversions, you may
|
|
||||||
# change this value to false.
|
|
||||||
lock: true
|
|
||||||
|
|
||||||
# Unlike others commands' templates, you have access to FilesPaths instead of FilePath: it gathers all PDF files which should be merged.
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
|
|
||||||
# You may add more commands (or less, or even none).
|
|
||||||
conversions:
|
|
||||||
|
|
||||||
# The command template: you have access to FilePath and ResultFilePath variables.
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
# The binary which will call the command.
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
# Duration in seconds after which the command will be killed if it has not finished.
|
|
||||||
timeout: 30
|
|
||||||
# Files with the following extensions will be converted by the current command.
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.git
|
||||||
7
.env
Normal file
7
.env
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
GOTENBERG_VERSION=snapshot
|
||||||
|
DOCKER_REGISTRY=gotenberg
|
||||||
|
DOCKER_REPOSITORY=gotenberg
|
||||||
|
DOCKERFILE=build/Dockerfile
|
||||||
|
DOCKERFILE_CLOUDRUN=build/Dockerfile.cloudrun
|
||||||
|
DOCKERFILE_AWS_LAMBDA=build/Dockerfile.aws-lambda
|
||||||
|
DOCKER_BUILD_CONTEXT='.'
|
||||||
46
.github/CODE_OF_CONDUCT.md
vendored
46
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,46 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at neuhart.julien@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
|
||||||
|
|
||||||
[homepage]: http://contributor-covenant.org
|
|
||||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
||||||
33
.github/CONTRIBUTING.md
vendored
33
.github/CONTRIBUTING.md
vendored
@@ -1,33 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
Hi! Thank your for considering contributing to Gotenberg. You'll
|
|
||||||
find below useful information about how to contribute to the Gotenberg project.
|
|
||||||
|
|
||||||
## Contributing code
|
|
||||||
|
|
||||||
### Install from sources
|
|
||||||
|
|
||||||
1. Fork this repository
|
|
||||||
2. Clone it to your local Go environment (requires *Go* >= 1.10)
|
|
||||||
3. Install the latest [Orbit](https://github.com/gulien/orbit) release
|
|
||||||
|
|
||||||
### Working with git
|
|
||||||
|
|
||||||
1. Create your feature branch (`git checkout -b my-new-feature`)
|
|
||||||
2. Commit your changes (`git commit -am 'Add some feature'`)
|
|
||||||
3. Push to the branch (`git push origin my-new-feature`)
|
|
||||||
4. Create a new pull request
|
|
||||||
|
|
||||||
## Reporting bugs and feature request
|
|
||||||
|
|
||||||
Your issue or feature request may already be reported!
|
|
||||||
Please search on the [issue tracker](../../../issues) before creating one.
|
|
||||||
|
|
||||||
If you do not find any relevant issue or feature request, feel free to
|
|
||||||
add a new one!
|
|
||||||
|
|
||||||
## Additional resources
|
|
||||||
|
|
||||||
* [Code of conduct](CODE_OF_CONDUCT.md)
|
|
||||||
* [Issue template](ISSUE_TEMPLATE.md)
|
|
||||||
* [Pull request template](PULL_REQUEST_TEMPLATE.md)
|
|
||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: [gulien]
|
||||||
32
.github/ISSUE_TEMPLATE.md
vendored
32
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,32 +0,0 @@
|
|||||||
Your issue may already be reported!
|
|
||||||
Please search on the [issue tracker](../../../issues) before creating one.
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
<!--- If you're describing a bug, tell us what should happen -->
|
|
||||||
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
|
||||||
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
|
||||||
|
|
||||||
## Possible Solution
|
|
||||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
|
||||||
<!--- or ideas how to implement the addition or change -->
|
|
||||||
|
|
||||||
## Steps to Reproduce (for bugs)
|
|
||||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
|
||||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
4.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
|
||||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
|
||||||
|
|
||||||
## Your Environment
|
|
||||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
|
||||||
* Version used:
|
|
||||||
* Operating System and version:
|
|
||||||
* Link to your project:
|
|
||||||
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,44 +0,0 @@
|
|||||||
A similar PR may already be submitted!
|
|
||||||
Please search among the [pull requests](../../../pulls) before creating one.
|
|
||||||
|
|
||||||
Thanks for submitting a pull request! Please provide enough information so that others can review your pull request:
|
|
||||||
|
|
||||||
For more information, see the [CONTRIBUTING](CONTRIBUTING.md) guide.
|
|
||||||
|
|
||||||
**Summary**
|
|
||||||
|
|
||||||
<!-- Summary of the PR -->
|
|
||||||
|
|
||||||
This PR fixes/implements the following **bugs/features**
|
|
||||||
|
|
||||||
* [ ] Bug 1
|
|
||||||
* [ ] Bug 2
|
|
||||||
* [ ] Feature 1
|
|
||||||
* [ ] Feature 2
|
|
||||||
* [ ] Breaking changes
|
|
||||||
|
|
||||||
<!-- You can skip this if you're fixing a typo or adding an app to the Showcase. -->
|
|
||||||
|
|
||||||
Explain the **motivation** for making this change. What existing problem does the pull request solve?
|
|
||||||
|
|
||||||
<!-- Example: When "Adding a function to do X", explain why it is necessary to have a way to do X. -->
|
|
||||||
|
|
||||||
**Test plan (required)**
|
|
||||||
|
|
||||||
Demonstrate the code is solid. Example: The exact commands you ran and their output.
|
|
||||||
|
|
||||||
<!-- Make sure tests pass on both Travis and AppVeyor. -->
|
|
||||||
|
|
||||||
**Closing issues**
|
|
||||||
|
|
||||||
<!-- Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). -->
|
|
||||||
Fixes #
|
|
||||||
|
|
||||||
**Checklist**
|
|
||||||
|
|
||||||
- [ ] Have you followed the guidelines in our [CONTRIBUTING](CONTRIBUTING.md) guide?
|
|
||||||
- [ ] Have you lint your code locally prior to submission (`orbit run fmt`)?
|
|
||||||
- [ ] Have you written new tests for your core changes, as applicable?
|
|
||||||
- [ ] Have you successfully ran tests with your changes locally (`orbit run ci`)?
|
|
||||||
- [ ] I have squashed any insignificant commits
|
|
||||||
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code
|
|
||||||
106
.github/actions/build-test-push/action.yml
vendored
Normal file
106
.github/actions/build-test-push/action.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
name: Build Test Push
|
||||||
|
description: Build, test and push Docker images for a given platform
|
||||||
|
author: Julien Neuhart
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
github_token:
|
||||||
|
description: The GitHub token
|
||||||
|
required: true
|
||||||
|
default: ${{ github.token }}
|
||||||
|
docker_hub_username:
|
||||||
|
description: The Docker Hub username
|
||||||
|
required: true
|
||||||
|
docker_hub_password:
|
||||||
|
description: The Docker Hub password
|
||||||
|
required: true
|
||||||
|
platform:
|
||||||
|
description: linux/amd64, linux/ppc64le, linux/386, linux/arm64, linux/arm/v7
|
||||||
|
required: true
|
||||||
|
version:
|
||||||
|
description: Gotenberg version
|
||||||
|
required: true
|
||||||
|
skip_integrations_tests:
|
||||||
|
description: Define whether to skip integration testing
|
||||||
|
default: false
|
||||||
|
alternate_repository:
|
||||||
|
description: Alternate repository to push the tags to
|
||||||
|
dry_run:
|
||||||
|
description: Dry run this action
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
tags:
|
||||||
|
description: Comma separated list of tag
|
||||||
|
value: ${{ steps.build.outputs.tags }}
|
||||||
|
tags_cloud_run:
|
||||||
|
description: Comma separated list of Cloud Run tags (linux/amd64 only)
|
||||||
|
value: ${{ steps.build.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda:
|
||||||
|
description: Comma separated list of AWS Lambda tags (linux/amd64 and linux/arm64 only)
|
||||||
|
value: ${{ steps.build.outputs.tags_aws_lambda }}
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
if: inputs.docker_hub_username != ''
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ inputs.docker_hub_username }}
|
||||||
|
password: ${{ inputs.docker_hub_password }}
|
||||||
|
|
||||||
|
- name: Build ${{ inputs.platform }}
|
||||||
|
id: build
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_VERSION: ${{ inputs.version }}
|
||||||
|
INPUT_PLATFORM: ${{ inputs.platform }}
|
||||||
|
INPUT_ALTERNATE_REPOSITORY: ${{ inputs.alternate_repository }}
|
||||||
|
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
run: |
|
||||||
|
.github/actions/build-test-push/build.sh \
|
||||||
|
--version "$INPUT_VERSION" \
|
||||||
|
--platform "$INPUT_PLATFORM" \
|
||||||
|
--alternate-repository "$INPUT_ALTERNATE_REPOSITORY" \
|
||||||
|
--dry-run "$INPUT_DRY_RUN"
|
||||||
|
|
||||||
|
- name: Run integration tests
|
||||||
|
if: inputs.skip_integrations_tests != 'true'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_VERSION: ${{ inputs.version }}
|
||||||
|
INPUT_PLATFORM: ${{ inputs.platform }}
|
||||||
|
INPUT_ALTERNATE_REPOSITORY: ${{ inputs.alternate_repository }}
|
||||||
|
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
run: |
|
||||||
|
.github/actions/build-test-push/test.sh \
|
||||||
|
--version "$INPUT_VERSION" \
|
||||||
|
--platform "$INPUT_PLATFORM" \
|
||||||
|
--alternate-repository "$INPUT_ALTERNATE_REPOSITORY" \
|
||||||
|
--dry-run "$INPUT_DRY_RUN"
|
||||||
|
|
||||||
|
- name: Push
|
||||||
|
if: inputs.docker_hub_username != ''
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_TAGS: ${{ steps.build.outputs.tags }},${{ steps.build.outputs.tags_cloud_run }},${{ steps.build.outputs.tags_aws_lambda }}
|
||||||
|
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
run: |
|
||||||
|
.github/actions/build-test-push/push.sh \
|
||||||
|
--tags "$INPUT_TAGS" \
|
||||||
|
--dry-run "$INPUT_DRY_RUN"
|
||||||
|
|
||||||
|
- name: Outputs
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "tags=${{ steps.build.outputs.tags }}"
|
||||||
|
echo "tags_cloud_run=${{ steps.build.outputs.tags_cloud_run }}"
|
||||||
|
echo "tags_aws_lambda=${{ steps.build.outputs.tags_aws_lambda }}"
|
||||||
184
.github/actions/build-test-push/build.sh
vendored
Executable file
184
.github/actions/build-test-push/build.sh
vendored
Executable file
@@ -0,0 +1,184 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit early.
|
||||||
|
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Source dot env file.
|
||||||
|
source .env
|
||||||
|
|
||||||
|
# Arguments.
|
||||||
|
version=""
|
||||||
|
platform=""
|
||||||
|
alternate_repository=""
|
||||||
|
dry_run=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--version)
|
||||||
|
version="${2//v/}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--platform)
|
||||||
|
platform="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--alternate-repository)
|
||||||
|
alternate_repository="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
dry_run="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Build and push 👷"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "Gotenberg version: $version"
|
||||||
|
echo "Target platform: $platform"
|
||||||
|
|
||||||
|
if [ -n "$alternate_repository" ]; then
|
||||||
|
DOCKER_REPOSITORY=$alternate_repository
|
||||||
|
echo "⚠️ Using $alternate_repository for DOCKER_REPOSITORY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build tags arrays.
|
||||||
|
tags=()
|
||||||
|
tags_cloud_run=()
|
||||||
|
tags_aws_lambda=()
|
||||||
|
|
||||||
|
IFS='/' read -ra arch <<< "$platform"
|
||||||
|
IFS='.' read -ra semver <<< "$version"
|
||||||
|
|
||||||
|
if [ "${#semver[@]}" -eq 3 ]; then
|
||||||
|
echo
|
||||||
|
echo "Semver version detected"
|
||||||
|
|
||||||
|
major="${semver[0]}"
|
||||||
|
minor="${semver[1]}"
|
||||||
|
patch="${semver[2]}"
|
||||||
|
|
||||||
|
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest-${arch[1]}")
|
||||||
|
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major-${arch[1]}")
|
||||||
|
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor-${arch[1]}")
|
||||||
|
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor.$patch-${arch[1]}")
|
||||||
|
|
||||||
|
if [ "$platform" = "linux/amd64" ]; then
|
||||||
|
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest-cloudrun")
|
||||||
|
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major-cloudrun")
|
||||||
|
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor-cloudrun")
|
||||||
|
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor.$patch-cloudrun")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$platform" = "linux/amd64" ] || [ "$platform" = "linux/arm64" ]; then
|
||||||
|
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest-aws-lambda-${arch[1]}")
|
||||||
|
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major-aws-lambda-${arch[1]}")
|
||||||
|
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor-aws-lambda-${arch[1]}")
|
||||||
|
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor.$patch-aws-lambda-${arch[1]}")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo "Non-semver version detected, fallback to $version"
|
||||||
|
|
||||||
|
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-${arch[1]}")
|
||||||
|
if [ "$platform" = "linux/amd64" ]; then
|
||||||
|
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-cloudrun")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$platform" = "linux/amd64" ] || [ "$platform" = "linux/arm64" ]; then
|
||||||
|
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-aws-lambda-${arch[1]}")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
tags_flags=()
|
||||||
|
tags_cloud_run_flags=()
|
||||||
|
tags_aws_lambda_flags=()
|
||||||
|
|
||||||
|
echo "Will use the following tags:"
|
||||||
|
for tag in "${tags[@]}"; do
|
||||||
|
tags_flags+=("-t" "$tag")
|
||||||
|
echo "- $tag"
|
||||||
|
done
|
||||||
|
for tag in "${tags_cloud_run[@]}"; do
|
||||||
|
tags_cloud_run_flags+=("-t" "$tag")
|
||||||
|
echo "- $tag"
|
||||||
|
done
|
||||||
|
for tag in "${tags_aws_lambda[@]}"; do
|
||||||
|
tags_aws_lambda_flags+=("-t" "$tag")
|
||||||
|
echo "- $tag"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Build images.
|
||||||
|
run_cmd() {
|
||||||
|
local cmd="$1"
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run - would run the following command:"
|
||||||
|
echo "$cmd"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "⚙️ Running command:"
|
||||||
|
echo "$cmd"
|
||||||
|
eval "$cmd"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
join() {
|
||||||
|
local delimiter="$1"
|
||||||
|
shift
|
||||||
|
local IFS="$delimiter"
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
no_arch_tag="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version"
|
||||||
|
|
||||||
|
cmd="docker buildx build \
|
||||||
|
--build-arg GOTENBERG_VERSION=$version \
|
||||||
|
--platform $platform \
|
||||||
|
--load \
|
||||||
|
${tags_flags[*]} \
|
||||||
|
-t $no_arch_tag \
|
||||||
|
-f $DOCKERFILE $DOCKER_BUILD_CONTEXT
|
||||||
|
"
|
||||||
|
run_cmd "$cmd"
|
||||||
|
|
||||||
|
if [ "$platform" = "linux/amd64" ]; then
|
||||||
|
cmd="docker build \
|
||||||
|
--build-arg DOCKER_REGISTRY=$DOCKER_REGISTRY \
|
||||||
|
--build-arg DOCKER_REPOSITORY=$DOCKER_REPOSITORY \
|
||||||
|
--build-arg GOTENBERG_VERSION=$version \
|
||||||
|
${tags_cloud_run_flags[*]} \
|
||||||
|
-f $DOCKERFILE_CLOUDRUN $DOCKER_BUILD_CONTEXT
|
||||||
|
"
|
||||||
|
run_cmd "$cmd"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$platform" = "linux/amd64" ] || [ "$platform" = "linux/arm64" ]; then
|
||||||
|
cmd="docker build \
|
||||||
|
--build-arg DOCKER_REGISTRY=$DOCKER_REGISTRY \
|
||||||
|
--build-arg DOCKER_REPOSITORY=$DOCKER_REPOSITORY \
|
||||||
|
--build-arg GOTENBERG_VERSION=$version \
|
||||||
|
${tags_aws_lambda_flags[*]} \
|
||||||
|
-f $DOCKERFILE_AWS_LAMBDA $DOCKER_BUILD_CONTEXT
|
||||||
|
"
|
||||||
|
run_cmd "$cmd"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Done!"
|
||||||
|
echo "tags=$(join "," "${tags[@]}")" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tags_cloud_run=$(join "," "${tags_cloud_run[@]}")" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tags_aws_lambda=$(join "," "${tags_aws_lambda[@]}")" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
76
.github/actions/build-test-push/push.sh
vendored
Executable file
76
.github/actions/build-test-push/push.sh
vendored
Executable file
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit early.
|
||||||
|
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Source dot env file.
|
||||||
|
source .env
|
||||||
|
|
||||||
|
# Arguments.
|
||||||
|
tags=""
|
||||||
|
dry_run=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--tags)
|
||||||
|
tags="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
dry_run=$2
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Push tag(s) 📦"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "Tag(s) to push:"
|
||||||
|
IFS=',' read -ra tmp_tags_to_push <<< "$tags"
|
||||||
|
|
||||||
|
tags_to_push=()
|
||||||
|
for tag in "${tmp_tags_to_push[@]}"; do
|
||||||
|
[ -n "$tag" ] && tags_to_push+=("$tag")
|
||||||
|
done
|
||||||
|
|
||||||
|
for tag in "${tags_to_push[@]}"; do
|
||||||
|
echo "- $tag"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Push tags.
|
||||||
|
run_cmd() {
|
||||||
|
local cmd="$1"
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run - would run the following command:"
|
||||||
|
echo "$cmd"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "⚙️ Running command:"
|
||||||
|
echo "$cmd"
|
||||||
|
eval "$cmd"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in "${tags_to_push[@]}"; do
|
||||||
|
cmd="docker push $tag"
|
||||||
|
run_cmd "$cmd"
|
||||||
|
|
||||||
|
echo "➡️ $tag pushed"
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ Done!"
|
||||||
|
exit 0
|
||||||
79
.github/actions/build-test-push/test.sh
vendored
Executable file
79
.github/actions/build-test-push/test.sh
vendored
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit early.
|
||||||
|
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Source dot env file.
|
||||||
|
source .env
|
||||||
|
|
||||||
|
# Arguments.
|
||||||
|
version=""
|
||||||
|
platform=""
|
||||||
|
alternate_repository=""
|
||||||
|
dry_run=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--version)
|
||||||
|
version="${2//v/}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--platform)
|
||||||
|
platform="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--alternate-repository)
|
||||||
|
alternate_repository="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
dry_run="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Integration testing 🧪"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "Gotenberg version: $version"
|
||||||
|
echo "Target platform: $platform"
|
||||||
|
|
||||||
|
repository=$DOCKER_REPOSITORY
|
||||||
|
|
||||||
|
if [ -n "$alternate_repository" ]; then
|
||||||
|
echo "⚠️ Using $alternate_repository for DOCKER_REPOSITORY"
|
||||||
|
repository=$alternate_repository
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Test image.
|
||||||
|
run_cmd() {
|
||||||
|
local cmd="$1"
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run - would run the following command:"
|
||||||
|
echo "$cmd"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "⚙️ Running command:"
|
||||||
|
echo "$cmd"
|
||||||
|
eval "$cmd"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="make test-integration DOCKER_REPOSITORY=$repository GOTENBERG_VERSION=$version PLATFORM=$platform NO_CONCURRENCY=true"
|
||||||
|
run_cmd "$cmd"
|
||||||
|
|
||||||
|
echo "✅ Done!"
|
||||||
|
exit 0
|
||||||
34
.github/actions/clean/action.yml
vendored
Normal file
34
.github/actions/clean/action.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Clean
|
||||||
|
description: Clean tags from Docker Hub
|
||||||
|
author: Julien Neuhart
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
docker_hub_username:
|
||||||
|
description: The Docker Hub username
|
||||||
|
required: true
|
||||||
|
docker_hub_password:
|
||||||
|
description: The Docker Hub password
|
||||||
|
required: true
|
||||||
|
tags:
|
||||||
|
description: Comma separated list of tags to clean
|
||||||
|
snapshot_version:
|
||||||
|
description: Snapshot version to clean
|
||||||
|
dry_run:
|
||||||
|
description: Dry run this action
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Clean tags from Docker Hub
|
||||||
|
env:
|
||||||
|
DOCKERHUB_USERNAME: ${{ inputs.docker_hub_username }}
|
||||||
|
DOCKERHUB_TOKEN: ${{ inputs.docker_hub_password }}
|
||||||
|
INPUT_TAGS: ${{ inputs.tags }}
|
||||||
|
INPUT_SNAPSHOT_VERSION: ${{ inputs.snapshot_version }}
|
||||||
|
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
.github/actions/clean/clean.sh \
|
||||||
|
--tags "$INPUT_TAGS" \
|
||||||
|
--snapshot-version "$INPUT_SNAPSHOT_VERSION" \
|
||||||
|
--dry-run "$INPUT_DRY_RUN"
|
||||||
123
.github/actions/clean/clean.sh
vendored
Executable file
123
.github/actions/clean/clean.sh
vendored
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit early.
|
||||||
|
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Source dot env file.
|
||||||
|
source .env
|
||||||
|
|
||||||
|
# Arguments.
|
||||||
|
tags=""
|
||||||
|
snapshot_version=""
|
||||||
|
dry_run=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--tags)
|
||||||
|
tags="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--snapshot-version)
|
||||||
|
snapshot_version="${2//v/}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
dry_run="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Clean tag(s) from Docker Hub 🧹"
|
||||||
|
echo
|
||||||
|
|
||||||
|
IFS=',' read -ra tags_to_delete <<< "$tags"
|
||||||
|
if [ -n "$snapshot_version" ]; then
|
||||||
|
tags_to_delete+=("$DOCKER_REGISTRY/snapshot:$snapshot_version")
|
||||||
|
tags_to_delete+=("$DOCKER_REGISTRY/snapshot:$snapshot_version-cloudrun")
|
||||||
|
tags_to_delete+=("$DOCKER_REGISTRY/snapshot:$snapshot_version-aws-lambda")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Will delete the following tag(s):"
|
||||||
|
for tag in "${tags_to_delete[@]}"; do
|
||||||
|
echo "- $tag"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Delete tags.
|
||||||
|
base_url="https://hub.docker.com/v2"
|
||||||
|
token=""
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
token="placeholder"
|
||||||
|
echo "🚧 Dry run - would call $base_url to get a token"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "🌐 Get token from $base_url"
|
||||||
|
|
||||||
|
readarray -t lines < <(
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\":\"$DOCKERHUB_USERNAME\", \"password\":\"$DOCKERHUB_TOKEN\"}" \
|
||||||
|
-w "\n%{http_code}" \
|
||||||
|
"$base_url/users/login"
|
||||||
|
)
|
||||||
|
|
||||||
|
http_code="${lines[-1]}"
|
||||||
|
unset 'lines[-1]'
|
||||||
|
json_body=$(printf "%s\n" "${lines[@]}")
|
||||||
|
|
||||||
|
if [ "$http_code" -ne "200" ]; then
|
||||||
|
echo "❌ Wrong HTTP status - $http_code"
|
||||||
|
echo "$json_body"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
token=$(jq -r '.token' <<< "$json_body")
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$token" ]; then
|
||||||
|
echo "❌ No token from Docker Hub"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for tag in "${tags_to_delete[@]}"; do
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run - would call $base_url to delete tag $tag"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "🌐 Delete tag $tag"
|
||||||
|
IFS=':' read -ra tag_parts <<< "$tag"
|
||||||
|
|
||||||
|
readarray -t lines < <(
|
||||||
|
curl -s -X DELETE \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-w "\n%{http_code}" \
|
||||||
|
"$base_url/repositories/${tag_parts[0]}/tags/${tag_parts[1]}/"
|
||||||
|
)
|
||||||
|
|
||||||
|
http_code="${lines[-1]}"
|
||||||
|
unset 'lines[-1]'
|
||||||
|
|
||||||
|
if [ "$http_code" -ne "200" ] && [ "$http_code" -ne "204" ]; then
|
||||||
|
echo "❌ Wrong HTTP status - $http_code"
|
||||||
|
printf '%s\n' "${lines[@]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ Done!"
|
||||||
|
exit 0
|
||||||
52
.github/actions/merge/action.yml
vendored
Normal file
52
.github/actions/merge/action.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: Merge
|
||||||
|
description: Merge tags to single multi-platform tags
|
||||||
|
author: Julien Neuhart
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
github_token:
|
||||||
|
description: The GitHub token
|
||||||
|
required: true
|
||||||
|
default: ${{ github.token }}
|
||||||
|
docker_hub_username:
|
||||||
|
description: The Docker Hub username
|
||||||
|
required: true
|
||||||
|
docker_hub_password:
|
||||||
|
description: The Docker Hub password
|
||||||
|
required: true
|
||||||
|
tags:
|
||||||
|
description: Comma separated tags to merge
|
||||||
|
required: true
|
||||||
|
alternate_registry:
|
||||||
|
description: Alternate registry to also push resulting tags
|
||||||
|
dry_run:
|
||||||
|
description: Dry run this action
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ inputs.docker_hub_username }}
|
||||||
|
password: ${{ inputs.docker_hub_password }}
|
||||||
|
|
||||||
|
- name: Merge
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_TAGS: ${{ inputs.tags }}
|
||||||
|
INPUT_ALTERNATE_REGISTRY: ${{ inputs.alternate_registry }}
|
||||||
|
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
run: |
|
||||||
|
.github/actions/merge/merge.sh \
|
||||||
|
--tags "$INPUT_TAGS" \
|
||||||
|
--alternate-registry "$INPUT_ALTERNATE_REGISTRY" \
|
||||||
|
--dry-run "$INPUT_DRY_RUN"
|
||||||
108
.github/actions/merge/merge.sh
vendored
Executable file
108
.github/actions/merge/merge.sh
vendored
Executable file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit early.
|
||||||
|
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Source dot env file.
|
||||||
|
source .env
|
||||||
|
|
||||||
|
# Arguments.
|
||||||
|
tags=""
|
||||||
|
alternate_registry=""
|
||||||
|
dry_run=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--tags)
|
||||||
|
tags="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--alternate-registry)
|
||||||
|
alternate_registry="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
dry_run=$2
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Merge tag(s) 👷"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "Tag(s) to merge:"
|
||||||
|
IFS=',' read -ra tags_to_merge <<< "$tags"
|
||||||
|
for tag in "${tags_to_merge[@]}"; do
|
||||||
|
echo "- $tag"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$alternate_registry" ]; then
|
||||||
|
echo "⚠️ Will also push to $alternate_registry registry"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Build merge map.
|
||||||
|
declare -A merge_map
|
||||||
|
|
||||||
|
for tag in "${tags_to_merge[@]}"; do
|
||||||
|
target_tag="${tag//-amd64/}"
|
||||||
|
target_tag="${target_tag//-ppc64le/}"
|
||||||
|
target_tag="${target_tag//-arm64/}"
|
||||||
|
target_tag="${target_tag//-arm/}"
|
||||||
|
target_tag="${target_tag//-386/}"
|
||||||
|
|
||||||
|
merge_map["$target_tag"]+="$tag "
|
||||||
|
done
|
||||||
|
|
||||||
|
# Merge tags.
|
||||||
|
run_cmd() {
|
||||||
|
local cmd="$1"
|
||||||
|
|
||||||
|
if [ "$dry_run" = "true" ]; then
|
||||||
|
echo "🚧 Dry run - would run the following command:"
|
||||||
|
echo "$cmd"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "⚙️ Running command:"
|
||||||
|
echo "$cmd"
|
||||||
|
eval "$cmd"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for target in "${!merge_map[@]}"; do
|
||||||
|
IFS=' ' read -ra source_tags <<< "${merge_map[$target]}"
|
||||||
|
|
||||||
|
cmd="docker buildx imagetools create \
|
||||||
|
-t $target \
|
||||||
|
${source_tags[*]}
|
||||||
|
"
|
||||||
|
run_cmd "$cmd"
|
||||||
|
|
||||||
|
echo "➡️ $target pushed"
|
||||||
|
echo
|
||||||
|
if [ -n "$alternate_registry" ]; then
|
||||||
|
alternate_target="${target/$DOCKER_REGISTRY/$alternate_registry}"
|
||||||
|
cmd="docker buildx imagetools create \
|
||||||
|
-t $alternate_target \
|
||||||
|
$target
|
||||||
|
"
|
||||||
|
run_cmd "$cmd"
|
||||||
|
|
||||||
|
echo "➡️ $alternate_target pushed"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ Done!"
|
||||||
|
exit 0
|
||||||
18
.github/dependabot.yml
vendored
Normal file
18
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/build"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
18
.github/stale.yml
vendored
Normal file
18
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 7
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 3
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- bug
|
||||||
|
- documentation
|
||||||
|
- enhancement
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: wontfix
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
for your contributions.
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: false
|
||||||
150
.github/workflows/continuous-delivery.yml
vendored
Normal file
150
.github/workflows/continuous-delivery.yml
vendored
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
name: Continuous Delivery
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release_amd64:
|
||||||
|
name: Release linux/amd64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
platform: linux/amd64
|
||||||
|
skip_integrations_tests: true
|
||||||
|
|
||||||
|
release_386:
|
||||||
|
name: Release linux/386
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
platform: linux/386
|
||||||
|
skip_integrations_tests: true
|
||||||
|
|
||||||
|
release_ppc64le:
|
||||||
|
name: Release linux/ppc64le
|
||||||
|
runs-on: ubuntu-24.04-ppc64le
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
platform: linux/ppc64le
|
||||||
|
skip_integrations_tests: true
|
||||||
|
|
||||||
|
release_arm64:
|
||||||
|
name: Release linux/arm64
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
platform: linux/arm64
|
||||||
|
skip_integrations_tests: true
|
||||||
|
|
||||||
|
release_arm_v7:
|
||||||
|
name: Release linux/arm/v7
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
platform: linux/arm/v7
|
||||||
|
skip_integrations_tests: true
|
||||||
|
|
||||||
|
merge_clean_release_tags:
|
||||||
|
needs:
|
||||||
|
- release_amd64
|
||||||
|
- release_386
|
||||||
|
- release_ppc64le
|
||||||
|
- release_arm64
|
||||||
|
- release_arm_v7
|
||||||
|
name: Merge and clean release tags
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Merge
|
||||||
|
uses: ./.github/actions/merge
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_ppc64le.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }}"
|
||||||
|
alternate_registry: thecodingmachine
|
||||||
|
|
||||||
|
- name: Merge AWS Lambda
|
||||||
|
uses: ./.github/actions/merge
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
tags: "${{ needs.release_amd64.outputs.tags_aws_lambda }},${{ needs.release_arm64.outputs.tags_aws_lambda }}"
|
||||||
|
alternate_registry: thecodingmachine
|
||||||
|
|
||||||
|
- name: Clean
|
||||||
|
uses: ./.github/actions/clean
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_ppc64le.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }},${{ needs.release_amd64.outputs.tags_aws_lambda }},${{ needs.release_arm64.outputs.tags_aws_lambda }}"
|
||||||
398
.github/workflows/continuous-integration.yml
vendored
Normal file
398
.github/workflows/continuous-integration.yml
vendored
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
name: Continuous Integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ (github.event_name == 'pull_request' && github.event.pull_request.number) || 'main' }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint Golang codebase
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Run linters
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: v2.10.1
|
||||||
|
|
||||||
|
lint-prettier:
|
||||||
|
name: Lint non-Golang codebase
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: .node-version
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm i
|
||||||
|
|
||||||
|
- name: Run linters
|
||||||
|
run: make lint-prettier
|
||||||
|
|
||||||
|
test-unit:
|
||||||
|
needs:
|
||||||
|
- lint
|
||||||
|
- lint-prettier
|
||||||
|
name: Run unit tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: make test-unit
|
||||||
|
|
||||||
|
snapshot_amd64:
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Snapshot linux/amd64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: pr-${{ github.event.pull_request.number }}
|
||||||
|
platform: linux/amd64
|
||||||
|
alternate_repository: snapshot
|
||||||
|
|
||||||
|
snapshot_ppc64le:
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Snapshot linux/ppc64le
|
||||||
|
runs-on: ubuntu-24.04-ppc64le
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: pr-${{ github.event.pull_request.number }}
|
||||||
|
platform: linux/ppc64le
|
||||||
|
alternate_repository: snapshot
|
||||||
|
|
||||||
|
snapshot_386:
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Snapshot linux/386
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: pr-${{ github.event.pull_request.number }}
|
||||||
|
platform: linux/386
|
||||||
|
alternate_repository: snapshot
|
||||||
|
|
||||||
|
snapshot_arm64:
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Snapshot linux/arm64
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: pr-${{ github.event.pull_request.number }}
|
||||||
|
platform: linux/arm64
|
||||||
|
alternate_repository: snapshot
|
||||||
|
|
||||||
|
snapshot_arm_v7:
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Snapshot linux/arm/v7
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: pr-${{ github.event.pull_request.number }}
|
||||||
|
platform: linux/arm/v7
|
||||||
|
alternate_repository: snapshot
|
||||||
|
|
||||||
|
merge_clean_snapshot_guard:
|
||||||
|
needs:
|
||||||
|
- snapshot_amd64
|
||||||
|
- snapshot_386
|
||||||
|
- snapshot_ppc64le
|
||||||
|
- snapshot_arm64
|
||||||
|
- snapshot_arm_v7
|
||||||
|
name: Secrets access check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
continue: ${{ steps.check.outputs.continue }}
|
||||||
|
steps:
|
||||||
|
- name: Check
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ secrets.DOCKERHUB_USERNAME }}" ]; then
|
||||||
|
echo "continue=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "continue=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
merge_clean_snapshot_tags:
|
||||||
|
if: needs.merge_clean_snapshot_guard.outputs.continue == 'true'
|
||||||
|
needs:
|
||||||
|
- merge_clean_snapshot_guard
|
||||||
|
- snapshot_amd64
|
||||||
|
- snapshot_386
|
||||||
|
- snapshot_ppc64le
|
||||||
|
- snapshot_arm64
|
||||||
|
- snapshot_arm_v7
|
||||||
|
name: Merge and clean snapshot tags
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Merge
|
||||||
|
uses: ./.github/actions/merge
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_ppc64le.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }}"
|
||||||
|
|
||||||
|
- name: Merge AWS Lambda
|
||||||
|
uses: ./.github/actions/merge
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
tags: "${{ needs.snapshot_amd64.outputs.tags_aws_lambda }},${{ needs.snapshot_arm64.outputs.tags_aws_lambda }}"
|
||||||
|
|
||||||
|
- name: Clean
|
||||||
|
uses: ./.github/actions/clean
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_ppc64le.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }},${{ needs.snapshot_amd64.outputs.tags_aws_lambda }},${{ needs.snapshot_arm64.outputs.tags_aws_lambda }}"
|
||||||
|
|
||||||
|
edge_amd64:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Edge linux/amd64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: edge
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
|
edge_386:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Edge linux/386
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: edge
|
||||||
|
platform: linux/386
|
||||||
|
|
||||||
|
edge_ppc64le:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Edge linux/ppc64le
|
||||||
|
runs-on: ubuntu-24.04-ppc64le
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: edge
|
||||||
|
platform: linux/ppc64le
|
||||||
|
|
||||||
|
edge_arm64:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Edge linux/arm64
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: edge
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
edge_arm_v7:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
name: Edge linux/arm/v7
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.build_test_push.outputs.tags }}
|
||||||
|
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
|
||||||
|
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build, test and push
|
||||||
|
id: build_test_push
|
||||||
|
uses: ./.github/actions/build-test-push
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
version: edge
|
||||||
|
platform: linux/arm/v7
|
||||||
|
|
||||||
|
merge_clean_edge_tags:
|
||||||
|
needs:
|
||||||
|
- edge_amd64
|
||||||
|
- edge_386
|
||||||
|
- edge_ppc64le
|
||||||
|
- edge_arm64
|
||||||
|
- edge_arm_v7
|
||||||
|
name: Merge and clean edge tags
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Merge
|
||||||
|
uses: ./.github/actions/merge
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_ppc64le.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }}"
|
||||||
|
alternate_registry: thecodingmachine
|
||||||
|
|
||||||
|
- name: Merge AWS Lambda
|
||||||
|
uses: ./.github/actions/merge
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
tags: "${{ needs.edge_amd64.outputs.tags_aws_lambda }},${{ needs.edge_arm64.outputs.tags_aws_lambda }}"
|
||||||
|
alternate_registry: thecodingmachine
|
||||||
|
|
||||||
|
- name: Clean
|
||||||
|
uses: ./.github/actions/clean
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_ppc64le.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }},${{ needs.edge_amd64.outputs.tags_aws_lambda }},${{ needs.edge_arm64.outputs.tags_aws_lambda }}"
|
||||||
24
.github/workflows/pull-request-cleanup.yml
vendored
Normal file
24
.github/workflows/pull-request-cleanup.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Pull Request Cleanup
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
name: Cleanup Docker images
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
uses: ./.github/actions/clean
|
||||||
|
with:
|
||||||
|
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
snapshot_version: pr-${{ github.event.pull_request.number }}
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
.ci/coverage.txt
|
.idea
|
||||||
.ci/gotenberg
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
|||||||
65
.golangci.yml
Normal file
65
.golangci.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
issues-exit-code: 1
|
||||||
|
tests: false
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bidichk
|
||||||
|
- bodyclose
|
||||||
|
- copyloopvar
|
||||||
|
- decorder
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- dupword
|
||||||
|
- durationcheck
|
||||||
|
- errcheck
|
||||||
|
- errname
|
||||||
|
- exhaustive
|
||||||
|
- gosec
|
||||||
|
- govet
|
||||||
|
- importas
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- prealloc
|
||||||
|
- promlinter
|
||||||
|
- staticcheck
|
||||||
|
- testableexamples
|
||||||
|
- tparallel
|
||||||
|
- unconvert
|
||||||
|
- unused
|
||||||
|
- usetesting
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gci
|
||||||
|
- gofmt
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
settings:
|
||||||
|
gci:
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- default
|
||||||
|
- prefix(github.com/gotenberg/gotenberg/v8)
|
||||||
|
custom-order: true
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
project_name: Gotenberg
|
|
||||||
builds:
|
|
||||||
- main: ./main.go
|
|
||||||
binary: gotenberg
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
- darwin
|
|
||||||
- windows
|
|
||||||
goarch:
|
|
||||||
- 386
|
|
||||||
- amd64
|
|
||||||
- arm
|
|
||||||
- arm64
|
|
||||||
archive:
|
|
||||||
name_template: '{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
|
||||||
replacements:
|
|
||||||
darwin: Darwin
|
|
||||||
linux: Linux
|
|
||||||
windows: Windows
|
|
||||||
386: i386
|
|
||||||
amd64: x86_64
|
|
||||||
format: tar.gz
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
format: zip
|
|
||||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
24.11.0
|
||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
build
|
||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-gherkin", "prettier-plugin-sh"],
|
||||||
|
"escapeBackslashes": true
|
||||||
|
}
|
||||||
23
.travis.yml
23
.travis.yml
@@ -1,23 +0,0 @@
|
|||||||
sudo: required
|
|
||||||
|
|
||||||
language: generic
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- secure: d6pliM7iR7KJ3TQkr1C0tHi0HgU6Vdmc/ycw11XrXs794/dgj6g9UDrhgcvT2mTzOVIUNouHx7gyEWAlwJv7p8PPyguHJfIsyqMWfaXNPoytTx1ETUiygl1G/UsvXVdry6I5tZF/U+/PJ3hBXoAvo8EOFe0/MxxvP3ghCeeCpeEwEdoYP2jbADGffywVUNI7OEdmS29A28GusXXp2cWCIZYSq5Gn5iu1ZMPaJA6SkRTXpS+iVVLUvlbz5HW09ETFfurTvsdz5Ze4JICiuQt///CR1uwdxD8K7Ils/mkenaMtja9NHA9RdQBa51Azn8iCLsObCTYc52Zb6atbCi/X4ae9M8GA8qaaLigNz5G4v1BHneJylYfWKj5GwqUA3VmRu1mlEw7d2sN2uFRmgIl5kgsYYyOVOLBOKaJWhyBhNnR4U8KeqwUkOnlSdmNZnjfexXXBEcKHen1SozpXwMnzZh8LZuN6BNYd6TVpKmjulGI3vqNMM0c4JJZ9pSm4q5FjbNGQArwCsujDDwY4bCGZSJJTPMXjYOrx4yJ1RY+0TpyV9oDU3ogEqaWnhrkgrphfiYBlDY2bEvrxMTMukLCxJb0Nru6LGCpg4X1P9hu9r0gceqUSnBgdJI4rYTNddenxlEd4j/Thqv835stJWz6YoV+rfvERuSUpntl8xvrk3U8=
|
|
||||||
- secure: CmLh169xwt3OC7l19lDUwdZYfw+l/+XXKjTGxnuJiXHa9hAH+nc4mEUSmqHMHwjquyYE6LnSVGZ94LH8sCaGed/TMm0Lj3DZtZ0VsjYu3sKsNrVJn5WdYEah+GvNJ8i8uzvHnGk1L2fAagLpXFUB2K9UraA1CFiHs1fF5Fua7Ra0mE7lMz0MFvyvzcmc9u8nMz71K2YVsXqn+I/rv8onJCTtc/Ds0hVrBXwG5Fh0T3xHqjOhME9ZI2hRHnnQVdBojSY3USifQ//qAKowfBVc0pebEtgFpyXx/I17A6xCm0KVtnA2tNqu1+XFkjWaBzryaLE9ujhJcTRC/YK0JUZ+QxfYbD5C7jVy9Jlx1lOEjJ42B/ImNxPP+9p3wi2MWNbcbZJPxIe635fnT17lI7CLK2Qg1xjfTQ4WTEKW5IhnYMiyfRKiripFh4PCkDm8cJlTbpntsX1n0uheydbsgf/11vS7o1WGhVQ8+Jelc4DvIbBHCqienQEOLJDBwm7FiK5JbwZJ6ZAGeDM/IhGU1h9PNjVbHUyJhUxYoPYUE9QsZFT7ENCrRQpq49KOzXBRfffyJNJRh+Ep9X5YoW0dUp0MNu5B7z2+6w9X9n/C+loa11xF42bFzniCvvbSOQ37J+EngDinVxJkNPeyivR4fye2ENhIAjAEjouTJLOSXL9nAOU=
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- wget -qO- https://github.com/gulien/orbit/releases/download/v3.0.0/orbit_Linux_x86_64.tar.gz | tar xvz -C .
|
|
||||||
- sudo mv ./orbit /usr/local/bin && chmod +x /usr/local/bin/orbit
|
|
||||||
|
|
||||||
script:
|
|
||||||
- orbit run ci -p "Version,${TRAVIS_BRANCH}"
|
|
||||||
- orbit run build -p "Version,${TRAVIS_BRANCH}"
|
|
||||||
# Uploads coverage.
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
||||||
# Let's push to Docker hub only if not in a pull request and the branch name matches the semver format.
|
|
||||||
- if [[ "$TRAVIS_PULL_REQUEST" = false && "$TRAVIS_BRANCH" =~ ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$ ]]; then docker login -u $DOCKER_USER -p $DOCKER_PASS && docker push thecodingmachine/gotenberg:${TRAVIS_BRANCH}; fi
|
|
||||||
336
AGENTS.md
Normal file
336
AGENTS.md
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
# Operational Guidelines for Gotenberg
|
||||||
|
|
||||||
|
You are working on **Gotenberg**, a Docker-based API for converting documents to PDF. It is a widely used production dependency. Stability and backward compatibility are paramount. When in doubt about whether a change is breaking, flag it rather than assuming it's safe.
|
||||||
|
|
||||||
|
## Mandatory Workflow
|
||||||
|
|
||||||
|
Every task MUST follow these five steps in order. Do not skip any step.
|
||||||
|
|
||||||
|
### Step 1 — Plan
|
||||||
|
|
||||||
|
Before writing any code, produce a plan that covers:
|
||||||
|
|
||||||
|
- **Problem statement**: What needs to change and why.
|
||||||
|
- **Proposed solution**: The recommended approach with enough detail to implement (files to modify, interface changes, pipeline positioning, form fields, etc.).
|
||||||
|
- **Alternatives considered**: At least one alternative approach when pertinent, with a brief explanation of why the proposed solution is preferred.
|
||||||
|
- **Scope**: List every file that will be created or modified.
|
||||||
|
- **Testing strategy**: Which integration test tags will be affected, what new scenarios are needed, and whether unit tests are required.
|
||||||
|
|
||||||
|
Present the plan to the user and wait for approval before proceeding to Step 2. If the user provides a plan, validate it against the codebase and flag any issues before implementing.
|
||||||
|
|
||||||
|
### Step 2 — Implement
|
||||||
|
|
||||||
|
Implement the approved plan following the coding standards and patterns described in this document. After implementation, verify the build compiles (`go build ./...`).
|
||||||
|
|
||||||
|
### Step 3 — Test
|
||||||
|
|
||||||
|
Write or update tests based on the plan's testing strategy:
|
||||||
|
|
||||||
|
- **Integration tests** (primary): Gherkin scenarios in `test/integration/features/`. See the [Integration Tests](#integration-tests) section.
|
||||||
|
- **Unit tests** (when applicable): Table-driven tests in `*_test.go` files using mocks from `pkg/gotenberg/mocks.go`.
|
||||||
|
|
||||||
|
### Step 4 — Review
|
||||||
|
|
||||||
|
Self-review the implementation against the [Review Checklist](#review-checklist). Fix any issues found before presenting the result to the user.
|
||||||
|
|
||||||
|
### Step 5 — Commit
|
||||||
|
|
||||||
|
Present the review to the user and **wait for explicit approval**. Do NOT commit until the user confirms. Once approved, create a commit following the [Conventional Commits](https://www.conventionalcommits.org/) specification:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <description>
|
||||||
|
```
|
||||||
|
|
||||||
|
Stage only the files related to the change. Do not use `git add -A` or `git add .`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
- **Backward compatibility is law.** Never modify existing CLI flags, environment variables, or API form fields unless explicitly instructed to perform a breaking change. Flag any breaking change immediately.
|
||||||
|
- **Defensive programming.** Assume input is malformed. Handle errors explicitly. Never panic.
|
||||||
|
- **Atomic commits.** One feature or fix per PR. Isolate refactoring from feature work.
|
||||||
|
- **Idiomatic Go.** Follow "Effective Go" principles. All exported symbols must have GoDoc comments starting with their name.
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/gotenberg/ → Entry point only (wiring/startup). No business logic.
|
||||||
|
pkg/gotenberg/ → Core module system, interfaces, utilities, mocks.
|
||||||
|
pkg/modules/ → Feature modules (api, chromium, libreoffice, pdfengines, etc.).
|
||||||
|
pkg/standard/ → Wires all standard modules together via imports.
|
||||||
|
test/integration/ → Gherkin feature files + Go test infrastructure.
|
||||||
|
build/ → Dockerfile, fonts, Chromium config.
|
||||||
|
.bruno/ → Bruno API collection (mirrors every route).
|
||||||
|
```
|
||||||
|
|
||||||
|
Key interfaces live in `pkg/gotenberg/` — `Module`, `Provisioner`, `Validator`, `Debuggable`. Every module implements `Descriptor()` and self-registers. When adding features, determine if they belong in an existing module or require a new one.
|
||||||
|
|
||||||
|
## Codebase Navigation
|
||||||
|
|
||||||
|
- Start with `pkg/gotenberg/` for core interfaces and `pkg/modules/` for feature implementations.
|
||||||
|
- The integration test infrastructure in `test/integration/scenario/` is well-structured — read `scenario.go` and `containers.go` to understand the Gherkin step definitions before writing new tests.
|
||||||
|
- Mocks for all major interfaces are in `pkg/gotenberg/mocks.go` — use them for unit tests rather than creating new ones.
|
||||||
|
- Import ordering is enforced: standard library, third-party, then `github.com/gotenberg/gotenberg/v8` — separated by blank lines.
|
||||||
|
- When making changes, run only the relevant integration test tag rather than the full suite (40min timeout).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Makefile — the Only Build Interface
|
||||||
|
|
||||||
|
All build and verification tasks go through the Makefile. Do not run `go` commands directly unless debugging a specific package.
|
||||||
|
|
||||||
|
| Command | Purpose | When to use |
|
||||||
|
| ----------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||||
|
| `make build` | Build the Docker image | Before integration tests, or to verify compilation |
|
||||||
|
| `make run` | Run a Gotenberg container locally | Manual testing. Flags are configured via `.env` and Makefile variables |
|
||||||
|
| `make fmt` | Format Go code (`go fix`, `golangci-lint fmt`, `go mod tidy`) | Before every commit |
|
||||||
|
| `make lint` | Lint Go code (strict `.golangci.yml` config) | Before every commit. Zero errors permitted |
|
||||||
|
| `make lint-prettier` | Lint non-Go files (Markdown, YAML, etc.) with Prettier | Before every commit |
|
||||||
|
| `make prettify` | Format non-Go files (Markdown, YAML, etc.) with Prettier | Before every commit |
|
||||||
|
| `make test-unit` | Run unit tests (`go test -race ./...`) | After code changes to `pkg/` |
|
||||||
|
| `make test-integration` | Run integration tests (Gherkin/Godog, 40min timeout) | After any feature or route change |
|
||||||
|
| `make godoc` | Serve GoDoc at `localhost:6060` | To verify documentation |
|
||||||
|
|
||||||
|
## Module System
|
||||||
|
|
||||||
|
Gotenberg uses a self-registering module architecture inspired by CaddyServer. Each module:
|
||||||
|
|
||||||
|
- Lives in `pkg/modules/<name>/`
|
||||||
|
- Implements the `gotenberg.Module` interface (at minimum `Descriptor()`)
|
||||||
|
- May also implement `gotenberg.Provisioner`, `gotenberg.Validator`, or `gotenberg.Debuggable`
|
||||||
|
- Self-registers via `init()` and is wired through `pkg/standard/`
|
||||||
|
|
||||||
|
When adding a feature, first determine if it belongs in an existing module. Only create a new module if the feature represents a genuinely separate concern.
|
||||||
|
|
||||||
|
## Commit Convention
|
||||||
|
|
||||||
|
Commits must follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <description>
|
||||||
|
```
|
||||||
|
|
||||||
|
Common types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `ci`, `build`. The scope should match the module or area of the change (e.g., `chromium`, `pdfengines`, `api`).
|
||||||
|
|
||||||
|
## Coding Patterns
|
||||||
|
|
||||||
|
- **Error handling:** Always wrap errors with context using `fmt.Errorf("description: %w", err)`. Never swallow errors silently.
|
||||||
|
- **Import ordering:** Enforced by `gci` — standard library, then third-party, then `github.com/gotenberg/gotenberg/v8`. Three groups separated by blank lines.
|
||||||
|
- **Mocks:** Comprehensive mock implementations for all major interfaces live in `pkg/gotenberg/mocks.go`. Use these for unit tests.
|
||||||
|
- **No business logic in `cmd/`:** The `cmd/gotenberg/` package is strictly for wiring and startup.
|
||||||
|
|
||||||
|
## Adding PDF Engine Features
|
||||||
|
|
||||||
|
When adding a new PDF engine capability (e.g., bookmarks, watermark, stamp, embed), you must update the Makefile to include the corresponding engine list variable and flag. Every `--pdfengines-*-engines` flag registered in `pkg/modules/pdfengines/pdfengines.go` must have a matching entry in the Makefile:
|
||||||
|
|
||||||
|
1. **Add a variable** in the Makefile's variable block (around line 60-70):
|
||||||
|
```makefile
|
||||||
|
PDFENGINES_<FEATURE>_ENGINES=<default engines>
|
||||||
|
```
|
||||||
|
2. **Add the flag** in the Makefile's command args block (around line 140-155):
|
||||||
|
```makefile
|
||||||
|
--pdfengines-<feature>-engines=$(PDFENGINES_<FEATURE>_ENGINES) \
|
||||||
|
```
|
||||||
|
|
||||||
|
The default value should match what is defined in `pdfengines.go`'s `fs.StringSlice(...)` call for that flag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
- **Framework:** Gherkin (BDD) via [Godog](https://github.com/cucumber/godog), with `testcontainers-go` for Docker orchestration.
|
||||||
|
- **Feature files:** `test/integration/features/*.feature` — one file per endpoint or capability.
|
||||||
|
- **Test infrastructure:** `test/integration/scenario/` — Go step definitions, container management, HTTP helpers, PDF validation.
|
||||||
|
- **Entry point:** `test/integration/main_test.go` (build tag: `integration`).
|
||||||
|
- **Test data:** `test/integration/testdata/`
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
Each scenario spins up a fresh Gotenberg Docker container via testcontainers. The step definitions in `scenario/scenario.go` map Gherkin steps to Go functions. An additional `gotenberg/integration-tools` container provides PDF validation tools (`verapdf`, `pdfinfo`, `pdftotext`).
|
||||||
|
|
||||||
|
**Important:** Integration tests require a Docker image. Run `make build` before `make test-integration`.
|
||||||
|
|
||||||
|
### Selective Test Runs
|
||||||
|
|
||||||
|
Use the `TAGS` variable to run only relevant scenarios:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-integration TAGS=health
|
||||||
|
make test-integration TAGS=chromium-convert-html
|
||||||
|
make test-integration TAGS="merge,split"
|
||||||
|
```
|
||||||
|
|
||||||
|
Available tags: `chromium`, `chromium-concurrent`, `chromium-convert-html`, `chromium-convert-markdown`, `chromium-convert-url`, `debug`, `health`, `libreoffice`, `libreoffice-convert`, `output-filename`, `pdfengines`, `pdfengines-convert`, `pdfengines-embed`, `embed`, `pdfengines-encrypt`, `encrypt`, `pdfengines-flatten`, `flatten`, `pdfengines-merge`, `merge`, `pdfengines-metadata`, `metadata`, `pdfengines-split`, `split`, `pdfengines-watermark`, `watermark`, `pdfengines-stamp`, `stamp`, `pdfengines-bookmarks`, `bookmarks`, `pdfengines-rotate`, `rotate`, `prometheus-metrics`, `root`, `version`, `webhook`, `download-from`.
|
||||||
|
|
||||||
|
Other useful flags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-integration NO_CONCURRENCY=true # Disable parallel scenarios
|
||||||
|
make test-integration PLATFORM=linux/arm64 # Force a specific platform
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing a New Integration Test
|
||||||
|
|
||||||
|
1. Create or update a `.feature` file in `test/integration/features/`.
|
||||||
|
2. Tag it appropriately (e.g., `@chromium @chromium-convert-html`).
|
||||||
|
3. If the feature requires new tag(s), add them to both the `TAGS` comment block in the `Makefile` and the "Available tags" list in this file.
|
||||||
|
4. If you create a new step definition, add it to `scenario/scenario.go`, register it in `InitializeScenario`, and update the "Available Gherkin Steps" list below.
|
||||||
|
5. Test data goes in `test/integration/testdata/`.
|
||||||
|
|
||||||
|
### Available Gherkin Steps
|
||||||
|
|
||||||
|
**Given (setup):**
|
||||||
|
|
||||||
|
- `I have a default Gotenberg container`
|
||||||
|
- `I have a Gotenberg container with the following environment variable(s):` (table: key | value)
|
||||||
|
- `I have a (webhook|static) server`
|
||||||
|
|
||||||
|
**When (action):**
|
||||||
|
|
||||||
|
- `I make a "(GET|HEAD)" request to Gotenberg at the "<endpoint>" endpoint`
|
||||||
|
- `I make a "(GET|HEAD)" request to Gotenberg at the "<endpoint>" endpoint with the following header(s):` (table: name | value)
|
||||||
|
- `I make a "(POST)" request to Gotenberg at the "<endpoint>" endpoint with the following form data and header(s):` (table: name | value | kind — where kind is `file`, `field`, or `header`)
|
||||||
|
- `I make <N> concurrent "(POST)" requests to Gotenberg at the "<endpoint>" endpoint with the following form data and header(s):` (same table format)
|
||||||
|
- `I wait for the asynchronous request to the webhook`
|
||||||
|
|
||||||
|
**Then (assertions):**
|
||||||
|
|
||||||
|
- `the response status code should be <code>`
|
||||||
|
- `the (response|webhook request) header "<name>" should be "<value>"`
|
||||||
|
- `the (response|webhook request) cookie "<name>" should be "<value>"`
|
||||||
|
- `the (response|webhook request) body should match string:` (docstring)
|
||||||
|
- `the (response|webhook request) body should contain string:` (docstring)
|
||||||
|
- `the (response|webhook request) body should match JSON:` (docstring — use `"ignore"` for dynamic values like timestamps)
|
||||||
|
- `there should be <N> PDF(s) in the (response|webhook request)`
|
||||||
|
- `there should be the following file(s) in the (response|webhook request):` (table of filenames)
|
||||||
|
- `the "<name>" PDF should have <N> page(s)`
|
||||||
|
- `the "<name>" PDF (should|should NOT) be set to landscape orientation`
|
||||||
|
- `the "<name>" PDF (should|should NOT) have the following content at page <N>:` (docstring)
|
||||||
|
- `the (response|webhook request) PDF(s) should be valid "<standard>" with a tolerance of <N> failed rule(s)` (standards: `PDF/A-1b`, `PDF/A-2b`, `PDF/A-3b`, `PDF/UA-1`, `PDF/UA-2`)
|
||||||
|
- `the (response|webhook request) PDF(s) (should|should NOT) be flatten`
|
||||||
|
- `the (response|webhook request) PDF(s) (should|should NOT) be encrypted`
|
||||||
|
- `the (response|webhook request) PDF(s) (should|should NOT) have the "<filename>" file embedded`
|
||||||
|
- `the Gotenberg container (should|should NOT) log the following entries:` (table of log substrings)
|
||||||
|
- `all concurrent response status codes should be <code>`
|
||||||
|
- `all concurrent responses should have <N> PDF(s)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
|
||||||
|
- [ ] No existing CLI flags renamed or removed
|
||||||
|
- [ ] No existing environment variables renamed or removed
|
||||||
|
- [ ] No existing API form fields renamed or removed
|
||||||
|
- [ ] No existing HTTP endpoints changed or removed
|
||||||
|
- [ ] No changes to default values that alter existing behavior
|
||||||
|
|
||||||
|
If any of these are violated, the change **must** be flagged as a breaking change.
|
||||||
|
|
||||||
|
### Linting Standards
|
||||||
|
|
||||||
|
The `.golangci.yml` enforces strict rules including: `gosec`, `govet`, `errcheck`, `staticcheck`, `dupl`, `bodyclose`, `exhaustive`, `errname`, and more. Zero linting errors are permitted.
|
||||||
|
|
||||||
|
Formatters enforce `gci`, `gofmt`, `gofumpt`, `goimports` with import ordering:
|
||||||
|
|
||||||
|
1. Standard library
|
||||||
|
2. Third-party packages
|
||||||
|
3. `github.com/gotenberg/gotenberg/v8`
|
||||||
|
|
||||||
|
Three groups separated by blank lines.
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- Errors are wrapped with context: `fmt.Errorf("description: %w", err)`. No swallowed errors.
|
||||||
|
- No business logic in `cmd/`.
|
||||||
|
- No panics in production code paths.
|
||||||
|
- Input is validated defensively.
|
||||||
|
- New features belong in the correct module (or justify a new one).
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Every exported function, type, constant, and variable has a GoDoc comment starting with its name.
|
||||||
|
- New packages include a `doc.go` file.
|
||||||
|
- `README.md` is not modified unless explicitly requested.
|
||||||
|
|
||||||
|
### Definition of Done
|
||||||
|
|
||||||
|
A change is ready to merge only when:
|
||||||
|
|
||||||
|
1. Code compiles: `go build ./...`
|
||||||
|
2. Code is formatted: `make fmt`
|
||||||
|
3. All linters pass: `make lint` and `make lint-prettier`
|
||||||
|
4. Integration tests pass: `make test-integration` (at minimum, the relevant `TAGS`)
|
||||||
|
5. Unit tests pass: `make test-unit`
|
||||||
|
6. All exported symbols and new packages have compliant GoDoc
|
||||||
|
7. Bruno collection is updated (if routes were added or modified)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bruno API Collection
|
||||||
|
|
||||||
|
A [Bruno](https://www.usebruno.com/) collection lives in `.bruno/` and mirrors every Gotenberg route. When adding or updating a route, update the collection to match.
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.bruno/
|
||||||
|
├── bruno.json # Collection config
|
||||||
|
├── collection.bru # Collection-level defaults (Gotenberg-Trace header)
|
||||||
|
├── environments/
|
||||||
|
│ ├── Local.bru # baseUrl: http://localhost:3000
|
||||||
|
│ └── Demo.bru # baseUrl: https://demo.gotenberg.dev
|
||||||
|
├── Health & Info/ # GET routes
|
||||||
|
├── Chromium/Convert/ # POST routes grouped by module
|
||||||
|
├── Chromium/Screenshot/
|
||||||
|
├── LibreOffice/
|
||||||
|
└── PDF Engines/<Feature>/ # One folder per feature (Merge, Split, Rotate, …)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.bru` file format
|
||||||
|
|
||||||
|
```bru
|
||||||
|
meta {
|
||||||
|
name: <Human-readable name>
|
||||||
|
type: http
|
||||||
|
seq: <order within folder>
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/forms/<path>
|
||||||
|
body: multipartForm
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:multipart-form {
|
||||||
|
files: @file(../../test/integration/testdata/<file>)
|
||||||
|
<mandatoryField>: <value>
|
||||||
|
~<optionalField>: <value>
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
~Gotenberg-Output-Filename: <name>
|
||||||
|
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||||
|
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||||
|
~Gotenberg-Webhook-Method: POST
|
||||||
|
~Gotenberg-Webhook-Error-Method: POST
|
||||||
|
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conventions
|
||||||
|
|
||||||
|
- **Mandatory fields** are listed without prefix; **optional fields** are prefixed with `~` (disabled by default in Bruno).
|
||||||
|
- **File references** use relative paths to `test/integration/testdata/`.
|
||||||
|
- **Webhook and output filename headers** are included on every POST route as optional (`~`).
|
||||||
|
- **One `.bru` file per request**. For routes with read/write variants (e.g., bookmarks, metadata), create separate files in the same folder.
|
||||||
|
|
||||||
|
### Checklist when adding/updating a route
|
||||||
|
|
||||||
|
1. Create or update the `.bru` file in the matching folder under `.bruno/`.
|
||||||
|
2. Include all form fields from the route handler — check `FormData*` calls in the route function.
|
||||||
|
3. For file upload fields (`files`, `watermark`, `stamp`, `embeds`), use `@file(...)` with a suitable test file.
|
||||||
|
4. Verify the URL path matches the route's `Path` field exactly.
|
||||||
|
5. If you add a new module folder, keep the naming consistent (e.g., `PDF Engines/Rotate/`).
|
||||||
3
CLAUDE.md
Normal file
3
CLAUDE.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Claude Code — Gotenberg
|
||||||
|
|
||||||
|
Read [AGENTS.md](AGENTS.md) first. It contains everything: core principles, project layout, coding standards, the mandatory 4-step workflow (Plan → Implement → Test → Review), integration test reference, review checklist, and Bruno collection guidelines.
|
||||||
63
CONTRIBUTING.md
Normal file
63
CONTRIBUTING.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Contributing to Gotenberg
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to Gotenberg! This guide will help you get started.
|
||||||
|
|
||||||
|
## Before You Start
|
||||||
|
|
||||||
|
Please read the [AGENTS.md](AGENTS.md) file — it describes the core principles, project layout, development standards, integration test reference, review checklist, and Bruno collection guidelines that all contributions must follow.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go (see version in `go.mod`)
|
||||||
|
- Docker
|
||||||
|
- Node.js (see version in `.node-version`) — for Prettier linting
|
||||||
|
- [golangci-lint](https://golangci-lint.run/) v2+
|
||||||
|
|
||||||
|
### Build and Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build # Build the Docker image
|
||||||
|
make run # Run a local Gotenberg container
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Loop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Write your code, then:
|
||||||
|
make fmt # Format Go code
|
||||||
|
make prettify # Format non-Go files (Markdown, YAML, etc.)
|
||||||
|
make lint # Lint Go code (zero errors permitted)
|
||||||
|
make lint-prettier # Lint non-Go files
|
||||||
|
make test-unit # Run unit tests
|
||||||
|
make build # Build the Docker image (required before integration tests)
|
||||||
|
make test-integration # Run all integration tests
|
||||||
|
```
|
||||||
|
|
||||||
|
To run only the integration tests relevant to your change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-integration TAGS=health
|
||||||
|
make test-integration TAGS=chromium-convert-html
|
||||||
|
make test-integration TAGS="merge,split"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Submitting a Pull Request
|
||||||
|
|
||||||
|
Before opening a PR, verify:
|
||||||
|
|
||||||
|
1. Code compiles: `make build`
|
||||||
|
2. Code is formatted: `make fmt` and `make prettify`
|
||||||
|
3. All linters pass: `make lint` and `make lint-prettier`
|
||||||
|
4. Integration tests pass: `make test-integration` (at minimum, the relevant tags)
|
||||||
|
5. Unit tests pass: `make test-unit`
|
||||||
|
6. All exported symbols and new packages have GoDoc comments
|
||||||
|
|
||||||
|
### Guidelines
|
||||||
|
|
||||||
|
- **Conventional Commits.** Commit messages must follow the [Conventional Commits](https://www.conventionalcommits.org/) specification (e.g., `feat(chromium): add screenshot endpoint`, `fix(api): handle empty body`).
|
||||||
|
- **One thing per PR.** Keep features, bug fixes, and refactoring in separate PRs.
|
||||||
|
- **Backward compatibility matters.** Do not rename or remove existing CLI flags, environment variables, or API form fields without discussion.
|
||||||
|
- **Integration tests first.** When adding a feature or route, start by writing the Gherkin scenario in `test/integration/features/`.
|
||||||
|
- **No business logic in `cmd/`.** All logic belongs in `pkg/`.
|
||||||
46
Dockerfile
46
Dockerfile
@@ -1,46 +0,0 @@
|
|||||||
FROM debian:stretch-slim
|
|
||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Libraries
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs all required libraries.
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y xvfb wkhtmltopdf pdftk unoconv ttf-mscorefonts-installer &&\
|
|
||||||
ln -s /usr/bin/xvfb-run /usr/local/bin/xvfb-run &&\
|
|
||||||
ln -s /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf &&\
|
|
||||||
ln -s /usr/bin/pdftk /usr/local/bin/pdftk &&\
|
|
||||||
ln -s /usr/bin/unoconv /usr/local/bin/unoconv
|
|
||||||
|
|
||||||
RUN apt-get install -y curl gnupg &&\
|
|
||||||
curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y nodejs
|
|
||||||
|
|
||||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y yarn bzip2 &&\
|
|
||||||
yarn global add markdown-pdf --prefix /usr/local
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Gotenberg
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | All Gotenberg related stuff.
|
|
||||||
# |
|
|
||||||
|
|
||||||
COPY .ci/gotenberg /usr/bin/gotenberg
|
|
||||||
RUN ln -s /usr/bin/gotenberg /usr/local/bin/gotenberg
|
|
||||||
|
|
||||||
COPY .ci/gotenberg.yml /gotenberg/gotenberg.yml
|
|
||||||
|
|
||||||
WORKDIR /gotenberg
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
CMD ["gotenberg"]
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
FROM golang:1.10-stretch
|
|
||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Libraries
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | Installs all required libraries.
|
|
||||||
# |
|
|
||||||
|
|
||||||
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y xvfb wkhtmltopdf pdftk unoconv ttf-mscorefonts-installer &&\
|
|
||||||
ln -s /usr/bin/xvfb-run /usr/local/bin/xvfb-run &&\
|
|
||||||
ln -s /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf &&\
|
|
||||||
ln -s /usr/bin/pdftk /usr/local/bin/pdftk &&\
|
|
||||||
ln -s /usr/bin/unoconv /usr/local/bin/unoconv
|
|
||||||
|
|
||||||
RUN apt-get install -y curl gnupg &&\
|
|
||||||
curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y nodejs
|
|
||||||
|
|
||||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &&\
|
|
||||||
apt-get update &&\
|
|
||||||
apt-get install -y yarn bzip2 &&\
|
|
||||||
yarn global add markdown-pdf --prefix /usr/local
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# | Gotenberg
|
|
||||||
# |--------------------------------------------------------------------------
|
|
||||||
# |
|
|
||||||
# | All Gotenberg related stuff.
|
|
||||||
# |
|
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/thecodingmachine/gotenberg
|
|
||||||
|
|
||||||
# Installs lint dependencies.
|
|
||||||
RUN go get -u gopkg.in/alecthomas/gometalinter.v2 &&\
|
|
||||||
gometalinter.v2 --install
|
|
||||||
|
|
||||||
# Copies our Go source.
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Installs project dependencies.
|
|
||||||
RUN go get -d -v ./...
|
|
||||||
|
|
||||||
ENTRYPOINT [".ci/docker-entrypoint.sh"]
|
|
||||||
3
GEMINI.md
Normal file
3
GEMINI.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Gemini — Gotenberg
|
||||||
|
|
||||||
|
Read [AGENTS.md](AGENTS.md) first. It contains everything: core principles, project layout, coding standards, the mandatory 4-step workflow (Plan → Implement → Test → Review), integration test reference, review checklist, and Bruno collection guidelines.
|
||||||
66
Gopkg.lock
generated
66
Gopkg.lock
generated
@@ -1,66 +0,0 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/dustin/go-humanize"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "02af3965c54e8cacf948b97fef38925c4120652c"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/gorilla/context"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
|
|
||||||
version = "v1.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/gorilla/mux"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "53c1911da2b537f792e7cafcb446b05ffe33b996"
|
|
||||||
version = "v1.6.1"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/justinas/alice"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "03f45bd4b7dad4734bc4620e46a35789349abb20"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/satori/go.uuid"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
|
||||||
version = "v1.2.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/sirupsen/logrus"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
|
||||||
version = "v1.0.5"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/crypto"
|
|
||||||
packages = ["ssh/terminal"]
|
|
||||||
revision = "88942b9c40a4c9d203b82b3731787b672d6e809b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/sys"
|
|
||||||
packages = [
|
|
||||||
"unix",
|
|
||||||
"windows"
|
|
||||||
]
|
|
||||||
revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "gopkg.in/yaml.v2"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "86f5ed62f8a0ee96bd888d2efdfd6d4fb100a4eb"
|
|
||||||
version = "v2.2.0"
|
|
||||||
|
|
||||||
[solve-meta]
|
|
||||||
analyzer-name = "dep"
|
|
||||||
analyzer-version = 1
|
|
||||||
inputs-digest = "97e0dbb20631792bc25ad19e6a85a90a9d24bb03f773cd36dee11eebae152091"
|
|
||||||
solver-name = "gps-cdcl"
|
|
||||||
solver-version = 1
|
|
||||||
54
Gopkg.toml
54
Gopkg.toml
@@ -1,54 +0,0 @@
|
|||||||
# Gopkg.toml example
|
|
||||||
#
|
|
||||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
|
||||||
# for detailed Gopkg.toml documentation.
|
|
||||||
#
|
|
||||||
# required = ["github.com/user/thing/cmd/thing"]
|
|
||||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project"
|
|
||||||
# version = "1.0.0"
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project2"
|
|
||||||
# branch = "dev"
|
|
||||||
# source = "github.com/myfork/project2"
|
|
||||||
#
|
|
||||||
# [[override]]
|
|
||||||
# name = "github.com/x/y"
|
|
||||||
# version = "2.4.0"
|
|
||||||
#
|
|
||||||
# [prune]
|
|
||||||
# non-go = false
|
|
||||||
# go-tests = true
|
|
||||||
# unused-packages = true
|
|
||||||
|
|
||||||
|
|
||||||
[prune]
|
|
||||||
go-tests = true
|
|
||||||
unused-packages = true
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/satori/go.uuid"
|
|
||||||
version = "1.2.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/gorilla/mux"
|
|
||||||
version = "1.6.1"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/justinas/alice"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "gopkg.in/yaml.v2"
|
|
||||||
version = "2.2.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/sirupsen/logrus"
|
|
||||||
version = "1.0.5"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/dustin/go-humanize"
|
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 TheCodingMachine
|
Copyright (c) Julien Neuhart
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
254
Makefile
Normal file
254
Makefile
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
include .env
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show the help
|
||||||
|
@grep -hE '^[A-Za-z0-9_ \-]*?:.*##.*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: ## Build the Gotenberg's Docker image
|
||||||
|
docker build \
|
||||||
|
-t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
|
||||||
|
-f $(DOCKERFILE) $(DOCKER_BUILD_CONTEXT)
|
||||||
|
|
||||||
|
TZ=UTC
|
||||||
|
GOTENBERG_HIDE_BANNER=false
|
||||||
|
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
|
||||||
|
GOTENBERG_BUILD_DEBUG_DATA=true
|
||||||
|
API_PORT=3000
|
||||||
|
API_PORT_FROM_ENV=
|
||||||
|
API_BIND_IP=
|
||||||
|
API_START_TIMEOUT=30s
|
||||||
|
API_TIMEOUT=30s
|
||||||
|
API_BODY_LIMIT=
|
||||||
|
API_ROOT_PATH="/"
|
||||||
|
API_TRACE_HEADER=Gotenberg-Trace
|
||||||
|
API_ENABLE_BASIC_AUTH=false
|
||||||
|
GOTENBERG_API_BASIC_AUTH_USERNAME=
|
||||||
|
GOTENBERG_API_BASIC_AUTH_PASSWORD=
|
||||||
|
API-DOWNLOAD-FROM-ALLOW-LIST=
|
||||||
|
API-DOWNLOAD-FROM-DENY-LIST=
|
||||||
|
API-DOWNLOAD-FROM-FROM-MAX-RETRY=4
|
||||||
|
API-DISABLE-DOWNLOAD-FROM=false
|
||||||
|
API_DISABLE_HEALTH_CHECK_LOGGING=false
|
||||||
|
API_ENABLE_DEBUG_ROUTE=false
|
||||||
|
CHROMIUM_RESTART_AFTER=100
|
||||||
|
CHROMIUM_MAX_QUEUE_SIZE=0
|
||||||
|
CHROMIUM_MAX_CONCURRENCY=6
|
||||||
|
CHROMIUM_AUTO_START=false
|
||||||
|
CHROMIUM_START_TIMEOUT=20s
|
||||||
|
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
|
||||||
|
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
|
||||||
|
CHROMIUM_DISABLE_WEB_SECURITY=false
|
||||||
|
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
|
||||||
|
CHROMIUM_HOST_RESOLVER_RULES=
|
||||||
|
CHROMIUM_PROXY_SERVER=
|
||||||
|
CHROMIUM_ALLOW_LIST=
|
||||||
|
CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
|
||||||
|
CHROMIUM_CLEAR_CACHE=false
|
||||||
|
CHROMIUM_CLEAR_COOKIES=false
|
||||||
|
CHROMIUM_DISABLE_JAVASCRIPT=false
|
||||||
|
CHROMIUM_DISABLE_ROUTES=false
|
||||||
|
LIBREOFFICE_RESTART_AFTER=10
|
||||||
|
LIBREOFFICE_MAX_QUEUE_SIZE=0
|
||||||
|
LIBREOFFICE_AUTO_START=false
|
||||||
|
LIBREOFFICE_START_TIMEOUT=20s
|
||||||
|
LIBREOFFICE_DISABLE_ROUTES=false
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_FORMAT=auto
|
||||||
|
LOG_FIELDS_PREFIX=
|
||||||
|
LOG_ENABLE_GCP_FIELDS=false
|
||||||
|
PDFENGINES_DISABLE_ROUTES=false
|
||||||
|
PDFENGINES_MERGE_ENGINES=qpdf,pdfcpu,pdftk
|
||||||
|
PDFENGINES_SPLIT_ENGINES=pdfcpu,qpdf,pdftk
|
||||||
|
PDFENGINES_FLATTEN_ENGINES=qpdf
|
||||||
|
PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine
|
||||||
|
PDFENGINES_READ_METADATA_ENGINES=exiftool
|
||||||
|
PDFENGINES_WRITE_METADATA_ENGINES=exiftool
|
||||||
|
PDFENGINES_READ_BOOKMARKS_ENGINES=pdfcpu
|
||||||
|
PDFENGINES_WRITE_BOOKMARKS_ENGINES=pdfcpu
|
||||||
|
PDFENGINES_WATERMARK_ENGINES=pdfcpu,pdftk
|
||||||
|
PDFENGINES_STAMP_ENGINES=pdfcpu,pdftk
|
||||||
|
PDFENGINES_ENCRYPT_ENGINES=qpdf,pdfcpu,pdftk
|
||||||
|
PDFENGINES_ROTATE_ENGINES=pdfcpu,pdftk
|
||||||
|
PDFENGINES_EMBED_ENGINES=pdfcpu
|
||||||
|
PROMETHEUS_NAMESPACE=gotenberg
|
||||||
|
PROMETHEUS_COLLECT_INTERVAL=1s
|
||||||
|
PROMETHEUS_DISABLE_ROUTE_LOGGING=false
|
||||||
|
PROMETHEUS_DISABLE_COLLECT=false
|
||||||
|
PROMETHEUS_METRICS_PATH=/prometheus/metrics
|
||||||
|
WEBHOOK_ENABLE_SYNC_MODE=false
|
||||||
|
WEBHOOK_ALLOW_LIST=
|
||||||
|
WEBHOOK_DENY_LIST=
|
||||||
|
WEBHOOK_ERROR_ALLOW_LIST=
|
||||||
|
WEBHOOK_ERROR_DENY_LIST=
|
||||||
|
WEBHOOK_MAX_RETRY=4
|
||||||
|
WEBHOOK_RETRY_MIN_WAIT=1s
|
||||||
|
WEBHOOK_RETRY_MAX_WAIT=30s
|
||||||
|
WEBHOOK_CLIENT_TIMEOUT=30s
|
||||||
|
WEBHOOK_DISABLE=false
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run: ## Start a Gotenberg container
|
||||||
|
docker run --rm -it \
|
||||||
|
-p $(API_PORT):$(API_PORT) \
|
||||||
|
-e GOTENBERG_API_BASIC_AUTH_USERNAME=$(GOTENBERG_API_BASIC_AUTH_USERNAME) \
|
||||||
|
-e GOTENBERG_API_BASIC_AUTH_PASSWORD=$(GOTENBERG_API_BASIC_AUTH_PASSWORD) \
|
||||||
|
-e TZ=$(TZ) \
|
||||||
|
$(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
|
||||||
|
gotenberg \
|
||||||
|
--gotenberg-hide-banner=$(GOTENBERG_HIDE_BANNER) \
|
||||||
|
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
|
||||||
|
--gotenberg-build-debug-data="$(GOTENBERG_BUILD_DEBUG_DATA)" \
|
||||||
|
--api-port=$(API_PORT) \
|
||||||
|
--api-port-from-env=$(API_PORT_FROM_ENV) \
|
||||||
|
--api-bind-ip=$(API_BIND_IP) \
|
||||||
|
--api-start-timeout=$(API_START_TIMEOUT) \
|
||||||
|
--api-timeout=$(API_TIMEOUT) \
|
||||||
|
--api-body-limit="$(API_BODY_LIMIT)" \
|
||||||
|
--api-root-path=$(API_ROOT_PATH) \
|
||||||
|
--api-trace-header=$(API_TRACE_HEADER) \
|
||||||
|
--api-enable-basic-auth=$(API_ENABLE_BASIC_AUTH) \
|
||||||
|
--api-download-from-allow-list=$(API-DOWNLOAD-FROM-ALLOW-LIST) \
|
||||||
|
--api-download-from-deny-list=$(API-DOWNLOAD-FROM-DENY-LIST) \
|
||||||
|
--api-download-from-max-retry=$(API-DOWNLOAD-FROM-FROM-MAX-RETRY) \
|
||||||
|
--api-disable-download-from=$(API-DISABLE-DOWNLOAD-FROM) \
|
||||||
|
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
|
||||||
|
--api-enable-debug-route=$(API_ENABLE_DEBUG_ROUTE) \
|
||||||
|
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
|
||||||
|
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
|
||||||
|
--chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \
|
||||||
|
--chromium-max-concurrency=$(CHROMIUM_MAX_CONCURRENCY) \
|
||||||
|
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
|
||||||
|
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
|
||||||
|
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
|
||||||
|
--chromium-disable-web-security=$(CHROMIUM_DISABLE_WEB_SECURITY) \
|
||||||
|
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
|
||||||
|
--chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \
|
||||||
|
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
|
||||||
|
--chromium-allow-list="$(CHROMIUM_ALLOW_LIST)" \
|
||||||
|
--chromium-deny-list="$(CHROMIUM_DENY_LIST)" \
|
||||||
|
--chromium-clear-cache=$(CHROMIUM_CLEAR_CACHE) \
|
||||||
|
--chromium-clear-cookies=$(CHROMIUM_CLEAR_COOKIES) \
|
||||||
|
--chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \
|
||||||
|
--chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \
|
||||||
|
--libreoffice-restart-after=$(LIBREOFFICE_RESTART_AFTER) \
|
||||||
|
--libreoffice-max-queue-size=$(LIBREOFFICE_MAX_QUEUE_SIZE) \
|
||||||
|
--libreoffice-auto-start=$(LIBREOFFICE_AUTO_START) \
|
||||||
|
--libreoffice-start-timeout=$(LIBREOFFICE_START_TIMEOUT) \
|
||||||
|
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLE_ROUTES) \
|
||||||
|
--log-level=$(LOG_LEVEL) \
|
||||||
|
--log-format=$(LOG_FORMAT) \
|
||||||
|
--log-fields-prefix=$(LOG_FIELDS_PREFIX) \
|
||||||
|
--log-enable-gcp-fields=$(LOG_ENABLE_GCP_FIELDS) \
|
||||||
|
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
|
||||||
|
--pdfengines-merge-engines=$(PDFENGINES_MERGE_ENGINES) \
|
||||||
|
--pdfengines-split-engines=$(PDFENGINES_SPLIT_ENGINES) \
|
||||||
|
--pdfengines-flatten-engines=$(PDFENGINES_FLATTEN_ENGINES) \
|
||||||
|
--pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \
|
||||||
|
--pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \
|
||||||
|
--pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \
|
||||||
|
--pdfengines-read-bookmarks-engines=$(PDFENGINES_READ_BOOKMARKS_ENGINES) \
|
||||||
|
--pdfengines-write-bookmarks-engines=$(PDFENGINES_WRITE_BOOKMARKS_ENGINES) \
|
||||||
|
--pdfengines-watermark-engines=$(PDFENGINES_WATERMARK_ENGINES) \
|
||||||
|
--pdfengines-stamp-engines=$(PDFENGINES_STAMP_ENGINES) \
|
||||||
|
--pdfengines-encrypt-engines=$(PDFENGINES_ENCRYPT_ENGINES) \
|
||||||
|
--pdfengines-rotate-engines=$(PDFENGINES_ROTATE_ENGINES) \
|
||||||
|
--pdfengines-embed-engines=$(PDFENGINES_EMBED_ENGINES) \
|
||||||
|
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
|
||||||
|
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
||||||
|
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
|
||||||
|
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
|
||||||
|
--prometheus-metrics-path=$(PROMETHEUS_METRICS_PATH) \
|
||||||
|
--webhook-enable-sync-mode="$(WEBHOOK_ENABLE_SYNC_MODE)" \
|
||||||
|
--webhook-allow-list="$(WEBHOOK_ALLOW_LIST)" \
|
||||||
|
--webhook-deny-list="$(WEBHOOK_DENY_LIST)" \
|
||||||
|
--webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \
|
||||||
|
--webhook-error-deny-list=$(WEBHOOK_ERROR_DENY_LIST) \
|
||||||
|
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \
|
||||||
|
--webhook-retry-min-wait=$(WEBHOOK_RETRY_MIN_WAIT) \
|
||||||
|
--webhook-retry-max-wait=$(WEBHOOK_RETRY_MAX_WAIT) \
|
||||||
|
--webhook-client-timeout=$(WEBHOOK_CLIENT_TIMEOUT) \
|
||||||
|
--webhook-disable=$(WEBHOOK_DISABLE)
|
||||||
|
|
||||||
|
.PHONY: test-unit
|
||||||
|
test-unit: ## Run unit tests
|
||||||
|
go test -race ./...
|
||||||
|
|
||||||
|
PLATFORM=
|
||||||
|
NO_CONCURRENCY=false
|
||||||
|
# Available tags:
|
||||||
|
# chromium
|
||||||
|
# chromium-concurrent
|
||||||
|
# chromium-convert-html
|
||||||
|
# chromium-convert-markdown
|
||||||
|
# chromium-convert-url
|
||||||
|
# debug
|
||||||
|
# health
|
||||||
|
# libreoffice
|
||||||
|
# libreoffice-convert
|
||||||
|
# output-filename
|
||||||
|
# pdfengines
|
||||||
|
# pdfengines-convert
|
||||||
|
# pdfengines-embed
|
||||||
|
# embed
|
||||||
|
# pdfengines-encrypt
|
||||||
|
# encrypt
|
||||||
|
# pdfengines-flatten
|
||||||
|
# flatten
|
||||||
|
# pdfengines-merge
|
||||||
|
# merge
|
||||||
|
# pdfengines-metadata
|
||||||
|
# metadata
|
||||||
|
# pdfengines-split
|
||||||
|
# split
|
||||||
|
# pdfengines-watermark
|
||||||
|
# watermark
|
||||||
|
# pdfengines-stamp
|
||||||
|
# stamp
|
||||||
|
# pdfengines-rotate
|
||||||
|
# rotate
|
||||||
|
# pdfengines-bookmarks
|
||||||
|
# bookmarks
|
||||||
|
# prometheus-metrics
|
||||||
|
# root
|
||||||
|
# version
|
||||||
|
# webhook
|
||||||
|
# download-from
|
||||||
|
TAGS=
|
||||||
|
|
||||||
|
.PHONY: test-integration
|
||||||
|
test-integration: ## Run integration tests
|
||||||
|
go test -timeout 40m -tags=integration -v github.com/gotenberg/gotenberg/v8/test/integration -args \
|
||||||
|
--gotenberg-docker-repository=$(DOCKER_REPOSITORY) \
|
||||||
|
--gotenberg-version=$(GOTENBERG_VERSION) \
|
||||||
|
--gotenberg-container-platform=$(PLATFORM) \
|
||||||
|
--no-concurrency=$(NO_CONCURRENCY) \
|
||||||
|
--tags="$(TAGS)"
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: ## Lint Golang codebase
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
.PHONY: lint-prettier
|
||||||
|
lint-prettier: ## Lint non-Golang codebase
|
||||||
|
npx prettier --check .
|
||||||
|
|
||||||
|
.PHONY: lint-todo
|
||||||
|
lint-todo: ## Find TODOs in Golang codebase
|
||||||
|
golangci-lint run --no-config --disable-all --enable godox
|
||||||
|
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt: ## Format Golang codebase and "optimize" the dependencies
|
||||||
|
go fix ./...
|
||||||
|
golangci-lint fmt
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
.PHONY: prettify
|
||||||
|
prettify: ## Format non-Golang codebase
|
||||||
|
npx prettier --write .
|
||||||
|
|
||||||
|
# go install golang.org/x/tools/cmd/godoc@latest
|
||||||
|
.PHONY: godoc
|
||||||
|
godoc: ## Run a webserver with Gotenberg godoc
|
||||||
|
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v8)
|
||||||
|
godoc -http=:6060
|
||||||
215
README.md
215
README.md
@@ -1,202 +1,49 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://user-images.githubusercontent.com/8983173/38133342-11df3bd8-340f-11e8-9fe4-50baecdceeca.png" alt="Gotenberg's logo" width="250" height="250" />
|
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
|
||||||
</p>
|
<h3 align="center">Gotenberg</h3>
|
||||||
<h3 align="center">Gotenberg</h3>
|
<p align="center">A containerized API for seamless PDF conversion</p>
|
||||||
<p align="center">A stateless API for converting Markdown files, HTML files and Office documents to PDF</p>
|
<p align="center">
|
||||||
<p align="center">
|
<a href="https://hub.docker.com/r/gotenberg/gotenberg"><img alt="Total downloads (gotenberg/gotenberg)" src="https://img.shields.io/docker/pulls/gotenberg/gotenberg"></a>
|
||||||
<a href="https://microbadger.com/images/thecodingmachine/gotenberg:2.0.0">
|
<a href="https://github.com/gotenberg/gotenberg/actions/workflows/continuous-integration.yml"><img alt="Continuous Integration" src="https://github.com/gotenberg/gotenberg/actions/workflows/continuous-integration.yml/badge.svg"></a>
|
||||||
<img src="https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:2.0.0.svg" alt="MicroBadger layers">
|
<a href="https://pkg.go.dev/github.com/gotenberg/gotenberg/v8"><img alt="Go Reference" src="https://pkg.go.dev/badge/github.com/gotenberg/gotenberg.svg"></a>
|
||||||
</a>
|
</p>
|
||||||
<a href="https://travis-ci.org/thecodingmachine/gotenberg">
|
<p align="center">
|
||||||
<img src="https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=master" alt="Travis CI">
|
<a href="https://trendshift.io/repositories/2996"><img src="https://trendshift.io/api/badge/repositories/2996" alt="gotenberg%2Fgotenberg | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
</a>
|
</p>
|
||||||
<a href="https://godoc.org/github.com/thecodingmachine/gotenberg">
|
<p align="center"><a href="https://gotenberg.dev/docs/getting-started/introduction">Read the Documentation</a> · <a href="https://gotenberg.dev/docs/getting-started/installation#live-demo-">Try the Live Demo</a> 🔥</p>
|
||||||
<img src="https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg" alt="GoDoc">
|
|
||||||
</a>
|
|
||||||
<a href="https://goreportcard.com/report/thecodingmachine/gotenberg">
|
|
||||||
<img src="https://goreportcard.com/badge/github.com/thecodingmachine/gotenberg" alt="Go Report Card">
|
|
||||||
</a>
|
|
||||||
<a href="https://codecov.io/gh/thecodingmachine/gotenberg/branch/master">
|
|
||||||
<img src="https://codecov.io/gh/thecodingmachine/gotenberg/branch/master/graph/badge.svg" alt="Codecov">
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from
|
**Gotenberg** is a containerized API that abstracts the complexity of PDF conversion.
|
||||||
various sources. Each time, we ended up using some well known libraries like **wkhtmltopdf** or **unoconv** and kind of lost time by
|
|
||||||
reimplementing a solution from a project to another project. Meh.
|
|
||||||
|
|
||||||
# Menu
|
It provides a `multipart/form-data` interface for interacting with powerful engines like Chromium and LibreOffice.
|
||||||
|
Instead of managing heavy dependencies, browser versions, or fonts in your own backend, simply send your files to
|
||||||
|
Gotenberg and get a PDF in return.
|
||||||
|
|
||||||
* [Usage](#usage)
|
## Quick Start
|
||||||
* [Security](#security)
|
|
||||||
* [Scalability](#scalability)
|
|
||||||
* [Custom implementation](#custom-implementation)
|
|
||||||
* [Clients](#clients)
|
|
||||||
|
|
||||||
## Usage
|
Open a terminal and run the following command:
|
||||||
|
|
||||||
Let's say you're starting the API using this simple command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:2.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
The API is now available on your host under `http://127.0.0.1:3000`.
|
|
||||||
|
|
||||||
It accepts `POST` requests with a `multipart/form-data` Content-Type. Your form data should provide one or more files to convert.
|
|
||||||
The default image accepts the following:
|
|
||||||
|
|
||||||
* Markdown files
|
|
||||||
* HTML files
|
|
||||||
* Office documents (.docx, .doc, .odt, .pptx, .ppt, .odp and so on)
|
|
||||||
* PDF files
|
|
||||||
|
|
||||||
**Heads up:** the API relies on the file extension to determine which library to use for conversion.
|
|
||||||
|
|
||||||
There are two use cases:
|
|
||||||
|
|
||||||
* If you send one file, it will convert it and return the resulting PDF
|
|
||||||
* If many files, it will convert them to PDF, merge the resulting PDFs into a single PDF and return it
|
|
||||||
|
|
||||||
### Examples:
|
|
||||||
|
|
||||||
* One file
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://127.0.0.1:3000 \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@file.docx \
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
* Many files
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://127.0.0.1:3000 \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@file.md \
|
|
||||||
--form files=@file.html \
|
|
||||||
--form files=@file.pdf \
|
|
||||||
--form files=@file.docx \
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
The API does not provide any authentication mechanisms. Make sure to not put it on a public facing port and your client(s) should always
|
|
||||||
controls what is sent to the API.
|
|
||||||
|
|
||||||
## Scalability
|
|
||||||
|
|
||||||
Some libraries like **unoconv** cannot perform concurrent conversions. That's why the API does only one conversion at a time.
|
|
||||||
If your API is under heavy load, a request will take time to be processed.
|
|
||||||
|
|
||||||
Fortunately, you may pass through this limitation by scaling the API.
|
|
||||||
|
|
||||||
In the following example, I'll demonstrate how to do some vertical scaling (= on the same machine) with Docker Compose, but of course horizontal scaling works too!
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
# your others services
|
|
||||||
|
|
||||||
gotenberg:
|
|
||||||
image: thecodingmachine/gotenberg:2.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
You may now launch your services using:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up --scale gotenberg=your_number_of_instances
|
docker run --rm -p 3000:3000 gotenberg/gotenberg:8
|
||||||
```
|
```
|
||||||
|
|
||||||
When requesting the Gotenberg service with your client(s), Docker will automatically redirect a request to a Gotenberg container
|
With the API running at `http://localhost:3000`, you are now ready to head
|
||||||
according to the round-robin strategy.
|
to the **[Full Documentation](https://gotenberg.dev/docs/getting-started/introduction)** to discover how to convert URLs,
|
||||||
|
local files, inject custom CSS, merge PDFs, and more.
|
||||||
|
|
||||||
## Custom implementation
|
## Sponsors
|
||||||
|
|
||||||
The API relies on a simple YAML configuration file called `gotenberg.yml`. It allows you to tweak some values and even provides you
|
Open-source development takes a significant amount of time, energy, and dedication. If Gotenberg helps streamline your
|
||||||
a way to change the commands called for each kind of conversion.
|
workflow or powers your business, please consider supporting its continuous improvement by [**becoming a sponsor**](https://github.com/sponsors/gulien)! ❤️
|
||||||
|
|
||||||
Below the default configuration file:
|
**Historic & GitHub Sponsors**
|
||||||
|
|
||||||
```yaml
|
- [TheCodingMachine](https://thecodingmachine.com/)
|
||||||
# The port the application will listen to.
|
- [pdfme](https://pdfme.com/)
|
||||||
port: 3000
|
|
||||||
|
|
||||||
logs:
|
**Powered By**
|
||||||
# Accepted values, in order of severity: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
|
|
||||||
# Messages at and above the selected level will be logged.
|
|
||||||
level: "INFO"
|
|
||||||
|
|
||||||
# Accepted values: text, json.
|
- [Docker](https://docs.docker.com/docker-hub/repos/manage/trusted-content/dsos-program/)
|
||||||
# When a TTY is not attached, the output will be in the defined format.
|
- [JetBrains](https://www.jetbrains.com/community/opensource/)
|
||||||
formatter: "text"
|
|
||||||
|
|
||||||
# You don't like a library which is used for a conversion? You want to handle a new file type?
|
|
||||||
# You may provide here your own implementation!
|
|
||||||
commands:
|
|
||||||
|
|
||||||
# Some libraries like unoconv cannot perform concurrent conversions. That's why the API does only one conversion at a time.
|
|
||||||
# If your current implementation uses libraries which are able to perform concurrent conversions, you may
|
|
||||||
# change this value to false.
|
|
||||||
lock: true
|
|
||||||
|
|
||||||
# Unlike others commands' templates, you have access to FilesPaths instead of FilePath: it gathers all PDF files which should be merged.
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
|
|
||||||
# You may add more commands (or less, or even none).
|
|
||||||
conversions:
|
|
||||||
|
|
||||||
# The command template: you have access to FilePath and ResultFilePath variables.
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
# The binary which will call the command.
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
# Duration in seconds after which the command will be killed if it has not finished.
|
|
||||||
timeout: 30
|
|
||||||
# Files with the following extensions will be converted by the current command.
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
```
|
|
||||||
|
|
||||||
We provide binaries for a wide range of OS and architecture in the [releases page](../../releases),
|
|
||||||
so feel free to create your own Docker image for your implementation of the Gotenberg API :metal:
|
|
||||||
|
|
||||||
## Clients
|
|
||||||
|
|
||||||
* https://github.com/thecodingmachine/gotenberg-php-client (PHP client)
|
|
||||||
* Add your own client by submitting a [pull request](../../pulls)!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Would you like to update this documentation ? Feel free to open an [issue](../../issues).
|
|
||||||
|
|||||||
41
SECURITY.md
Normal file
41
SECURITY.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Please ensure to keep your environment up to date and use only the latest version of Gotenberg.
|
||||||
|
Security updates and patches will be applied only to the most recent version.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Your help in identifying vulnerabilities in our project is much appreciated.
|
||||||
|
We take all reports regarding security seriously.
|
||||||
|
|
||||||
|
If you discover a security vulnerability, please refrain from publishing it publicly.
|
||||||
|
Instead, kindly send us the details via email to _neuhart [dot] julien [at] gmail [dot] com_.
|
||||||
|
|
||||||
|
In the subject of your email, please indicate that it's a security vulnerability report for Gotenberg.
|
||||||
|
In your message, please include:
|
||||||
|
|
||||||
|
- A detailed description of the vulnerability.
|
||||||
|
- The steps to reproduce the issue.
|
||||||
|
- Any potential impact of the vulnerability on the users or system.
|
||||||
|
|
||||||
|
Please remember that this process is done in a _'best-effort'_ manner.
|
||||||
|
This means we strive to respond and act as quickly as possible, but the speed may vary depending on the severity of
|
||||||
|
the issue and our resources.
|
||||||
|
|
||||||
|
Thank you in advance for helping to keep our project safe!
|
||||||
|
|
||||||
|
## Disclosure Policy
|
||||||
|
|
||||||
|
Once we have received your vulnerability report, we will work to validate and reproduce the issue.
|
||||||
|
If we can confirm the vulnerability, we will proceed to:
|
||||||
|
|
||||||
|
- Work on a fix and a release timeline.
|
||||||
|
- Notify you when the fix has been implemented and released.
|
||||||
|
- Credit you for discovering the vulnerability (unless you request anonymity).
|
||||||
|
- Please note that we will do our best to keep you informed about the progress towards resolving the issue.
|
||||||
|
|
||||||
|
## Comments on this Policy
|
||||||
|
|
||||||
|
If you have suggestions on how this process could be improved, please submit a pull request.
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "DEBUG"
|
|
||||||
formatter: {
|
|
||||||
value: [
|
|
||||||
...
|
|
||||||
}
|
|
||||||
commands:
|
|
||||||
lock: true
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "DEBUG"
|
|
||||||
formatter: "text"
|
|
||||||
commands:
|
|
||||||
lock: true
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "DEBUG"
|
|
||||||
formatter: "text"
|
|
||||||
commands:
|
|
||||||
lock: true
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "DEBUG"
|
|
||||||
formatter: "text"
|
|
||||||
commands:
|
|
||||||
lock: true
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 0
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "DEBUG"
|
|
||||||
formatter: "text"
|
|
||||||
commands:
|
|
||||||
lock: false
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "DEBUG"
|
|
||||||
formatter: "text"
|
|
||||||
commands:
|
|
||||||
lock: true
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 0
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 0
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 0
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 0
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "DEBUG"
|
|
||||||
formatter: "text"
|
|
||||||
commands:
|
|
||||||
lock: true
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "DEBUG"
|
|
||||||
formatter: "DEBUG"
|
|
||||||
commands:
|
|
||||||
lock: true
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "text"
|
|
||||||
formatter: "text"
|
|
||||||
commands:
|
|
||||||
lock: true
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
port: 3000
|
|
||||||
logs:
|
|
||||||
level: "DEBUG"
|
|
||||||
formatter: "text"
|
|
||||||
commands:
|
|
||||||
lock: true
|
|
||||||
merge:
|
|
||||||
template: "pdftk {{ range $filePath := FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
conversions:
|
|
||||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".md"
|
|
||||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".html"
|
|
||||||
- ".htm"
|
|
||||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
|
||||||
interpreter: "/bin/sh -c"
|
|
||||||
timeout: 30
|
|
||||||
extensions:
|
|
||||||
- ".doc"
|
|
||||||
- ".docx"
|
|
||||||
- ".odt"
|
|
||||||
- ".xls"
|
|
||||||
- ".xlsx"
|
|
||||||
- ".ods"
|
|
||||||
- ".ppt"
|
|
||||||
- ".pptx"
|
|
||||||
- ".odp"
|
|
||||||
BIN
_tests/file.docx
BIN
_tests/file.docx
Binary file not shown.
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Some title</title>
|
|
||||||
<meta name="author" content="Julien Neuhart">
|
|
||||||
<meta name="description" content="Simple HTML page">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<p>Hi! I have been converted from an HTML page!</p>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Hi, I've been converted from a markdown file!
|
|
||||||
BIN
_tests/file.pdf
BIN
_tests/file.pdf
Binary file not shown.
@@ -1,213 +0,0 @@
|
|||||||
/*
|
|
||||||
Package config contains all the logic allowing us to instantiate the application's configuration.
|
|
||||||
|
|
||||||
The application's configuration is loaded from a YAML file named gotenberg.yml.
|
|
||||||
It should be located where the user starts the application from the CLI.
|
|
||||||
*/
|
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// appConfig gathers all configuration data.
|
|
||||||
appConfig struct {
|
|
||||||
port string
|
|
||||||
logsLevel logrus.Level
|
|
||||||
logsFormatter logrus.Formatter
|
|
||||||
lock bool
|
|
||||||
// commands associates a file extension with a Command instance.
|
|
||||||
// Particular case: ".pdf" extension is used for the merge command.
|
|
||||||
commands map[string]*Command
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command gathers information on how to launch an external binary used for converting
|
|
||||||
// a file to PDF.
|
|
||||||
Command struct {
|
|
||||||
// Template is the data-driven template of the command.
|
|
||||||
Template *template.Template
|
|
||||||
// The binary which will call the command.
|
|
||||||
Interpreter []string
|
|
||||||
// Timeout is the duration in seconds after which the command's process will be killed
|
|
||||||
// if it does not finish before.
|
|
||||||
Timeout int
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// our default instance of appConfig.
|
|
||||||
var config = &appConfig{}
|
|
||||||
|
|
||||||
// Reset reinitializes our configuration.
|
|
||||||
func Reset() {
|
|
||||||
config = &appConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPort sets the port which will be used by the application.
|
|
||||||
func WithPort(port string) {
|
|
||||||
config.port = port
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPort returns the current port.
|
|
||||||
func GetPort() string {
|
|
||||||
return config.port
|
|
||||||
}
|
|
||||||
|
|
||||||
// levels associates logging levels as defined in the configuration file gotenberg.yml
|
|
||||||
// with its counterpart from the logrus library.
|
|
||||||
var levels = map[string]logrus.Level{
|
|
||||||
"DEBUG": logrus.DebugLevel,
|
|
||||||
"INFO": logrus.InfoLevel,
|
|
||||||
"WARN": logrus.WarnLevel,
|
|
||||||
"ERROR": logrus.ErrorLevel,
|
|
||||||
"FATAL": logrus.FatalLevel,
|
|
||||||
"PANIC": logrus.PanicLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
type wrongLogsLevelError struct{}
|
|
||||||
|
|
||||||
const wrongLogsLevelErrorMessage = "accepted values for logs level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC"
|
|
||||||
|
|
||||||
func (e *wrongLogsLevelError) Error() string {
|
|
||||||
return wrongLogsLevelErrorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLogsLevel sets the logs level.
|
|
||||||
// If the given string does not match with a logrus level,
|
|
||||||
// throws an error.
|
|
||||||
func WithLogsLevel(level string) error {
|
|
||||||
l, ok := levels[level]
|
|
||||||
if !ok {
|
|
||||||
return &wrongLogsLevelError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.logsLevel = l
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLogsLevel returns the current logs level.
|
|
||||||
func GetLogsLevel() logrus.Level {
|
|
||||||
return config.logsLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatters associates logging formatter as defined in the configuration file gotenberg.yml
|
|
||||||
// with its counterpart from the logrus library.
|
|
||||||
var formatters = map[string]logrus.Formatter{
|
|
||||||
"text": &logrus.TextFormatter{},
|
|
||||||
"json": &logrus.JSONFormatter{},
|
|
||||||
}
|
|
||||||
|
|
||||||
type wrongLogsFormatterError struct{}
|
|
||||||
|
|
||||||
const wrongLogsFormatterErrorMessage = "accepted value for logs formatter: text, json"
|
|
||||||
|
|
||||||
func (e *wrongLogsFormatterError) Error() string {
|
|
||||||
return wrongLogsFormatterErrorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLogsFormatter sets the logs formatter.
|
|
||||||
// If the given string does not match with a logrus formatter,
|
|
||||||
// throws an error.
|
|
||||||
func WithLogsFormatter(formatter string) error {
|
|
||||||
f, ok := formatters[formatter]
|
|
||||||
if !ok {
|
|
||||||
return &wrongLogsFormatterError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.logsFormatter = f
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLogsFormatter returns the current logs formatter.
|
|
||||||
func GetLogsFormatter() logrus.Formatter {
|
|
||||||
return config.logsFormatter
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLock sets the lock strategy.
|
|
||||||
func WithLock(lock bool) {
|
|
||||||
config.lock = lock
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasLock returns the current lock strategy.
|
|
||||||
func HasLock() bool {
|
|
||||||
return config.lock
|
|
||||||
}
|
|
||||||
|
|
||||||
type interpreterEmptyError struct {
|
|
||||||
command string
|
|
||||||
}
|
|
||||||
|
|
||||||
const interpreterEmptyErrorMessage = "the interepreter for command %s should not be empty"
|
|
||||||
|
|
||||||
func (e *interpreterEmptyError) Error() string {
|
|
||||||
return fmt.Sprintf(interpreterEmptyErrorMessage, e.command)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCommand instantiates a Command. If the given command string
|
|
||||||
// is not a valid template, throws an error.
|
|
||||||
func NewCommand(command string, interpreter string, timeout int) (*Command, error) {
|
|
||||||
t, err := template.New(command).Parse(command)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if interpreter == "" {
|
|
||||||
return nil, &interpreterEmptyError{command}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Command{t, strings.Fields(interpreter), timeout}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type fileExtensionAlreadyUsedError struct {
|
|
||||||
extension string
|
|
||||||
command *Command
|
|
||||||
existingCommand *Command
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileExtensionAlreadyUsedErrorMessage = "file extension %s from command %s is already used by command %s"
|
|
||||||
|
|
||||||
func (e *fileExtensionAlreadyUsedError) Error() string {
|
|
||||||
return fmt.Sprintf(fileExtensionAlreadyUsedErrorMessage, e.extension, e.command.Template.Name(), e.existingCommand.Template.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithCommand adds a Command instance and associates it with the given
|
|
||||||
// file extension. If the file extension is already used by another Command
|
|
||||||
// instance, throws an error.
|
|
||||||
func WithCommand(extension string, command *Command) error {
|
|
||||||
if config.commands == nil {
|
|
||||||
config.commands = make(map[string]*Command)
|
|
||||||
}
|
|
||||||
|
|
||||||
existingCommand, ok := config.commands[extension]
|
|
||||||
if ok {
|
|
||||||
return &fileExtensionAlreadyUsedError{extension, command, existingCommand}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.commands[extension] = command
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type noCommandFoundForFileExtensionError struct {
|
|
||||||
extension string
|
|
||||||
}
|
|
||||||
|
|
||||||
const noCommandFoundForFileExtensionErrorMessage = "no command found for file extension %s"
|
|
||||||
|
|
||||||
func (e *noCommandFoundForFileExtensionError) Error() string {
|
|
||||||
return fmt.Sprintf(noCommandFoundForFileExtensionErrorMessage, e.extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCommand returns the Command instance associated with the given
|
|
||||||
// file extension. If no Command instance found, throws an error.
|
|
||||||
func GetCommand(extension string) (*Command, error) {
|
|
||||||
c, ok := config.commands[extension]
|
|
||||||
if !ok {
|
|
||||||
return nil, &noCommandFoundForFileExtensionError{extension}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReset(t *testing.T) {
|
|
||||||
c := &appConfig{}
|
|
||||||
config.port = "3000"
|
|
||||||
Reset()
|
|
||||||
|
|
||||||
if c.port != config.port {
|
|
||||||
t.Error("Configuration should have been reset")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithPort(t *testing.T) {
|
|
||||||
port := "3000"
|
|
||||||
WithPort(port)
|
|
||||||
|
|
||||||
if config.port != port {
|
|
||||||
t.Errorf("Configuration populated with a wrong port: got '%s' want '%s'", config.port, port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetPort(t *testing.T) {
|
|
||||||
port := "3000"
|
|
||||||
config.port = port
|
|
||||||
|
|
||||||
if GetPort() != port {
|
|
||||||
t.Errorf("Configuration returned a wrong port: got '%s' want '%s'", GetPort(), port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWrongLogsLevelError(t *testing.T) {
|
|
||||||
err := &wrongLogsLevelError{}
|
|
||||||
if err.Error() != wrongLogsLevelErrorMessage {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLogsLevelErrorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithLogsLevel(t *testing.T) {
|
|
||||||
var lvl string
|
|
||||||
|
|
||||||
// case 1: uses a wrong logs level.
|
|
||||||
lvl = "text"
|
|
||||||
if err := WithLogsLevel(lvl); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been populated by using '%s' as logs level", lvl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a correct logs level.
|
|
||||||
lvl = "DEBUG"
|
|
||||||
if err := WithLogsLevel(lvl); err != nil {
|
|
||||||
t.Errorf("Configuration should have been populated by using '%s' as logs level", lvl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetLogsLevel(t *testing.T) {
|
|
||||||
lvl := logrus.DebugLevel
|
|
||||||
config.logsLevel = lvl
|
|
||||||
|
|
||||||
if GetLogsLevel() != lvl {
|
|
||||||
t.Errorf("Configuration returned a wrong logs level: got '%s' want '%s'", GetLogsLevel(), lvl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWrongLogsFormatterError(t *testing.T) {
|
|
||||||
err := &wrongLogsFormatterError{}
|
|
||||||
if err.Error() != wrongLogsFormatterErrorMessage {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLogsFormatterErrorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithLogsFormatter(t *testing.T) {
|
|
||||||
var formatter string
|
|
||||||
|
|
||||||
// case 1: uses a wrong logs formatter.
|
|
||||||
formatter = "DEBUG"
|
|
||||||
if err := WithLogsFormatter(formatter); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been populated by using '%s' as logs formatter", formatter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a correct logs formatter.
|
|
||||||
formatter = "text"
|
|
||||||
if err := WithLogsFormatter(formatter); err != nil {
|
|
||||||
t.Errorf("Configuration should have been populated by using '%s' as logs formatter", formatter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetLogsFormatter(t *testing.T) {
|
|
||||||
formatter := &logrus.TextFormatter{}
|
|
||||||
config.logsFormatter = formatter
|
|
||||||
|
|
||||||
if GetLogsFormatter() != formatter {
|
|
||||||
t.Errorf("Configuration returned a wrong logs formatter: got '%v' want '%v'", GetLogsFormatter(), formatter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithLock(t *testing.T) {
|
|
||||||
lock := true
|
|
||||||
WithLock(lock)
|
|
||||||
|
|
||||||
if config.lock != lock {
|
|
||||||
t.Errorf("Configuration populated with a wrong lock strategy: got '%t' want '%t'", config.lock, lock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHasLock(t *testing.T) {
|
|
||||||
lock := true
|
|
||||||
config.lock = true
|
|
||||||
|
|
||||||
if HasLock() != lock {
|
|
||||||
t.Errorf("Configuration returned a wrong port: got '%t' want '%t'", HasLock(), lock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInterpreterEmptyError(t *testing.T) {
|
|
||||||
err := &interpreterEmptyError{"echo hello world"}
|
|
||||||
expected := fmt.Sprintf(interpreterEmptyErrorMessage, err.command)
|
|
||||||
if err.Error() != expected {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewCommand(t *testing.T) {
|
|
||||||
var cmd string
|
|
||||||
|
|
||||||
// case 1: uses a wrong command template.
|
|
||||||
cmd = "pdftk {{ range $filePath := FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
if _, err := NewCommand(cmd, "/bin/sh -c", 0); err == nil {
|
|
||||||
t.Errorf("Command should not have been instantiated by using '%s' as command template", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a correct command template.
|
|
||||||
cmd = "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
|
||||||
if _, err := NewCommand(cmd, "/bin/sh -c", 0); err != nil {
|
|
||||||
t.Errorf("Command should have been instantiated by using '%s' as command template", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 3: uses an empty interpreter.
|
|
||||||
if _, err := NewCommand(cmd, "", 0); err == nil {
|
|
||||||
t.Error("Command should not have been instantiated by using an empty interpreter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileExtensionAlreadyUsedError(t *testing.T) {
|
|
||||||
ext := ".pdf"
|
|
||||||
cmd1, _ := NewCommand("echo", "/bin/sh -c", 0)
|
|
||||||
cmd2, _ := NewCommand("echo", "/bin/sh -c", 0)
|
|
||||||
err := &fileExtensionAlreadyUsedError{ext, cmd1, cmd2}
|
|
||||||
expected := fmt.Sprintf(fileExtensionAlreadyUsedErrorMessage, err.extension, err.command.Template.Name(), err.existingCommand.Template.Name())
|
|
||||||
|
|
||||||
if err.Error() != expected {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithCommand(t *testing.T) {
|
|
||||||
ext := ".pdf"
|
|
||||||
cmd, _ := NewCommand("echo", "/bin/sh -c", 0)
|
|
||||||
|
|
||||||
// case 1: uses a command with a file extension not already referenced.
|
|
||||||
if err := WithCommand(ext, cmd); err != nil {
|
|
||||||
t.Errorf("Configuration should have been populated by using a command with the file extension '%s'", ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a command with a file extension already referenced.
|
|
||||||
if err := WithCommand(ext, cmd); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been populated by using a command with the file extension '%s'", ext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoCommandFoundForFileExtensionError(t *testing.T) {
|
|
||||||
err := &noCommandFoundForFileExtensionError{".pdf"}
|
|
||||||
expected := fmt.Sprintf(noCommandFoundForFileExtensionErrorMessage, err.extension)
|
|
||||||
|
|
||||||
if err.Error() != expected {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetCommand(t *testing.T) {
|
|
||||||
Reset()
|
|
||||||
ext := ".pdf"
|
|
||||||
cmd, _ := NewCommand("echo", "/bin/sh -c", 0)
|
|
||||||
WithCommand(ext, cmd)
|
|
||||||
|
|
||||||
// case 1: uses a file extension which has a command associated.
|
|
||||||
if _, err := GetCommand(ext); err != nil {
|
|
||||||
t.Errorf("Configuration should have been able to return a command by using the file extension '%s'", ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a file extension which has no command associated.
|
|
||||||
ext = ".docx"
|
|
||||||
if _, err := GetCommand(ext); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been able to return a command by using the file extension '%s'", ext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseFile instantiates the application's configuration using the given YAML file.
|
|
||||||
func ParseFile(configurationFilePath string) error {
|
|
||||||
fileConfig, err := readFile(configurationFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
WithPort(fileConfig.Port)
|
|
||||||
|
|
||||||
if err := WithLogsLevel(fileConfig.Logs.Level); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := WithLogsFormatter(fileConfig.Logs.Formatter); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
WithLock(fileConfig.Commands.Lock)
|
|
||||||
|
|
||||||
// handles merge command first...
|
|
||||||
cmd, err := NewCommand(fileConfig.Commands.Merge.Template, fileConfig.Commands.Merge.Interpreter, fileConfig.Commands.Merge.Timeout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
WithCommand(".pdf", cmd)
|
|
||||||
|
|
||||||
// ...then conversion commands!
|
|
||||||
for _, command := range fileConfig.Commands.Conversions {
|
|
||||||
cmd, err := NewCommand(command.Template, command.Interpreter, command.Timeout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ext := range command.Extensions {
|
|
||||||
if err := WithCommand(ext, cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
// fileConfig gathers all data coming from the configuration file gotenberg.yml.
|
|
||||||
fileConfig struct {
|
|
||||||
Port string `yaml:"port"`
|
|
||||||
Logs struct {
|
|
||||||
Level string `yaml:"level"`
|
|
||||||
Formatter string `yaml:"formatter"`
|
|
||||||
} `yaml:"logs"`
|
|
||||||
Commands struct {
|
|
||||||
Lock bool `yaml:"lock"`
|
|
||||||
Merge *mergeCommand `yaml:"merge"`
|
|
||||||
Conversions []*conversionCommand `yaml:"conversions,omitempty"`
|
|
||||||
} `yaml:"commands"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeCommand gathers all data regarding the... merge command.
|
|
||||||
mergeCommand struct {
|
|
||||||
Template string `yaml:"template"`
|
|
||||||
Interpreter string `yaml:"interpreter"`
|
|
||||||
Timeout int `yaml:"timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// conversionCommand gathers all data regarding a conversion command.
|
|
||||||
conversionCommand struct {
|
|
||||||
Template string `yaml:"template"`
|
|
||||||
Interpreter string `yaml:"interpreter"`
|
|
||||||
Timeout int `yaml:"timeout"`
|
|
||||||
Extensions []string `yaml:"extensions"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// readFile instantiates a fileConfig instance by reading
|
|
||||||
// the given YAML file.
|
|
||||||
func readFile(configurationFilePath string) (*fileConfig, error) {
|
|
||||||
c := &fileConfig{}
|
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(configurationFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaml.Unmarshal(data, &c); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func load(configurationFilePath string) error {
|
|
||||||
Reset()
|
|
||||||
return ParseFile(configurationFilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseFile(t *testing.T) {
|
|
||||||
var path string
|
|
||||||
|
|
||||||
// case 1: uses an empty configuration file path.
|
|
||||||
if err := load(""); err == nil {
|
|
||||||
t.Error("Configuration should not have been populated by using an empty configuration file path")
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a broken configuration file.
|
|
||||||
path, _ = filepath.Abs("../../_tests/configurations/broken-gotenberg.yml")
|
|
||||||
if err := load(path); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 3: uses a configuration file with a wrong logging level.
|
|
||||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-level-gotenberg.yml")
|
|
||||||
if err := load(path); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 4: uses a configuration file with a wrong logging formatter.
|
|
||||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-formatter-gotenberg.yml")
|
|
||||||
if err := load(path); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 5: uses a configuration file with a wrong merge command template.
|
|
||||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-merge-command-template-gotenberg.yml")
|
|
||||||
if err := load(path); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 6: uses a configuration file with a wrong command template.
|
|
||||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-command-template-gotenberg.yml")
|
|
||||||
if err := load(path); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 7: uses a configuration file with a duplicate command.
|
|
||||||
path, _ = filepath.Abs("../../_tests/configurations/duplicate-command-gotenberg.yml")
|
|
||||||
if err := load(path); err == nil {
|
|
||||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 8: uses a correct configuration file.
|
|
||||||
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
|
|
||||||
if err := load(path); err != nil {
|
|
||||||
t.Errorf("Configuration should have been populated with '%s'", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
// Package context provides functions for setting and getting values from
|
|
||||||
// a request's context.
|
|
||||||
package context
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/converter"
|
|
||||||
)
|
|
||||||
|
|
||||||
type key uint32
|
|
||||||
|
|
||||||
const (
|
|
||||||
requestIDKey key = iota
|
|
||||||
converterKey
|
|
||||||
resultFilePathKey
|
|
||||||
)
|
|
||||||
|
|
||||||
// WithRequestID populates a request's context with the given request ID
|
|
||||||
// and returns the updated request.
|
|
||||||
func WithRequestID(r *http.Request, requestID string) *http.Request {
|
|
||||||
ctx := r.Context()
|
|
||||||
ctx = context.WithValue(ctx, requestIDKey, requestID)
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestIDNotFoundError struct{}
|
|
||||||
|
|
||||||
const requestIDNotFoundErrorMessage = "the request ID was not found in request context"
|
|
||||||
|
|
||||||
func (e *requestIDNotFoundError) Error() string {
|
|
||||||
return requestIDNotFoundErrorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRequestID returns the request ID if found in
|
|
||||||
// the request's context. Otherwise throws an error.
|
|
||||||
func GetRequestID(r *http.Request) (string, error) {
|
|
||||||
ID, ok := r.Context().Value(requestIDKey).(string)
|
|
||||||
if !ok {
|
|
||||||
return "", &requestIDNotFoundError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithConverter populates a request's context with the given converter
|
|
||||||
// and returns the updated request.
|
|
||||||
func WithConverter(r *http.Request, converter *converter.Converter) *http.Request {
|
|
||||||
ctx := r.Context()
|
|
||||||
ctx = context.WithValue(ctx, converterKey, converter)
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
type converterNotFoundError struct{}
|
|
||||||
|
|
||||||
const converterNotFoundErrorMessage = "the converter was not found in request context"
|
|
||||||
|
|
||||||
func (e *converterNotFoundError) Error() string {
|
|
||||||
return converterNotFoundErrorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConverter returns the converter if found in
|
|
||||||
// the request's context. Otherwise throws an error.
|
|
||||||
func GetConverter(r *http.Request) (*converter.Converter, error) {
|
|
||||||
c, ok := r.Context().Value(converterKey).(*converter.Converter)
|
|
||||||
if !ok {
|
|
||||||
return nil, &converterNotFoundError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithResultFilePath populates a request's context with the given result file path
|
|
||||||
// and returns the updated request.
|
|
||||||
func WithResultFilePath(r *http.Request, resultFilePath string) *http.Request {
|
|
||||||
ctx := r.Context()
|
|
||||||
ctx = context.WithValue(ctx, resultFilePathKey, resultFilePath)
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
type resultFilePathNotFoundError struct{}
|
|
||||||
|
|
||||||
const resultFilePathNotFoundErrorMessage = "the result file path was not found in request context"
|
|
||||||
|
|
||||||
func (e *resultFilePathNotFoundError) Error() string {
|
|
||||||
return resultFilePathNotFoundErrorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetResultFilePath returns the result file path if found in
|
|
||||||
// the request's context. Otherwise throws an error.
|
|
||||||
func GetResultFilePath(r *http.Request) (string, error) {
|
|
||||||
path, ok := r.Context().Value(resultFilePathKey).(string)
|
|
||||||
if !ok {
|
|
||||||
return "", &resultFilePathNotFoundError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package context
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/converter"
|
|
||||||
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWithRequestID(t *testing.T) {
|
|
||||||
requestID := uuid.NewV4().String()
|
|
||||||
req := WithRequestID(httptest.NewRequest(http.MethodPost, "/", nil), requestID)
|
|
||||||
if ID, _ := req.Context().Value(requestIDKey).(string); ID != requestID {
|
|
||||||
t.Errorf("Context returned a wrong converter: got '%s' want '%s'", ID, requestID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequestIDNotFoundError(t *testing.T) {
|
|
||||||
err := &requestIDNotFoundError{}
|
|
||||||
if err.Error() != requestIDNotFoundErrorMessage {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), requestIDNotFoundErrorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRequestID(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
|
||||||
|
|
||||||
// case 1: uses a request without a request ID entry in its context.
|
|
||||||
if _, err := GetRequestID(req); err == nil {
|
|
||||||
t.Error("Context should not have a request ID entry")
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a request with a request ID entry in its context.
|
|
||||||
req = WithRequestID(req, uuid.NewV4().String())
|
|
||||||
if _, err := GetRequestID(req); err != nil {
|
|
||||||
t.Error("Context should have a request ID entry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithConverter(t *testing.T) {
|
|
||||||
req := WithConverter(httptest.NewRequest(http.MethodPost, "/", nil), &converter.Converter{})
|
|
||||||
if c, _ := req.Context().Value(converterKey).(*converter.Converter); c == nil {
|
|
||||||
t.Errorf("Context returned a wrong converter: got '%v' want not nil", c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConverterNotFoundError(t *testing.T) {
|
|
||||||
err := &converterNotFoundError{}
|
|
||||||
if err.Error() != converterNotFoundErrorMessage {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), converterNotFoundErrorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetConverter(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
|
||||||
|
|
||||||
// case 1: uses a request without a converter entry in its context.
|
|
||||||
if _, err := GetConverter(req); err == nil {
|
|
||||||
t.Error("Context should not have a converter entry")
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a request with a converter entry in its context.
|
|
||||||
req = WithConverter(req, &converter.Converter{})
|
|
||||||
if _, err := GetConverter(req); err != nil {
|
|
||||||
t.Error("Context should have a converter entry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithResultFilePath(t *testing.T) {
|
|
||||||
filePath := "file.pdf"
|
|
||||||
req := WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), filePath)
|
|
||||||
if path, _ := req.Context().Value(resultFilePathKey).(string); path != filePath {
|
|
||||||
t.Errorf("Context returned a wrong result file path: got '%s' want '%s'", path, filePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResultFilePathNotFoundError(t *testing.T) {
|
|
||||||
err := &resultFilePathNotFoundError{}
|
|
||||||
if err.Error() != resultFilePathNotFoundErrorMessage {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), resultFilePathNotFoundErrorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetResultFilePath(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
|
||||||
|
|
||||||
// case 1: uses a request without a result file path entry in its context.
|
|
||||||
if _, err := GetResultFilePath(req); err == nil {
|
|
||||||
t.Error("Context should not have a result file path entry")
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a request with a result file path entry in its context.
|
|
||||||
req = WithResultFilePath(req, "file.pdf")
|
|
||||||
if _, err := GetResultFilePath(req); err != nil {
|
|
||||||
t.Error("Context should have a result file path entry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
// Package converter implements a solution for converting one or more files to PDF.
|
|
||||||
package converter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/converter/process"
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/logger"
|
|
||||||
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Converter handles conversion into PDF of files coming from a request.
|
|
||||||
type Converter struct {
|
|
||||||
files []*gfile.File
|
|
||||||
workingDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoFileToConvertError is raided when a request has not file attached to it.
|
|
||||||
type NoFileToConvertError struct{}
|
|
||||||
|
|
||||||
const noFileToConvertErrorMessage = "no file to convert"
|
|
||||||
|
|
||||||
func (e *NoFileToConvertError) Error() string {
|
|
||||||
return noFileToConvertErrorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConverter instantiates a converter by parsing a request.
|
|
||||||
func NewConverter(r *http.Request) (*Converter, error) {
|
|
||||||
c := &Converter{
|
|
||||||
workingDir: fmt.Sprintf("./%s/", uuid.NewV4().String()),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Mkdir(c.workingDir, 0666); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("created working directory %s", c.workingDir)
|
|
||||||
|
|
||||||
reader, err := r.MultipartReader()
|
|
||||||
if err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
part, err := reader.NextPart()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
fileName := part.FileName()
|
|
||||||
if fileName == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := gfile.NewFile(c.workingDir, part, fileName)
|
|
||||||
if err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.files = append(c.files, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.files) == 0 {
|
|
||||||
return c, &NoFileToConvertError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert converts its associated files to PDF. If more than one file,
|
|
||||||
// it will merge all of them into one unique PDF file.
|
|
||||||
// Returns the new file path or an error if something bad happened.
|
|
||||||
func (c *Converter) Convert() (string, error) {
|
|
||||||
var filesPaths []string
|
|
||||||
for _, f := range c.files {
|
|
||||||
if f.Extension != ".pdf" {
|
|
||||||
path, err := process.Unconv(c.workingDir, f)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
filesPaths = append(filesPaths, path)
|
|
||||||
} else {
|
|
||||||
filesPaths = append(filesPaths, f.Path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(filesPaths) == 1 {
|
|
||||||
return filesPaths[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err := process.Merge(c.workingDir, filesPaths)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all file inside its working directory.
|
|
||||||
func (c *Converter) Clear() error {
|
|
||||||
if err := os.RemoveAll(c.workingDir); err != nil {
|
|
||||||
logger.Error(fmt.Errorf("failed to remove working directory %s", c.workingDir))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("removed working directory %s", c.workingDir)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package converter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeRequest(filesPaths ...string) *http.Request {
|
|
||||||
r, w := io.Pipe()
|
|
||||||
mpw := multipart.NewWriter(w)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
var part io.Writer
|
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
if len(filesPaths) == 0 {
|
|
||||||
part, _ = mpw.CreateFormField("foo")
|
|
||||||
part.Write([]byte("bar"))
|
|
||||||
} else {
|
|
||||||
for _, filePath := range filesPaths {
|
|
||||||
file, _ := os.Open(filePath)
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
fileInfo, _ := file.Stat()
|
|
||||||
part, _ = mpw.CreateFormFile("files", fileInfo.Name())
|
|
||||||
io.Copy(part, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mpw.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", r)
|
|
||||||
req.Header.Set("Content-Type", mpw.FormDataContentType())
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
func load(configurationFilePath string) {
|
|
||||||
config.Reset()
|
|
||||||
path, _ := filepath.Abs(configurationFilePath)
|
|
||||||
config.ParseFile(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoFileToConvertError(t *testing.T) {
|
|
||||||
err := &NoFileToConvertError{}
|
|
||||||
if err.Error() != noFileToConvertErrorMessage {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), noFileToConvertErrorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewConverter(t *testing.T) {
|
|
||||||
var (
|
|
||||||
path string
|
|
||||||
oPath string
|
|
||||||
)
|
|
||||||
|
|
||||||
load("../../_tests/configurations/gotenberg.yml")
|
|
||||||
|
|
||||||
// case 1: uses a request with a single file.
|
|
||||||
path, _ = filepath.Abs("../../_tests/file.docx")
|
|
||||||
if _, err := NewConverter(makeRequest(path)); err != nil {
|
|
||||||
t.Errorf("Converter should have been instantiated with '%s'", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a request with wrong file type.
|
|
||||||
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
|
|
||||||
if _, err := NewConverter(makeRequest(path)); err == nil {
|
|
||||||
t.Errorf("Converter should not have been instantiated with '%s'", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 3: uses a request with two files.
|
|
||||||
path, _ = filepath.Abs("../../_tests/file.pdf")
|
|
||||||
oPath, _ = filepath.Abs("../../_tests/file.docx")
|
|
||||||
if _, err := NewConverter(makeRequest(path, oPath)); err != nil {
|
|
||||||
t.Errorf("Converter should have been instantiated with '%s' and '%s'", path, oPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 4: uses a request with one Office file type and one wrong file type.
|
|
||||||
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
|
|
||||||
oPath, _ = filepath.Abs("../../_tests/file.docx")
|
|
||||||
if _, err := NewConverter(makeRequest(path, oPath)); err == nil {
|
|
||||||
t.Errorf("Converter should not have been instantiated with '%s' and '%s'", path, oPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 5: uses a request with no file.
|
|
||||||
if _, err := NewConverter(makeRequest()); err == nil {
|
|
||||||
t.Error("Converter should not have been instantiated with no file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvert(t *testing.T) {
|
|
||||||
var (
|
|
||||||
path string
|
|
||||||
oPath string
|
|
||||||
c *Converter
|
|
||||||
)
|
|
||||||
|
|
||||||
load("../../_tests/configurations/gotenberg.yml")
|
|
||||||
|
|
||||||
// case 1: uses a request with a single file.
|
|
||||||
path, _ = filepath.Abs("../../_tests/file.docx")
|
|
||||||
c, _ = NewConverter(makeRequest(path))
|
|
||||||
if _, err := c.Convert(); err != nil {
|
|
||||||
t.Errorf("Converter should have been able to convert '%s' to PDF", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a request with two files.
|
|
||||||
path, _ = filepath.Abs("../../_tests/file.pdf")
|
|
||||||
oPath, _ = filepath.Abs("../../_tests/file.docx")
|
|
||||||
c, _ = NewConverter(makeRequest(path, oPath))
|
|
||||||
if _, err := c.Convert(); err != nil {
|
|
||||||
t.Errorf("Converter should have been able to convert '%s' and '%s' to PDF", path, oPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
load("../../_tests/configurations/timeout-gotenberg.yml")
|
|
||||||
|
|
||||||
// case 3: uses a request with a single file and a configuration with an unsuitable timeout for the conversion commands.
|
|
||||||
path, _ = filepath.Abs("../../_tests/file.docx")
|
|
||||||
c, _ = NewConverter(makeRequest(path))
|
|
||||||
if _, err := c.Convert(); err == nil {
|
|
||||||
t.Errorf("Converter should not have been able to convert '%s' to PDF", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
load("../../_tests/configurations/merge-timeout-gotenberg.yml")
|
|
||||||
|
|
||||||
// case 4: uses a request with two files and a configuration with an unsuitable timeout for the merge command.
|
|
||||||
path, _ = filepath.Abs("../../_tests/file.pdf")
|
|
||||||
oPath, _ = filepath.Abs("../../_tests/file.docx")
|
|
||||||
c, _ = NewConverter(makeRequest(path, oPath))
|
|
||||||
if _, err := c.Convert(); err == nil {
|
|
||||||
t.Errorf("Converter should not have been able to merge '%s' and '%s' into PDF", path, oPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClear(t *testing.T) {
|
|
||||||
load("../../_tests/configurations/gotenberg.yml")
|
|
||||||
|
|
||||||
path, _ := filepath.Abs("../../_tests/file.docx")
|
|
||||||
c, _ := NewConverter(makeRequest(path))
|
|
||||||
if err := c.Clear(); err != nil {
|
|
||||||
t.Error("Converter should have been able to clear itself")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// Package file implements a solution for handling files coming from a request.
|
|
||||||
package file
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/config"
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/logger"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// File represents a file which has been created
|
|
||||||
// from a request.
|
|
||||||
type File struct {
|
|
||||||
// Extension is the extension of the file.
|
|
||||||
Extension string
|
|
||||||
// Path is the file path.
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFile creates a file in the considered directory.
|
|
||||||
// Returns a *File instance or an error if something bad happened.
|
|
||||||
func NewFile(workingDir string, r io.Reader, fileName string) (*File, error) {
|
|
||||||
ext := filepath.Ext(fileName)
|
|
||||||
|
|
||||||
if _, err := config.GetCommand(ext); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
f := &File{ext, MakeFilePath(workingDir, ext)}
|
|
||||||
|
|
||||||
file, err := os.Create(f.Path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
n, err := io.Copy(file, r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// resets the read pointer.
|
|
||||||
file.Seek(0, 0)
|
|
||||||
|
|
||||||
logger.Debugf("working file %s has been created from %s (%s copied)", f.Path, fileName, humanize.Bytes(uint64(n)))
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeFilePath is a simple helper which generates a random file name
|
|
||||||
// and associates it with the considered directory to make a path.
|
|
||||||
func MakeFilePath(workingDir string, ext string) string {
|
|
||||||
return fmt.Sprintf("%s%s%s", workingDir, uuid.NewV4().String(), ext)
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package file
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func load(configurationFilePath string) {
|
|
||||||
config.Reset()
|
|
||||||
path, _ := filepath.Abs(configurationFilePath)
|
|
||||||
config.ParseFile(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewFile(t *testing.T) {
|
|
||||||
load("../../../_tests/configurations/gotenberg.yml")
|
|
||||||
|
|
||||||
workingDir := "test"
|
|
||||||
os.Mkdir(workingDir, 0666)
|
|
||||||
|
|
||||||
// case 1: uses a wrong file name.
|
|
||||||
if _, err := NewFile(workingDir, new(bytes.Buffer), "file.yml"); err == nil {
|
|
||||||
t.Error("File should not have been instantiated with an empty buffer")
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a file name.
|
|
||||||
filePath, _ := filepath.Abs("../../../_tests/file.pdf")
|
|
||||||
r, _ := os.Open(filePath)
|
|
||||||
defer r.Close()
|
|
||||||
if _, err := NewFile(workingDir, r, "file.pdf"); err != nil {
|
|
||||||
t.Errorf("File should have been instantiated using a reader of '%s'", filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.RemoveAll(workingDir)
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
// Package process handles all commands executions.
|
|
||||||
package process
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/config"
|
|
||||||
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type runner struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
var forest = &runner{}
|
|
||||||
|
|
||||||
type commandTimeoutError struct {
|
|
||||||
command string
|
|
||||||
timeout int
|
|
||||||
}
|
|
||||||
|
|
||||||
const commandTimeoutErrorMessage = "the command %s has reached the %d second(s) timeout"
|
|
||||||
|
|
||||||
func (e *commandTimeoutError) Error() string {
|
|
||||||
return fmt.Sprintf(commandTimeoutErrorMessage, e.command, e.timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// run runs the given command. If timeout is reached or
|
|
||||||
// something bad happened, returns an error.
|
|
||||||
func (r *runner) run(command string, interpreter []string, timeout int) error {
|
|
||||||
if config.HasLock() {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
} else {
|
|
||||||
logger.Warn("lock disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
binary := interpreter[0]
|
|
||||||
parameters := append(interpreter[1:], command)
|
|
||||||
cmd := exec.Command(binary, parameters...)
|
|
||||||
logger.Debugf("executing command %s", cmd.Args)
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
done <- cmd.Wait()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// wait for the process to finish or kill it after a timeout.
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Duration(timeout) * time.Second):
|
|
||||||
if err := cmd.Process.Kill(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &commandTimeoutError{command, timeout}
|
|
||||||
case err := <-done:
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// conversionData will be applied to the data-driven templates of conversions commands.
|
|
||||||
type conversionData struct {
|
|
||||||
FilePath string
|
|
||||||
ResultFilePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unconv converts a file to PDF and returns the new file path.
|
|
||||||
func Unconv(workingDir string, file *gfile.File) (string, error) {
|
|
||||||
cmdData := &conversionData{file.Path, gfile.MakeFilePath(workingDir, ".pdf")}
|
|
||||||
|
|
||||||
cmd, err := config.GetCommand(file.Extension)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var data bytes.Buffer
|
|
||||||
if err := cmd.Template.Execute(&data, cmdData); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = forest.run(data.String(), cmd.Interpreter, cmd.Timeout)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("created %s from %s", cmdData.ResultFilePath, cmdData.FilePath)
|
|
||||||
return cmdData.ResultFilePath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeData will be applied to the data-driven template of the merge command.
|
|
||||||
type mergeData struct {
|
|
||||||
FilesPaths []string
|
|
||||||
ResultFilePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge merges many PDF files to one unique PDF file and returns the new file path.
|
|
||||||
func Merge(workingDir string, filesPaths []string) (string, error) {
|
|
||||||
cmdData := &mergeData{filesPaths, gfile.MakeFilePath(workingDir, ".pdf")}
|
|
||||||
|
|
||||||
cmd, err := config.GetCommand(".pdf")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var data bytes.Buffer
|
|
||||||
if err := cmd.Template.Execute(&data, cmdData); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = forest.run(data.String(), cmd.Interpreter, cmd.Timeout)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("created %s from %+v", cmdData.ResultFilePath, cmdData.FilesPaths)
|
|
||||||
return cmdData.ResultFilePath, nil
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package process
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/config"
|
|
||||||
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeFile(workingDir string, fileName string) *gfile.File {
|
|
||||||
filePath := fmt.Sprintf("%s%s", "../../../_tests/", fileName)
|
|
||||||
absPath, _ := filepath.Abs(filePath)
|
|
||||||
|
|
||||||
r, _ := os.Open(absPath)
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
f, _ := gfile.NewFile(workingDir, r, fileName)
|
|
||||||
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
func load(configurationFilePath string) {
|
|
||||||
config.Reset()
|
|
||||||
path, _ := filepath.Abs(configurationFilePath)
|
|
||||||
config.ParseFile(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCommandTimeoutError(t *testing.T) {
|
|
||||||
err := &commandTimeoutError{"echo hello", 30}
|
|
||||||
expected := fmt.Sprintf(commandTimeoutErrorMessage, err.command, err.timeout)
|
|
||||||
|
|
||||||
if err.Error() != expected {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRun(t *testing.T) {
|
|
||||||
var cmd string
|
|
||||||
|
|
||||||
load("../../../_tests/configurations/gotenberg.yml")
|
|
||||||
|
|
||||||
// case 1: uses a simple command.
|
|
||||||
cmd = "echo Hello world"
|
|
||||||
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err != nil {
|
|
||||||
t.Errorf("Command '%s' should have worked", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a simple command but with an unsuitable timeout.
|
|
||||||
cmd = "sleep 5"
|
|
||||||
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 0); err == nil {
|
|
||||||
t.Errorf("Command '%s' should not have worked", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 3: uses a broken command.
|
|
||||||
cmd = "helloworld"
|
|
||||||
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err == nil {
|
|
||||||
t.Errorf("Command '%s' should not have worked", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
load("../../../_tests/configurations/no-lock-gotenberg.yml")
|
|
||||||
|
|
||||||
// case 4: uses a configuration with a no lock strategy.
|
|
||||||
cmd = "echo Hello world"
|
|
||||||
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err != nil {
|
|
||||||
t.Errorf("Command '%s' should have worked", cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnconv(t *testing.T) {
|
|
||||||
var file *gfile.File
|
|
||||||
|
|
||||||
workingDir := "test"
|
|
||||||
os.Mkdir(workingDir, 0666)
|
|
||||||
|
|
||||||
load("../../../_tests/configurations/gotenberg.yml")
|
|
||||||
|
|
||||||
// case 1: uses an Markdown file type.
|
|
||||||
file = makeFile(workingDir, "file.md")
|
|
||||||
if _, err := Unconv(workingDir, file); err != nil {
|
|
||||||
t.Errorf("Converting '%s' to PDF should have worked", file.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses an HTML file type.
|
|
||||||
file = makeFile(workingDir, "file.html")
|
|
||||||
if _, err := Unconv(workingDir, file); err != nil {
|
|
||||||
t.Errorf("Converting '%s' to PDF should have worked", file.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 3: uses an Office file type.
|
|
||||||
file = makeFile(workingDir, "file.docx")
|
|
||||||
if _, err := Unconv(workingDir, file); err != nil {
|
|
||||||
t.Errorf("Converting '%s' to PDF should have worked", file.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 4: uses a PDF file type.
|
|
||||||
file = makeFile(workingDir, "file.pdf")
|
|
||||||
if _, err := Unconv(workingDir, file); err == nil {
|
|
||||||
t.Errorf("Converting '%s' to PDF should not have worked", file.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
load("../../../_tests/configurations/timeout-gotenberg.yml")
|
|
||||||
|
|
||||||
// case 5: uses a command with an unsuitable timeout.
|
|
||||||
file = makeFile(workingDir, "file.docx")
|
|
||||||
if _, err := Unconv(workingDir, makeFile(workingDir, "file.docx")); err == nil {
|
|
||||||
t.Errorf("Converting '%s' to PDF should have reached timeout", file.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.RemoveAll(workingDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
workingDir := "test"
|
|
||||||
os.Mkdir(workingDir, 0666)
|
|
||||||
|
|
||||||
load("../../../_tests/configurations/gotenberg.yml")
|
|
||||||
|
|
||||||
var filesPaths []string
|
|
||||||
path, _ := filepath.Abs("../../../_tests/file.pdf")
|
|
||||||
filesPaths = append(filesPaths, path)
|
|
||||||
filesPaths = append(filesPaths, path)
|
|
||||||
|
|
||||||
// case 1: simple merge.
|
|
||||||
if _, err := Merge(workingDir, filesPaths); err != nil {
|
|
||||||
t.Error("Merge should have worked")
|
|
||||||
}
|
|
||||||
|
|
||||||
load("../../../_tests/configurations/timeout-gotenberg.yml")
|
|
||||||
|
|
||||||
// case 2: uses a command with an unsuitable timeout.
|
|
||||||
if _, err := Merge(workingDir, filesPaths); err == nil {
|
|
||||||
t.Error("Merge should have reached timeout")
|
|
||||||
}
|
|
||||||
|
|
||||||
os.RemoveAll(workingDir)
|
|
||||||
}
|
|
||||||
170
app/handlers.go
170
app/handlers.go
@@ -1,170 +0,0 @@
|
|||||||
// Package app implements all functions on which a request will pass through.
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/context"
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/converter"
|
|
||||||
ghttp "github.com/thecodingmachine/gotenberg/app/http"
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/logger"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/justinas/alice"
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetHandlersChain returns the handlers chaining
|
|
||||||
// thanks to the alice library.
|
|
||||||
func GetHandlersChain() http.Handler {
|
|
||||||
return alice.New(enforceContentLengthHandler, enforceContentTypeHandler, convertHandler).ThenFunc(serveHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestHasNoContentError struct{}
|
|
||||||
|
|
||||||
const requestHasNoContentErrorMessage = "request has not content"
|
|
||||||
|
|
||||||
func (e *requestHasNoContentError) Error() string {
|
|
||||||
return requestHasNoContentErrorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// enforeContentLengthHandler checks if the request has content.
|
|
||||||
func enforceContentLengthHandler(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.ContentLength == 0 {
|
|
||||||
e := &requestHasNoContentError{}
|
|
||||||
http.Error(w, e.Error(), http.StatusBadRequest)
|
|
||||||
logger.Error(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestID := uuid.NewV4().String()
|
|
||||||
r = context.WithRequestID(r, requestID)
|
|
||||||
logger.Infof("identified new request (%s) with %s", humanize.Bytes(uint64(r.ContentLength)), requestID)
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// enforceContentTypeHandler checks if the "Content-Type" entry
|
|
||||||
// from the request's header matches the allowed content type.
|
|
||||||
func enforceContentTypeHandler(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if err := ghttp.CheckAuthorizedContentType(r.Header); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
|
|
||||||
logger.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertHandler is in charge of converting the file(s) from the request to PDF.
|
|
||||||
func convertHandler(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
c, err := converter.NewConverter(r)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*converter.NoFileToConvertError); ok {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
} else {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error(err)
|
|
||||||
|
|
||||||
if c != nil {
|
|
||||||
r = context.WithConverter(r, c)
|
|
||||||
cleanup(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r = context.WithConverter(r, c)
|
|
||||||
|
|
||||||
path, err := c.Convert()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
logger.Error(err)
|
|
||||||
cleanup(r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r = context.WithResultFilePath(r, path)
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveHandler simply serves the created PDF.
|
|
||||||
func serveHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path, err := context.GetResultFilePath(r)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
logger.Error(err)
|
|
||||||
cleanup(r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
logger.Error(err)
|
|
||||||
cleanup(r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
resultFileInfo, err := reader.Stat()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
logger.Error(err)
|
|
||||||
cleanup(r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestID, err := context.GetRequestID(r)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("serving result file %s for request %s...", path, requestID)
|
|
||||||
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", resultFileInfo.Name()))
|
|
||||||
w.Header().Set("Content-Type", "application/pdf")
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", resultFileInfo.Size()))
|
|
||||||
_, err := io.Copy(w, reader)
|
|
||||||
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = <-done
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
logger.Error(err)
|
|
||||||
} else {
|
|
||||||
logger.Infof("result file %s (%s) sent for request %s", path, humanize.Bytes(uint64(resultFileInfo.Size())), requestID)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup removes all files created during the conversion.
|
|
||||||
func cleanup(r *http.Request) {
|
|
||||||
c, err := context.GetConverter(r)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Clear(); err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/config"
|
|
||||||
"github.com/thecodingmachine/gotenberg/app/context"
|
|
||||||
|
|
||||||
"github.com/justinas/alice"
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeRequest(filesPaths ...string) *http.Request {
|
|
||||||
r, w := io.Pipe()
|
|
||||||
mpw := multipart.NewWriter(w)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
var part io.Writer
|
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
if len(filesPaths) == 0 {
|
|
||||||
part, _ = mpw.CreateFormField("foo")
|
|
||||||
part.Write([]byte("bar"))
|
|
||||||
} else {
|
|
||||||
for _, filePath := range filesPaths {
|
|
||||||
file, _ := os.Open(filePath)
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
fileInfo, _ := file.Stat()
|
|
||||||
part, _ = mpw.CreateFormFile("files", fileInfo.Name())
|
|
||||||
io.Copy(part, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mpw.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", r)
|
|
||||||
req.Header.Set("Content-Type", mpw.FormDataContentType())
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
func load(configurationFilePath string) {
|
|
||||||
config.Reset()
|
|
||||||
path, _ := filepath.Abs(configurationFilePath)
|
|
||||||
config.ParseFile(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fakeSuccessHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetHandlersChain(t *testing.T) {
|
|
||||||
// dumb test to improve code coverage...
|
|
||||||
if GetHandlersChain() == nil {
|
|
||||||
t.Errorf("Handler chains should not be nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequestHasNoContentError(t *testing.T) {
|
|
||||||
err := &requestHasNoContentError{}
|
|
||||||
if err.Error() != requestHasNoContentErrorMessage {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), requestHasNoContentErrorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnforceContentLengthHandler(t *testing.T) {
|
|
||||||
var (
|
|
||||||
req *http.Request
|
|
||||||
rr *httptest.ResponseRecorder
|
|
||||||
)
|
|
||||||
|
|
||||||
h := alice.New(enforceContentLengthHandler).ThenFunc(fakeSuccessHandler)
|
|
||||||
|
|
||||||
// case 1: sends an empty request.
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/", nil)
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusBadRequest {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: sends a real body.
|
|
||||||
path, _ := filepath.Abs("../_tests/file.docx")
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, makeRequest(path))
|
|
||||||
if status := rr.Code; status != http.StatusOK {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnforceContentTypeHandler(t *testing.T) {
|
|
||||||
var (
|
|
||||||
req *http.Request
|
|
||||||
rr *httptest.ResponseRecorder
|
|
||||||
)
|
|
||||||
|
|
||||||
h := alice.New(enforceContentTypeHandler).ThenFunc(fakeSuccessHandler)
|
|
||||||
|
|
||||||
// case 1: sends a wrong content type.
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/", nil)
|
|
||||||
req.Header.Set("Content-Type", "application/pdf")
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusUnsupportedMediaType {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusUnsupportedMediaType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: sends a good content type.
|
|
||||||
path, _ := filepath.Abs("../_tests/file.docx")
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, makeRequest(path))
|
|
||||||
if status := rr.Code; status != http.StatusOK {
|
|
||||||
t.Errorf("Handler returned wrong a status code: got '%v' want '%v'", status, http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertHandler(t *testing.T) {
|
|
||||||
var (
|
|
||||||
req *http.Request
|
|
||||||
rr *httptest.ResponseRecorder
|
|
||||||
path string
|
|
||||||
oPath string
|
|
||||||
)
|
|
||||||
|
|
||||||
h := alice.New(convertHandler).ThenFunc(fakeSuccessHandler)
|
|
||||||
|
|
||||||
// case 1: sends a request without body.
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/", nil)
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusInternalServerError {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: sends a request with no file.
|
|
||||||
req = makeRequest()
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusBadRequest {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
load("../_tests/configurations/merge-timeout-gotenberg.yml")
|
|
||||||
|
|
||||||
// case 3: sends a request with two files and using an unsuitable timeout for merge commande.
|
|
||||||
path, _ = filepath.Abs("../_tests/file.pdf")
|
|
||||||
oPath, _ = filepath.Abs("../_tests/file.docx")
|
|
||||||
req = makeRequest(path, oPath)
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusInternalServerError {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
load("../_tests/configurations/gotenberg.yml")
|
|
||||||
|
|
||||||
// case 4: sends a request with two files.
|
|
||||||
path, _ = filepath.Abs("../_tests/file.pdf")
|
|
||||||
oPath, _ = filepath.Abs("../_tests/file.docx")
|
|
||||||
req = makeRequest(path, oPath)
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusOK {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 5: sends five requests (almost) simultany.
|
|
||||||
path, _ = filepath.Abs("../_tests/file.docx")
|
|
||||||
filesPaths := []string{
|
|
||||||
path,
|
|
||||||
path,
|
|
||||||
path,
|
|
||||||
path,
|
|
||||||
path,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(filesPaths); i++ {
|
|
||||||
go func(i int) {
|
|
||||||
req := makeRequest(filesPaths[i])
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusOK {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServeHandler(t *testing.T) {
|
|
||||||
var (
|
|
||||||
req *http.Request
|
|
||||||
rr *httptest.ResponseRecorder
|
|
||||||
)
|
|
||||||
|
|
||||||
h := alice.New().ThenFunc(serveHandler)
|
|
||||||
|
|
||||||
// case 1: sends a request without a result file path entry in its context.
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/", nil)
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusInternalServerError {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: sends a request with a wrong result file path entry in its context.
|
|
||||||
req = context.WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), "file")
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusInternalServerError {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 3: sends a request with a correct result file path entry in its context.
|
|
||||||
path, _ := filepath.Abs("../_tests/file.pdf")
|
|
||||||
req = context.WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), path)
|
|
||||||
rr = httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rr, req)
|
|
||||||
if status := rr.Code; status != http.StatusOK {
|
|
||||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Package http provides functions for detecting a request content type.
|
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ContentType is a string which represents a content type.
|
|
||||||
type ContentType string
|
|
||||||
|
|
||||||
// MultipartFormDataContentType represents... the multipart form data content type.
|
|
||||||
const MultipartFormDataContentType ContentType = "multipart/form-data"
|
|
||||||
|
|
||||||
type notAuthorizedContentTypeError struct{}
|
|
||||||
|
|
||||||
const notAuthorizedContentTypeErrorMessage = "accepted value for 'Content-Type': %s"
|
|
||||||
|
|
||||||
func (e *notAuthorizedContentTypeError) Error() string {
|
|
||||||
return fmt.Sprintf(notAuthorizedContentTypeErrorMessage, MultipartFormDataContentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckAuthorizedContentType checks if the request header header has an authorized content type.
|
|
||||||
// If no authorized content type found, throws an error.
|
|
||||||
func CheckAuthorizedContentType(h http.Header) error {
|
|
||||||
ct := findContentType(h.Get("Content-Type"), MultipartFormDataContentType)
|
|
||||||
if ct == "" {
|
|
||||||
return ¬AuthorizedContentTypeError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// findContentType parses a string representing a content type and tries to find
|
|
||||||
// one of the given content types.
|
|
||||||
func findContentType(requestContentType string, contentTypes ...ContentType) ContentType {
|
|
||||||
for _, ct := range contentTypes {
|
|
||||||
if i := strings.IndexRune(requestContentType, ';'); i != -1 {
|
|
||||||
requestContentType = requestContentType[0:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
if requestContentType == string(ct) {
|
|
||||||
return ct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCheckAuthorizedContentType(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
|
||||||
|
|
||||||
// case 1: uses a request without a content type entry in its header.
|
|
||||||
if err := CheckAuthorizedContentType(req.Header); err == nil {
|
|
||||||
t.Error("Function should not have been able to retrieve an authorized content type from request's header")
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: uses a request with a content type entry in its header.
|
|
||||||
req.Header.Set("Content-Type", string(MultipartFormDataContentType))
|
|
||||||
if err := CheckAuthorizedContentType(req.Header); err != nil {
|
|
||||||
t.Error("Function should have been able to retrieve an authorized content type from request's header")
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 3: uses a request with a composed content type entry in its header.
|
|
||||||
req.Header.Set("Content-Type", "multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW")
|
|
||||||
if err := CheckAuthorizedContentType(req.Header); err != nil {
|
|
||||||
t.Error("Function should have been able to retrieve an authorized content type from request's header")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotAuthorizedContentTypeError(t *testing.T) {
|
|
||||||
err := ¬AuthorizedContentTypeError{}
|
|
||||||
expected := fmt.Sprintf(notAuthorizedContentTypeErrorMessage, MultipartFormDataContentType)
|
|
||||||
|
|
||||||
if err.Error() != expected {
|
|
||||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user