mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
939 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 }}
|
||||
@@ -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
|
||||
93
.github/actions/build-test-push/action.yml
vendored
Normal file
93
.github/actions/build-test-push/action.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
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
|
||||
run: |
|
||||
.github/actions/build-test-push/build.sh \
|
||||
--version "${{ inputs.version }}" \
|
||||
--platform "${{ inputs.platform }}" \
|
||||
--alternate-repository "${{ inputs.alternate_repository }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
|
||||
- name: Run integration tests
|
||||
if: inputs.skip_integrations_tests != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/build-test-push/test.sh \
|
||||
--version "${{ inputs.version }}" \
|
||||
--platform "${{ inputs.platform }}" \
|
||||
--alternate-repository "${{ inputs.alternate_repository }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
|
||||
- name: Push
|
||||
if: inputs.docker_hub_username != ''
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/build-test-push/push.sh \
|
||||
--tags "${{ steps.build.outputs.tags }},${{ steps.build.outputs.tags_cloud_run }},${{ steps.build.outputs.tags_aws_lambda }}" \
|
||||
--dry-run "${{ inputs.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
|
||||
31
.github/actions/clean/action.yml
vendored
Normal file
31
.github/actions/clean/action.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
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 }}
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/clean/clean.sh \
|
||||
--tags "${{ inputs.tags }}" \
|
||||
--snapshot-version "${{ inputs.snapshot_version }}" \
|
||||
--dry-run "${{ inputs.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
|
||||
48
.github/actions/merge/action.yml
vendored
Normal file
48
.github/actions/merge/action.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
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
|
||||
run: |
|
||||
.github/actions/merge/merge.sh \
|
||||
--tags "${{ inputs.tags }}" \
|
||||
--alternate-registry "${{ inputs.alternate_registry }}" \
|
||||
--dry-run "${{ inputs.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.5.0
|
||||
|
||||
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
|
||||
.ci/gotenberg
|
||||
.idea
|
||||
.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
|
||||
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"]
|
||||
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
|
||||
|
||||
Copyright (c) 2018 TheCodingMachine
|
||||
Copyright (c) Julien Neuhart
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
230
Makefile
Normal file
230
Makefile
Normal file
@@ -0,0 +1,230 @@
|
||||
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)
|
||||
|
||||
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=10
|
||||
CHROMIUM_MAX_QUEUE_SIZE=0
|
||||
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_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_ENCRYPT_ENGINES=qpdf,pdfcpu,pdftk
|
||||
PDFENGINES_DISABLE_ROUTES=false
|
||||
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) \
|
||||
$(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-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-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-encrypt-engines=$(PDFENGINES_ENCRYPT_ENGINES) \
|
||||
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
|
||||
--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-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
|
||||
# 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
|
||||
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
|
||||
225
README.md
225
README.md
@@ -1,202 +1,63 @@
|
||||
<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" />
|
||||
</p>
|
||||
<h3 align="center">Gotenberg</h3>
|
||||
<p align="center">A stateless API for converting Markdown files, HTML files and Office documents to PDF</p>
|
||||
<p align="center">
|
||||
<a href="https://microbadger.com/images/thecodingmachine/gotenberg:2.0.0">
|
||||
<img src="https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:2.0.0.svg" alt="MicroBadger layers">
|
||||
</a>
|
||||
<a href="https://travis-ci.org/thecodingmachine/gotenberg">
|
||||
<img src="https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=master" alt="Travis CI">
|
||||
</a>
|
||||
<a href="https://godoc.org/github.com/thecodingmachine/gotenberg">
|
||||
<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>
|
||||
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
|
||||
<h3 align="center">Gotenberg</h3>
|
||||
<p align="center">A containerized API for seamless PDF conversion</p>
|
||||
<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://hub.docker.com/r/thecodingmachine/gotenberg"><img alt="Total downloads (thecodingmachine/gotenberg)" src="https://img.shields.io/docker/pulls/thecodingmachine/gotenberg"></a>
|
||||
<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>
|
||||
<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>
|
||||
</p>
|
||||
<p align="center">
|
||||
<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>
|
||||
</p>
|
||||
<p align="center"><a href="https://gotenberg.dev/docs/getting-started/introduction">Documentation</a> · <a href="https://gotenberg.dev/docs/getting-started/installation#live-demo-">Live Demo</a> 🔥</p>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from
|
||||
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.
|
||||
**Gotenberg** provides a developer-friendly API to interact with powerful tools like Chromium and LibreOffice for converting
|
||||
numerous document formats (HTML, Markdown, Word, Excel, etc.) into PDF files, and more!
|
||||
|
||||
# Menu
|
||||
## Quick Start
|
||||
|
||||
* [Usage](#usage)
|
||||
* [Security](#security)
|
||||
* [Scalability](#scalability)
|
||||
* [Custom implementation](#custom-implementation)
|
||||
* [Clients](#clients)
|
||||
Open a terminal and run the following command:
|
||||
|
||||
## Usage
|
||||
|
||||
Let's say you're starting the API using this simple command:
|
||||
|
||||
```sh
|
||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:2.0.0
|
||||
```
|
||||
docker run --rm -p 3000:3000 gotenberg/gotenberg:8
|
||||
```
|
||||
|
||||
The API is now available on your host under `http://127.0.0.1:3000`.
|
||||
Alternatively, using the historic Docker repository from our sponsor [TheCodingMachine](https://www.thecodingmachine.com):
|
||||
|
||||
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
|
||||
```
|
||||
docker run --rm -p 3000:3000 thecodingmachine/gotenberg:8
|
||||
```
|
||||
|
||||
* Many files
|
||||
The API is now available on your host at http://localhost:3000.
|
||||
|
||||
```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
|
||||
```
|
||||
Head to the [documentation](https://gotenberg.dev/docs/getting-started/introduction) to learn how to interact with it 🚀
|
||||
|
||||
## Security
|
||||
## Sponsors
|
||||
|
||||
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.
|
||||
<p align="center">
|
||||
<a href="https://thecodingmachine.com">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/130324668-9d6e7b35-53a3-49c7-a574-38190d2bd6b0.png" alt="TheCodingMachine Logo" width="333" height="163" />
|
||||
</a>
|
||||
<a href="https://pdfme.com?utm_source=gotenberg_github&utm_medium=website" target="_blank">
|
||||
<img src="https://github.com/user-attachments/assets/2a75dd40-ca18-4d34-acd5-5dd474595168" alt="pdfme Logo" width="333" height="163" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 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
|
||||
docker-compose up --scale gotenberg=your_number_of_instances
|
||||
```
|
||||
|
||||
When requesting the Gotenberg service with your client(s), Docker will automatically redirect a request to a Gotenberg container
|
||||
according to the round-robin strategy.
|
||||
|
||||
## Custom implementation
|
||||
|
||||
The API relies on a simple YAML configuration file called `gotenberg.yml`. It allows you to tweak some values and even provides you
|
||||
a way to change the commands called for each kind of conversion.
|
||||
|
||||
Below the default configuration file:
|
||||
|
||||
```yaml
|
||||
# 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"
|
||||
```
|
||||
|
||||
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)!
|
||||
Sponsorships help maintain and improve Gotenberg - [become a sponsor](https://github.com/sponsors/gulien) ❤️
|
||||
|
||||
---
|
||||
|
||||
Would you like to update this documentation ? Feel free to open an [issue](../../issues).
|
||||
<p align="center">
|
||||
<strong>Powered by</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://jb.gg/OpenSource">
|
||||
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="JetBrains logo" width="200"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// Package logger implements a simple wrapper of the logrus library.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// logger wraps a logrus.Logger instance.
|
||||
type logger struct {
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
// log is our logger instance used across the application.
|
||||
var log = newLogger()
|
||||
|
||||
// newLogger instantiates a logger instance with default values.
|
||||
func newLogger() *logger {
|
||||
l := &logger{logrus.New()}
|
||||
l.logger.Out = os.Stdout
|
||||
l.logger.Level = logrus.InfoLevel
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// SetLevel updates the level of messages which will be logged.
|
||||
func SetLevel(level logrus.Level) {
|
||||
log.logger.SetLevel(level)
|
||||
}
|
||||
|
||||
// SetFormatter updates the output format.
|
||||
// When a TTY is not attached, the output will be in the defined format.
|
||||
func SetFormatter(formatter logrus.Formatter) {
|
||||
log.logger.Formatter = formatter
|
||||
}
|
||||
|
||||
// Debug is a wrapper of the logrus Debug function.
|
||||
func Debug(message string) {
|
||||
log.logger.Debug(message)
|
||||
}
|
||||
|
||||
// Debugf is a wrapper of the logrus Debugf function.
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
log.logger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Info is a wrapper of the logrus Info function.
|
||||
func Info(message string) {
|
||||
log.logger.Info(message)
|
||||
}
|
||||
|
||||
// Infof is a wrapper of the logrus Infof function.
|
||||
func Infof(format string, args ...interface{}) {
|
||||
log.logger.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Warn is a wrapper of the logrus Warn function.
|
||||
func Warn(message string) {
|
||||
log.logger.Warn(message)
|
||||
}
|
||||
|
||||
// Warnf is wrapper of the logrus Warnf function.
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
log.logger.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Error is a wrapper of the logrus Error function.
|
||||
func Error(err error) {
|
||||
log.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
// Fatal is a wrapper of the logrus Fatal function.
|
||||
func Fatal(err error) {
|
||||
log.logger.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// Panic is a wrapper of the logrus Panic function.
|
||||
func Panic(err error) {
|
||||
log.logger.Panic(err.Error())
|
||||
}
|
||||
284
build/Dockerfile
Normal file
284
build/Dockerfile
Normal file
@@ -0,0 +1,284 @@
|
||||
# ARG instructions do not create additional layers. Instead, next layers will
|
||||
# concatenate them. Also, we have to repeat ARG instructions in each build
|
||||
# stage that uses them.
|
||||
ARG GOLANG_VERSION=1.25.5
|
||||
|
||||
# ----------------------------------------------
|
||||
# pdfcpu binary build stage
|
||||
# ----------------------------------------------
|
||||
# Note: this stage is required as pdfcpu does not release an armhf variant by
|
||||
# default.
|
||||
FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage
|
||||
|
||||
# See https://github.com/pdfcpu/pdfcpu/releases.
|
||||
ARG PDFCPU_VERSION=v0.11.1
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
# Define the working directory outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /home
|
||||
|
||||
RUN curl -Ls "https://github.com/pdfcpu/pdfcpu/archive/refs/tags/$PDFCPU_VERSION.tar.gz" -o pdfcpu.tar.gz &&\
|
||||
tar --strip-components=1 -xvzf pdfcpu.tar.gz
|
||||
|
||||
# Install module dependencies.
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
RUN go build -o pdfcpu -ldflags "-s -w -X 'main.version=$PDFCPU_VERSION' -X 'github.com/pdfcpu/pdfcpu/pkg/pdfcpu.VersionStr=$PDFCPU_VERSION' -X main.builtBy=gotenberg" ./cmd/pdfcpu &&\
|
||||
# Verify installation.
|
||||
./pdfcpu version
|
||||
|
||||
# ----------------------------------------------
|
||||
# Gotenberg binary build stage
|
||||
# ----------------------------------------------
|
||||
FROM golang:$GOLANG_VERSION AS gotenberg-binary-stage
|
||||
|
||||
ARG GOTENBERG_VERSION=snapshot
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
# Define the working directory outside of $GOPATH (we're using go modules).
|
||||
WORKDIR /home
|
||||
|
||||
# Install module dependencies.
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download &&\
|
||||
go mod verify
|
||||
|
||||
# Copy the source code.
|
||||
COPY cmd ./cmd
|
||||
COPY pkg ./pkg
|
||||
|
||||
RUN go build -o gotenberg -ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
|
||||
|
||||
# ----------------------------------------------
|
||||
# Custom JRE stage
|
||||
# Credits: https://github.com/jodconverter/docker-image-jodconverter-runtime
|
||||
# ----------------------------------------------
|
||||
FROM debian:13-slim AS custom-jre-stage
|
||||
|
||||
RUN \
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends default-jdk-headless binutils
|
||||
|
||||
# Note: jdeps helps finding which modules a JAR requires.
|
||||
# Currently only for PDFtk, as we don't rely on LibreOffice UNO Java SDK.
|
||||
ENV JAVA_MODULES=java.base,java.desktop,java.naming,java.sql
|
||||
|
||||
RUN jlink \
|
||||
--add-modules $JAVA_MODULES \
|
||||
--strip-debug \
|
||||
--no-man-pages \
|
||||
--no-header-files \
|
||||
--compress=2 \
|
||||
--output /custom-jre
|
||||
|
||||
# ----------------------------------------------
|
||||
# Base image stage
|
||||
# ----------------------------------------------
|
||||
FROM debian:13-slim AS base-image-stage
|
||||
|
||||
COPY --from=custom-jre-stage /custom-jre /opt/java
|
||||
|
||||
ENV PATH="/opt/java/bin:${PATH}"
|
||||
|
||||
# ----------------------------------------------
|
||||
# Final stage
|
||||
# ----------------------------------------------
|
||||
FROM base-image-stage
|
||||
|
||||
ARG GOTENBERG_VERSION=snapshot
|
||||
ARG GOTENBERG_USER_GID=1001
|
||||
ARG GOTENBERG_USER_UID=1001
|
||||
# See https://github.com/googlefonts/noto-emoji/releases.
|
||||
ARG NOTO_COLOR_EMOJI_VERSION=v2.051
|
||||
# See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
|
||||
ARG PDFTK_VERSION=v3.3.3
|
||||
|
||||
LABEL org.opencontainers.image.title="Gotenberg" \
|
||||
org.opencontainers.image.description="A containerized API for seamless PDF conversion." \
|
||||
org.opencontainers.image.version="$GOTENBERG_VERSION" \
|
||||
org.opencontainers.image.authors="Julien Neuhart <neuhart.julien@gmail.com>" \
|
||||
org.opencontainers.image.documentation="https://gotenberg.dev" \
|
||||
org.opencontainers.image.source="https://github.com/gotenberg/gotenberg"
|
||||
|
||||
RUN \
|
||||
# Create a non-root user.
|
||||
# All processes in the Docker container will run with this dedicated user.
|
||||
groupadd --gid "$GOTENBERG_USER_GID" gotenberg &&\
|
||||
useradd --uid "$GOTENBERG_USER_UID" --gid gotenberg --shell /bin/bash --home /home/gotenberg --no-create-home gotenberg &&\
|
||||
mkdir /home/gotenberg &&\
|
||||
chown gotenberg: /home/gotenberg
|
||||
|
||||
RUN \
|
||||
# Install system dependencies required for the next instructions or debugging.
|
||||
# Note: tini is a helper for reaping zombie processes.
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg tini python3 python3-distutils-extra &&\
|
||||
# Cleanup.
|
||||
# Note: the Debian image does automatically a clean after each install thanks to a hook.
|
||||
# Therefore, there is no need for apt-get clean.
|
||||
# See https://stackoverflow.com/a/24417119/3248473.
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN \
|
||||
# Install fonts.
|
||||
# Credits:
|
||||
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile.
|
||||
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html.
|
||||
curl -o ./ttf-mscorefonts-installer_3.8.1_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8.1_all.deb &&\
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
||||
./ttf-mscorefonts-installer_3.8.1_all.deb \
|
||||
culmus \
|
||||
fonts-beng \
|
||||
fonts-hosny-amiri \
|
||||
fonts-lklug-sinhala \
|
||||
fonts-lohit-guru \
|
||||
fonts-lohit-knda \
|
||||
fonts-samyak-gujr \
|
||||
fonts-samyak-mlym \
|
||||
fonts-samyak-taml \
|
||||
fonts-sarai \
|
||||
fonts-sil-abyssinica \
|
||||
fonts-sil-padauk \
|
||||
fonts-telu \
|
||||
fonts-thai-tlwg \
|
||||
ttf-wqy-zenhei \
|
||||
fonts-arphic-ukai \
|
||||
fonts-arphic-uming \
|
||||
fonts-ipafont-mincho \
|
||||
fonts-ipafont-gothic \
|
||||
fonts-unfonts-core \
|
||||
# LibreOffice recommends.
|
||||
fonts-crosextra-caladea \
|
||||
fonts-crosextra-carlito \
|
||||
fonts-dejavu \
|
||||
fonts-liberation \
|
||||
fonts-liberation2 \
|
||||
fonts-linuxlibertine \
|
||||
fonts-noto-cjk \
|
||||
fonts-noto-core \
|
||||
fonts-noto-mono \
|
||||
fonts-noto-ui-core \
|
||||
fonts-sil-gentium \
|
||||
fonts-sil-gentium-basic &&\
|
||||
rm -f ./ttf-mscorefonts-installer_3.8.1_all.deb &&\
|
||||
# Add Color and Black-and-White Noto emoji font.
|
||||
# Credits:
|
||||
# https://github.com/gotenberg/gotenberg/pull/325.
|
||||
# https://github.com/googlefonts/noto-emoji.
|
||||
curl -Ls "https://github.com/googlefonts/noto-emoji/raw/$NOTO_COLOR_EMOJI_VERSION/fonts/NotoColorEmoji.ttf" -o /usr/local/share/fonts/NotoColorEmoji.ttf &&\
|
||||
# Cleanup.
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN \
|
||||
# Install Hyphenation for LibreOffice.
|
||||
# Credits: https://wiki.archlinux.org/title/LibreOffice.
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
||||
hyphen-af hyphen-as hyphen-be hyphen-bg hyphen-bn hyphen-ca hyphen-cs hyphen-da hyphen-de hyphen-el \
|
||||
hyphen-en-gb hyphen-en-us hyphen-eo hyphen-es hyphen-fr hyphen-gl hyphen-gu hyphen-hi hyphen-hr hyphen-hu \
|
||||
hyphen-id hyphen-is hyphen-it hyphen-kn hyphen-lt hyphen-lv hyphen-ml hyphen-mn hyphen-mr hyphen-nl \
|
||||
hyphen-no hyphen-or hyphen-pa hyphen-pl hyphen-pt-br hyphen-pt-pt hyphen-ro hyphen-ru hyphen-sk hyphen-sl \
|
||||
hyphen-sr hyphen-sv hyphen-ta hyphen-te hyphen-th hyphen-uk hyphen-zu &&\
|
||||
# Cleanup.
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN \
|
||||
# Install Chromium.
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium &&\
|
||||
# Verify installation.
|
||||
chromium --version &&\
|
||||
# Cleanup.
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Set default characterset encoding to UTF-8.
|
||||
# See:
|
||||
# https://github.com/gotenberg/gotenberg/issues/104
|
||||
# https://github.com/gotenberg/gotenberg/issues/730
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
RUN \
|
||||
# Install LibreOffice & unoconverter. \
|
||||
echo "deb http://deb.debian.org/debian trixie-backports main" >> /etc/apt/sources.list &&\
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t trixie-backports libreoffice &&\
|
||||
curl -Ls https://raw.githubusercontent.com/gotenberg/unoconverter/v0.1.1/unoconv -o /usr/bin/unoconverter &&\
|
||||
chmod +x /usr/bin/unoconverter &&\
|
||||
# unoconverter will look for the Python binary, which has to be at version 3.
|
||||
ln -s /usr/bin/python3 /usr/bin/python &&\
|
||||
# Verify installations.
|
||||
libreoffice --version &&\
|
||||
unoconverter --version &&\
|
||||
# Cleanup.
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN \
|
||||
# Install PDFtk, QPDF & ExifTool (PDF engines).
|
||||
# See https://github.com/gotenberg/gotenberg/pull/273.
|
||||
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" &&\
|
||||
chmod a+x /usr/bin/pdftk-all.jar &&\
|
||||
printf '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
|
||||
chmod +x /usr/bin/pdftk &&\
|
||||
apt-get update -qq &&\
|
||||
apt-get upgrade -yqq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf exiftool &&\
|
||||
# See https://github.com/nextcloud/docker/issues/380.
|
||||
mkdir -p /usr/share/man/man1 &&\
|
||||
# Verify installations.
|
||||
pdftk --version &&\
|
||||
qpdf --version &&\
|
||||
exiftool --version &&\
|
||||
# Cleanup.
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Support for arbitrary user IDs (OpenShift).
|
||||
# See:
|
||||
# https://github.com/gotenberg/gotenberg/issues/1049.
|
||||
# https://docs.redhat.com/en/documentation/openshift_container_platform/4.15/html/images/creating-images#use-uid_create-images.
|
||||
RUN \
|
||||
usermod -aG root gotenberg &&\
|
||||
chgrp -R 0 /home/gotenberg &&\
|
||||
chmod -R g=u /home/gotenberg
|
||||
|
||||
# Improve fonts subpixel hinting and smoothing.
|
||||
# Credits:
|
||||
# https://github.com/arachnys/athenapdf/issues/69.
|
||||
# https://github.com/arachnys/athenapdf/commit/ba25a8d80a25d08d58865519c4cd8756dc9a336d.
|
||||
COPY build/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
||||
|
||||
# Copy dictionnaries so that hypens work on Chromium.
|
||||
# See https://github.com/gotenberg/gotenberg/issues/1293.
|
||||
COPY --chown=gotenberg:gotenberg build/chromium-hyphen-data /opt/gotenberg/chromium-hyphen-data
|
||||
|
||||
# Copy the Golang binaries.
|
||||
COPY --from=pdfcpu-binary-stage /home/pdfcpu /usr/bin/
|
||||
COPY --from=gotenberg-binary-stage /home/gotenberg /usr/bin/
|
||||
|
||||
# Environment variables required by modules or else.
|
||||
ENV CHROMIUM_BIN_PATH=/usr/bin/chromium
|
||||
ENV CHROMIUM_HYPHEN_DATA_DIR_PATH=/opt/gotenberg/chromium-hyphen-data
|
||||
ENV LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin
|
||||
ENV UNOCONVERTER_BIN_PATH=/usr/bin/unoconverter
|
||||
ENV PDFTK_BIN_PATH=/usr/bin/pdftk
|
||||
ENV QPDF_BIN_PATH=/usr/bin/qpdf
|
||||
ENV EXIFTOOL_BIN_PATH=/usr/bin/exiftool
|
||||
ENV PDFCPU_BIN_PATH=/usr/bin/pdfcpu
|
||||
|
||||
USER gotenberg
|
||||
WORKDIR /home/gotenberg
|
||||
|
||||
# Default API port.
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT [ "/usr/bin/tini", "--" ]
|
||||
CMD [ "gotenberg" ]
|
||||
21
build/Dockerfile.aws-lambda
Normal file
21
build/Dockerfile.aws-lambda
Normal file
@@ -0,0 +1,21 @@
|
||||
ARG DOCKER_REGISTRY
|
||||
ARG DOCKER_REPOSITORY
|
||||
ARG GOTENBERG_VERSION
|
||||
|
||||
FROM $DOCKER_REGISTRY/$DOCKER_REPOSITORY:$GOTENBERG_VERSION
|
||||
|
||||
USER root
|
||||
|
||||
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter
|
||||
|
||||
# AWS.
|
||||
ENV AWS_LWA_PORT=3000
|
||||
ENV AWS_LWA_READINESS_CHECK_PATH=/health
|
||||
ENV AWS_LWA_INVOKE_MODE=buffered
|
||||
|
||||
# Gotenberg.
|
||||
ENV API_PORT_FROM_ENV=AWS_LWA_PORT
|
||||
ENV WEBHOOK_ENABLE_SYNC_MODE=true
|
||||
ENV GOTENBERG_BUILD_DEBUG_DATA=false
|
||||
|
||||
USER gotenberg
|
||||
22
build/Dockerfile.cloudrun
Normal file
22
build/Dockerfile.cloudrun
Normal file
@@ -0,0 +1,22 @@
|
||||
ARG DOCKER_REGISTRY
|
||||
ARG DOCKER_REPOSITORY
|
||||
ARG GOTENBERG_VERSION
|
||||
|
||||
FROM $DOCKER_REGISTRY/$DOCKER_REPOSITORY:$GOTENBERG_VERSION
|
||||
|
||||
USER root
|
||||
|
||||
# For security reasons, the non-root user gotenberg does not own the Tini binary by default.
|
||||
# However, some providers like Cloud Run from Google Cloud cannot start a Docker container in that case.
|
||||
# See https://github.com/gotenberg/gotenberg/issues/90#issuecomment-543551353.
|
||||
RUN chown gotenberg: /usr/bin/tini
|
||||
|
||||
# Gotenberg.
|
||||
ENV API_PORT_FROM_ENV=PORT
|
||||
ENV CHROMIUM_AUTO_START=true
|
||||
ENV LIBREOFFICE_AUTO_START=true
|
||||
ENV WEBHOOK_ENABLE_SYNC_MODE=true
|
||||
ENV GOTENBERG_BUILD_DEBUG_DATA=false
|
||||
ENV LOG_ENABLE_GCP_FIELDS=true
|
||||
|
||||
USER gotenberg
|
||||
File diff suppressed because one or more lines are too long
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-af.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-af.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-as.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-as.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-be.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-be.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-bg.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-bg.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-bn.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-bn.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-cs.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-cs.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-cu.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-cu.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-cy.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-cy.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-da.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-da.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-de-1901.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-de-1901.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-de-1996.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-de-1996.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-de-ch-1901.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-de-ch-1901.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-el.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-el.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-en-gb.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-en-gb.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-en-us.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-en-us.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-es.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-es.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-et.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-et.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-eu.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-eu.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-fr.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-fr.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-ga.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-ga.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-gl.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-gl.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-gu.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-gu.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-hi.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-hi.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-hr.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-hr.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-hu.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-hu.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-hy.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-hy.hyb
Normal file
Binary file not shown.
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-it.hyb
Normal file
BIN
build/chromium-hyphen-data/120.0.6050.0/hyph-it.hyb
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user