Compare commits
1946 Commits
v1.14.0-be
...
v2.12.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
aeb49898e5 | ||
![]() |
a2c8fcd46b | ||
![]() |
357ae92d88 | ||
![]() |
74330623b3 | ||
![]() |
3813dc2e18 | ||
![]() |
3df8be0bc7 | ||
![]() |
cc25cbc026 | ||
![]() |
e98d52830f | ||
![]() |
4903e4290d | ||
![]() |
e1ba1a1898 | ||
![]() |
b29c1e91d1 | ||
![]() |
a03c7701a5 | ||
![]() |
b8c9d1316c | ||
![]() |
a63ef26d38 | ||
![]() |
96b2884458 | ||
![]() |
8543202723 | ||
![]() |
a63904b7af | ||
![]() |
eb27bc9e7d | ||
![]() |
489b24ad65 | ||
![]() |
5349eb2302 | ||
![]() |
a8fd023398 | ||
![]() |
e34d48d913 | ||
![]() |
ee529c2276 | ||
![]() |
3d5e45c20a | ||
![]() |
b8283047ae | ||
![]() |
dad3a1ff28 | ||
![]() |
ce663398e6 | ||
![]() |
f5ec6de047 | ||
![]() |
807f788f92 | ||
![]() |
eaaaa575b8 | ||
![]() |
e21552e053 | ||
![]() |
6a7274c414 | ||
![]() |
35de04a2ce | ||
![]() |
a0c227fe55 | ||
![]() |
057ce29676 | ||
![]() |
982eeb0d24 | ||
![]() |
dfa25343a3 | ||
![]() |
dcfb4494c9 | ||
![]() |
5a1ef27224 | ||
![]() |
6f79ee9877 | ||
![]() |
bc0e420d67 | ||
![]() |
8e34756e6b | ||
![]() |
76951ea482 | ||
![]() |
b5e4aaa778 | ||
![]() |
39998cb34f | ||
![]() |
a771d2afd9 | ||
![]() |
dac3def6b9 | ||
![]() |
674b4a839c | ||
![]() |
037dcb6a11 | ||
![]() |
ad8c60d153 | ||
![]() |
3ea312e136 | ||
![]() |
d2a04743eb | ||
![]() |
b34f9c3b20 | ||
![]() |
fea7b0ec8c | ||
![]() |
0042e3eca4 | ||
![]() |
36db6f3d4b | ||
![]() |
3c633c2015 | ||
![]() |
dd8b51de67 | ||
![]() |
5fe846de1d | ||
![]() |
4711468598 | ||
![]() |
19dfaf1b94 | ||
![]() |
183ea24c9f | ||
![]() |
99693b6d30 | ||
![]() |
c0ad82b695 | ||
![]() |
fd36323d1c | ||
![]() |
4059c83a21 | ||
![]() |
b25c015516 | ||
![]() |
8fa52046e4 | ||
![]() |
15554322dd | ||
![]() |
0ee85aae21 | ||
![]() |
839fb34c8e | ||
![]() |
928580bf4f | ||
![]() |
9cca7aaa08 | ||
![]() |
38560cf13a | ||
![]() |
474ca08ef9 | ||
![]() |
d4fd529e49 | ||
![]() |
2260617447 | ||
![]() |
7c3ba3e518 | ||
![]() |
849e3a10ac | ||
![]() |
bdceeef3fb | ||
![]() |
8630e5f5b6 | ||
![]() |
2312eba5b6 | ||
![]() |
ad9d654886 | ||
![]() |
fa19a8975e | ||
![]() |
8987cd448f | ||
![]() |
a7536e3ebf | ||
![]() |
9c3bc2eb83 | ||
![]() |
d179efbd48 | ||
![]() |
801df5f7bd | ||
![]() |
d1c3ea7faa | ||
![]() |
dcfc53b7f2 | ||
![]() |
45002f8083 | ||
![]() |
722a2ca1e4 | ||
![]() |
2bd8b67d02 | ||
![]() |
637efd5cb3 | ||
![]() |
ad3dd76c2f | ||
![]() |
75aba12589 | ||
![]() |
ec33edb2f4 | ||
![]() |
4b706fa4dd | ||
![]() |
98799d9a69 | ||
![]() |
b80070ac0b | ||
![]() |
6b2e5559ca | ||
![]() |
82340ad6e3 | ||
![]() |
f45ab723ae | ||
![]() |
8e3ca37b05 | ||
![]() |
aef387ed69 | ||
![]() |
b93c970635 | ||
![]() |
a7d8b5c960 | ||
![]() |
56c9a3f270 | ||
![]() |
0c3dac45b5 | ||
![]() |
6965165c76 | ||
![]() |
73d33ff25a | ||
![]() |
df153be30e | ||
![]() |
9950ff2337 | ||
![]() |
9a9ab85baf | ||
![]() |
eaea42334b | ||
![]() |
de8ac013ee | ||
![]() |
9e2bf4820a | ||
![]() |
9af879a2bf | ||
![]() |
ff4203938b | ||
![]() |
a63f8809fa | ||
![]() |
edcde1f142 | ||
![]() |
6dc094f760 | ||
![]() |
186f520819 | ||
![]() |
0365fc5ac3 | ||
![]() |
61811a4bec | ||
![]() |
bb83c1eb0a | ||
![]() |
4ad4862641 | ||
![]() |
c03aa03ac2 | ||
![]() |
ada283441c | ||
![]() |
3cf73a77ac | ||
![]() |
71fedcb466 | ||
![]() |
1b9cf5121b | ||
![]() |
7fe76656f2 | ||
![]() |
0deb8a11d6 | ||
![]() |
064d384d97 | ||
![]() |
5045d06744 | ||
![]() |
c57aa81d15 | ||
![]() |
d35e350c79 | ||
![]() |
dd878c8d70 | ||
![]() |
9cd1945a89 | ||
![]() |
faab8a5560 | ||
![]() |
fcc9847bc3 | ||
![]() |
a64d457c30 | ||
![]() |
e799d757c2 | ||
![]() |
ac0ed0def8 | ||
![]() |
f01283c309 | ||
![]() |
a3c468a004 | ||
![]() |
3435ffd00c | ||
![]() |
4f1185c65d | ||
![]() |
2b1498cc6d | ||
![]() |
0643db4347 | ||
![]() |
29e6371cd1 | ||
![]() |
80c2d90e74 | ||
![]() |
f3cf608caa | ||
![]() |
c6d0557a3b | ||
![]() |
f3b7ae93f0 | ||
![]() |
e4265d0594 | ||
![]() |
deda49c204 | ||
![]() |
276abc1404 | ||
![]() |
6defe24ae7 | ||
![]() |
6ed5d11758 | ||
![]() |
9d34327a6d | ||
![]() |
63f164d099 | ||
![]() |
0f9710dc8f | ||
![]() |
cccba47bd7 | ||
![]() |
91585a1fa6 | ||
![]() |
3bb6a32ab9 | ||
![]() |
31f592453e | ||
![]() |
56f5f93c48 | ||
![]() |
e6aefd1063 | ||
![]() |
6187ee82af | ||
![]() |
a066ccff4f | ||
![]() |
f73be01897 | ||
![]() |
07ee25be06 | ||
![]() |
4347c87e92 | ||
![]() |
807f0f1345 | ||
![]() |
12857890cc | ||
![]() |
2ad0f8325c | ||
![]() |
6aae8bf440 | ||
![]() |
5c7522b423 | ||
![]() |
37e607abb9 | ||
![]() |
8045f3d58c | ||
![]() |
a796e58a94 | ||
![]() |
9d4e2d4652 | ||
![]() |
28db7e84e6 | ||
![]() |
22a6360edf | ||
![]() |
61485b0f1d | ||
![]() |
fa7a5451db | ||
![]() |
70069cd502 | ||
![]() |
9e8b96cd34 | ||
![]() |
d03058e539 | ||
![]() |
c929a18da2 | ||
![]() |
5bd248578a | ||
![]() |
ebfb72a691 | ||
![]() |
fc440d8317 | ||
![]() |
b6f6d524d6 | ||
![]() |
f225f72145 | ||
![]() |
d9002005b1 | ||
![]() |
6ddb62bf3f | ||
![]() |
d1ac15baa9 | ||
![]() |
81e4092f53 | ||
![]() |
d8c96b6e4a | ||
![]() |
3d6aa8a656 | ||
![]() |
6d2ae3df1f | ||
![]() |
de7c22e8d6 | ||
![]() |
74c44fe418 | ||
![]() |
a6407d64e9 | ||
![]() |
e553e872df | ||
![]() |
e9e3ec5597 | ||
![]() |
3dbf2e73f9 | ||
![]() |
33c0b692e6 | ||
![]() |
4abc185a13 | ||
![]() |
43ede21c35 | ||
![]() |
5fc6736666 | ||
![]() |
d2883b83c5 | ||
![]() |
085447e7c4 | ||
![]() |
04f52f553a | ||
![]() |
d558367642 | ||
![]() |
324a2aa1c6 | ||
![]() |
617bb30f29 | ||
![]() |
d6191d2f2b | ||
![]() |
f7347bdb69 | ||
![]() |
ce3d5b0065 | ||
![]() |
235b0a4c33 | ||
![]() |
fdf873ad6a | ||
![]() |
b996022003 | ||
![]() |
6d4897a1b8 | ||
![]() |
180b32651d | ||
![]() |
3d56a56eb8 | ||
![]() |
2b85e812f8 | ||
![]() |
e6b856e13f | ||
![]() |
02ebcd29ee | ||
![]() |
719f76060b | ||
![]() |
8abb0cd75d | ||
![]() |
c0c44b512c | ||
![]() |
3e62f13f96 | ||
![]() |
2116964f67 | ||
![]() |
f7ce32f471 | ||
![]() |
d87208be51 | ||
![]() |
bb8ee1e5fb | ||
![]() |
2a0c03eda0 | ||
![]() |
3061c59c06 | ||
![]() |
79067041dd | ||
![]() |
622f624132 | ||
![]() |
3facdefa40 | ||
![]() |
e283bbe5c2 | ||
![]() |
c4f9828a10 | ||
![]() |
cb160212d4 | ||
![]() |
3fa448ecb5 | ||
![]() |
019a255753 | ||
![]() |
50a6181e48 | ||
![]() |
5ff791e4c5 | ||
![]() |
317a9114eb | ||
![]() |
2597d312ed | ||
![]() |
0e95b0a64b | ||
![]() |
853c745039 | ||
![]() |
ed05b40ba4 | ||
![]() |
97eec44647 | ||
![]() |
45138a1881 | ||
![]() |
67565ea1ff | ||
![]() |
05a240b6ed | ||
![]() |
4c6faa698b | ||
![]() |
654685873a | ||
![]() |
2ac5407dd4 | ||
![]() |
76ddc09dba | ||
![]() |
f45daa9445 | ||
![]() |
953ba9160e | ||
![]() |
64de6b8571 | ||
![]() |
5455850168 | ||
![]() |
e91af06189 | ||
![]() |
779f091c04 | ||
![]() |
0627c7f43e | ||
![]() |
302bc9e9f6 | ||
![]() |
a1e4365ff2 | ||
![]() |
7983487430 | ||
![]() |
ac666df4ce | ||
![]() |
68ca27c27c | ||
![]() |
52350f8b51 | ||
![]() |
6fa3522618 | ||
![]() |
84c3e7893e | ||
![]() |
74b850423f | ||
![]() |
43a6e3985d | ||
![]() |
83e3f8efb8 | ||
![]() |
8c93d1db42 | ||
![]() |
5b8cd96f37 | ||
![]() |
5fec764018 | ||
![]() |
22c8d8ef2a | ||
![]() |
9b3a29cddd | ||
![]() |
d461dcbe29 | ||
![]() |
3e22f033c7 | ||
![]() |
e7a5ebc64c | ||
![]() |
e1f5edc0a1 | ||
![]() |
48092d47c5 | ||
![]() |
d4d0604da2 | ||
![]() |
f7db5f3821 | ||
![]() |
ddb65d371a | ||
![]() |
e17b91b87c | ||
![]() |
47ce797ee9 | ||
![]() |
f8057ed4f1 | ||
![]() |
d3ff0ff8e0 | ||
![]() |
6ea25a96a3 | ||
![]() |
caec0ed4d1 | ||
![]() |
ce08400f4e | ||
![]() |
076b5b1af5 | ||
![]() |
44ed78b442 | ||
![]() |
3bd6a6fcfa | ||
![]() |
4fa08a9c96 | ||
![]() |
8ea3259fe7 | ||
![]() |
fae2399e46 | ||
![]() |
0d49314593 | ||
![]() |
fda4742e86 | ||
![]() |
190b648c72 | ||
![]() |
22e88046bc | ||
![]() |
06447c72c5 | ||
![]() |
7a6fe2da7c | ||
![]() |
f04cf1a974 | ||
![]() |
1ebce6f3e0 | ||
![]() |
e07777e38a | ||
![]() |
2c69d0fd2e | ||
![]() |
b58c114a76 | ||
![]() |
9aee6f5a78 | ||
![]() |
54edad29ba | ||
![]() |
0bf711259a | ||
![]() |
1b6250ae24 | ||
![]() |
f2b3521e6c | ||
![]() |
c2944402fa | ||
![]() |
a0f1f6faa1 | ||
![]() |
eaec0014c5 | ||
![]() |
571f3444d1 | ||
![]() |
57032e234c | ||
![]() |
321adaeb8b | ||
![]() |
5d937cf639 | ||
![]() |
3e7656e1e1 | ||
![]() |
93555cf2e7 | ||
![]() |
f60c201eb9 | ||
![]() |
78af59ec17 | ||
![]() |
b305372ed1 | ||
![]() |
16b8b58533 | ||
![]() |
5802163a0e | ||
![]() |
b403b9d9d5 | ||
![]() |
c6e7d06bb7 | ||
![]() |
40289cd714 | ||
![]() |
2de9d1b7ae | ||
![]() |
39b57f695a | ||
![]() |
f503cd8758 | ||
![]() |
8d516c08f0 | ||
![]() |
8b4fc02955 | ||
![]() |
6c24686509 | ||
![]() |
7be7185418 | ||
![]() |
63e1f9f5d3 | ||
![]() |
bd4476d484 | ||
![]() |
7a0334f353 | ||
![]() |
d03e48ea88 | ||
![]() |
342e6d4679 | ||
![]() |
584f1361ad | ||
![]() |
05b1ff9738 | ||
![]() |
d65fcf70f3 | ||
![]() |
a5d3d51cc5 | ||
![]() |
f4489ca2e7 | ||
![]() |
e40893e74f | ||
![]() |
d002ae2e05 | ||
![]() |
bf430865b4 | ||
![]() |
a47d36f5e5 | ||
![]() |
4392628bd7 | ||
![]() |
6d25eb26a1 | ||
![]() |
95fd1ae879 | ||
![]() |
78f338484f | ||
![]() |
40db1065dc | ||
![]() |
c644e57533 | ||
![]() |
b720aa3cd1 | ||
![]() |
e837f1e85b | ||
![]() |
ea2012bc81 | ||
![]() |
8e39315586 | ||
![]() |
ea8127202d | ||
![]() |
f009d9868e | ||
![]() |
1bbcd0961b | ||
![]() |
4fa2b54aed | ||
![]() |
7281c110c6 | ||
![]() |
f812f2af4d | ||
![]() |
47b4a602a7 | ||
![]() |
21c7675f66 | ||
![]() |
ca73c0d1f3 | ||
![]() |
7f6a50be5b | ||
![]() |
10e10f9ff4 | ||
![]() |
95c24a50f7 | ||
![]() |
d06faa2fcb | ||
![]() |
bed66cced0 | ||
![]() |
ceaf60e6ad | ||
![]() |
9885ca5103 | ||
![]() |
2f22beaaee | ||
![]() |
fb2c6282a4 | ||
![]() |
8c5b5d3948 | ||
![]() |
4e5135fe70 | ||
![]() |
579c35a3fe | ||
![]() |
4aedcb856d | ||
![]() |
0b34e70f6c | ||
![]() |
7afc91e7b1 | ||
![]() |
56b17ce6a2 | ||
![]() |
954912cac3 | ||
![]() |
e46f6b1156 | ||
![]() |
1d85caa8d0 | ||
![]() |
622fcf96a0 | ||
![]() |
654cc05f0e | ||
![]() |
974dd24e69 | ||
![]() |
fe824e0faa | ||
![]() |
377d89ae06 | ||
![]() |
629e24e031 | ||
![]() |
38414025c8 | ||
![]() |
1dc5b7a707 | ||
![]() |
f076418c50 | ||
![]() |
bbaad2cdfb | ||
![]() |
ef01658335 | ||
![]() |
9f4a6c3b42 | ||
![]() |
5450bfb67b | ||
![]() |
ae2b302962 | ||
![]() |
957691c454 | ||
![]() |
6b17ba2934 | ||
![]() |
4d3616cda9 | ||
![]() |
c4a9697e02 | ||
![]() |
c57b7520b9 | ||
![]() |
46bd09227f | ||
![]() |
971f92a05c | ||
![]() |
2c43b06910 | ||
![]() |
0f8b2e69c9 | ||
![]() |
00b04c2e86 | ||
![]() |
b3c66cae06 | ||
![]() |
6a79d417b4 | ||
![]() |
98ef68f720 | ||
![]() |
ed3b7aa8f2 | ||
![]() |
e536600052 | ||
![]() |
bb820a2127 | ||
![]() |
129933ff30 | ||
![]() |
41fc11efff | ||
![]() |
a712bc72ca | ||
![]() |
fbe7acc6b0 | ||
![]() |
c4153b6fbf | ||
![]() |
1f355a22e0 | ||
![]() |
4af8070450 | ||
![]() |
d6d0071175 | ||
![]() |
ef51633b2c | ||
![]() |
d4963b9cbe | ||
![]() |
01dabf7c05 | ||
![]() |
fc68f79cc8 | ||
![]() |
ebe1479503 | ||
![]() |
8c9fe4da06 | ||
![]() |
b2ef51af55 | ||
![]() |
32b35d8e4b | ||
![]() |
c8bda18cf2 | ||
![]() |
0a944975cc | ||
![]() |
48eaa31ecf | ||
![]() |
1540e88a06 | ||
![]() |
b1aa57abcb | ||
![]() |
df359730fe | ||
![]() |
43ec154bc2 | ||
![]() |
2c4a664df4 | ||
![]() |
a196c14a58 | ||
![]() |
6f549506d6 | ||
![]() |
8d463e05ae | ||
![]() |
373c91911d | ||
![]() |
1d3ac99c02 | ||
![]() |
cda4c8f87e | ||
![]() |
ef4f589094 | ||
![]() |
3aeb45bf34 | ||
![]() |
b91da77a8a | ||
![]() |
33357a3fc2 | ||
![]() |
025001499d | ||
![]() |
ccd6ad9936 | ||
![]() |
979fcb0570 | ||
![]() |
58afec98f1 | ||
![]() |
91434a5c6f | ||
![]() |
37c4545444 | ||
![]() |
7b7b257725 | ||
![]() |
1eff4b306f | ||
![]() |
700af8caa2 | ||
![]() |
90b43e154a | ||
![]() |
a3892302b0 | ||
![]() |
d2ee319684 | ||
![]() |
5a6923a9aa | ||
![]() |
0a634883a7 | ||
![]() |
428f9cd761 | ||
![]() |
d828c1a2ff | ||
![]() |
25b49db7c0 | ||
![]() |
55a40708a6 | ||
![]() |
dae5bca883 | ||
![]() |
fc74da9b82 | ||
![]() |
2b006907d5 | ||
![]() |
1ba1afdce5 | ||
![]() |
a98317c52a | ||
![]() |
ffddd0f323 | ||
![]() |
4cb2f0acef | ||
![]() |
98663e902f | ||
![]() |
0f5e935214 | ||
![]() |
b9636a3def | ||
![]() |
35574f3b86 | ||
![]() |
00a8f0cd6e | ||
![]() |
6779042242 | ||
![]() |
6379e7b54f | ||
![]() |
2fa742c94b | ||
![]() |
aa0da2f516 | ||
![]() |
f07441a408 | ||
![]() |
f6084acfc8 | ||
![]() |
23ceb2a5ec | ||
![]() |
a698791059 | ||
![]() |
41c1f38ab2 | ||
![]() |
83c85dc10e | ||
![]() |
a020d807d4 | ||
![]() |
464ee51de8 | ||
![]() |
c57c1d5389 | ||
![]() |
af16bb3934 | ||
![]() |
fba416e8e1 | ||
![]() |
3d8de50b5a | ||
![]() |
86263a52ea | ||
![]() |
754627681c | ||
![]() |
bf11dc8d1b | ||
![]() |
84721b001f | ||
![]() |
f48a20c75f | ||
![]() |
16f4552e0e | ||
![]() |
86811d0733 | ||
![]() |
d2f9b5d5e5 | ||
![]() |
f5e1675107 | ||
![]() |
ae016cae4b | ||
![]() |
c7f6d03508 | ||
![]() |
955f2d0db9 | ||
![]() |
ba32684df6 | ||
![]() |
2f7adf40ac | ||
![]() |
a52031161b | ||
![]() |
3e2c541b7b | ||
![]() |
967fc98090 | ||
![]() |
22e95f45bd | ||
![]() |
38c777ec0f | ||
![]() |
ccbc97399a | ||
![]() |
f43013a746 | ||
![]() |
1335ab5f1b | ||
![]() |
90b4691f16 | ||
![]() |
f053ee3191 | ||
![]() |
86748c1e96 | ||
![]() |
22ded7d4c3 | ||
![]() |
ff5063849a | ||
![]() |
ec49284274 | ||
![]() |
6bd5c34b54 | ||
![]() |
db0a2eb1a3 | ||
![]() |
4948438378 | ||
![]() |
76064178f5 | ||
![]() |
c772bd94b0 | ||
![]() |
7f8f7fbb15 | ||
![]() |
388d821f45 | ||
![]() |
9c15623a89 | ||
![]() |
966eb00de0 | ||
![]() |
1c699278a3 | ||
![]() |
4c6c976f63 | ||
![]() |
ebc9ce17b5 | ||
![]() |
8039ce3c2b | ||
![]() |
385d48f644 | ||
![]() |
f682fe25fc | ||
![]() |
7924bf8611 | ||
![]() |
d75b909d28 | ||
![]() |
47dfe85a7c | ||
![]() |
4d0e8a338f | ||
![]() |
cfc64d37bb | ||
![]() |
2db66280cc | ||
![]() |
9f045f4494 | ||
![]() |
4fdb28c8d6 | ||
![]() |
f1049cf889 | ||
![]() |
8d664fad56 | ||
![]() |
f6ddcfa839 | ||
![]() |
ce59f2ad5e | ||
![]() |
0de00a4ac1 | ||
![]() |
bec72dffeb | ||
![]() |
463e95367c | ||
![]() |
1739de2694 | ||
![]() |
fd8db27a88 | ||
![]() |
9ddf14bebe | ||
![]() |
1cf8ea3aba | ||
![]() |
134993fce6 | ||
![]() |
ff1955e014 | ||
![]() |
d83bbdc50b | ||
![]() |
907b6d1294 | ||
![]() |
4e7bb1c8da | ||
![]() |
09ab694d05 | ||
![]() |
03ced65d5f | ||
![]() |
01d919cf31 | ||
![]() |
8d5f331e63 | ||
![]() |
ed556ead6f | ||
![]() |
e10a904f33 | ||
![]() |
f2a05b61da | ||
![]() |
21f96f0679 | ||
![]() |
2ffabd54e5 | ||
![]() |
1197437750 | ||
![]() |
d1339374d0 | ||
![]() |
5ba4b9d6b2 | ||
![]() |
b386ea9426 | ||
![]() |
197174f400 | ||
![]() |
97dceba783 | ||
![]() |
2ed4400827 | ||
![]() |
58cbcbd6ef | ||
![]() |
4aeb2e1a74 | ||
![]() |
45c5f81b34 | ||
![]() |
13201dbfff | ||
![]() |
0b1523f4e5 | ||
![]() |
cd3b1a221e | ||
![]() |
4855f4b8b1 | ||
![]() |
6587470033 | ||
![]() |
6487dab132 | ||
![]() |
b643a68fa3 | ||
![]() |
b60e16fe33 | ||
![]() |
c508be6ecd | ||
![]() |
3b2d4fe876 | ||
![]() |
b47f301831 | ||
![]() |
a79b9de1a2 | ||
![]() |
e98da2e72c | ||
![]() |
718171a125 | ||
![]() |
aaa130e20d | ||
![]() |
4606caeaa8 | ||
![]() |
4813a7bc70 | ||
![]() |
fb82aa0ee1 | ||
![]() |
c7e0c32226 | ||
![]() |
607adf44f3 | ||
![]() |
625780899d | ||
![]() |
25542c56b9 | ||
![]() |
45e2b7f814 | ||
![]() |
6b34f592df | ||
![]() |
6cf732e6ec | ||
![]() |
dfd959839f | ||
![]() |
d165e89ac3 | ||
![]() |
421a87c94b | ||
![]() |
b55529b913 | ||
![]() |
c62d892969 | ||
![]() |
9e6aa55230 | ||
![]() |
6090305b77 | ||
![]() |
d1b516a089 | ||
![]() |
89aff63e52 | ||
![]() |
11e9c4d8cc | ||
![]() |
9d84e95771 | ||
![]() |
2aced1c305 | ||
![]() |
454098630b | ||
![]() |
8f3ab2791b | ||
![]() |
61209b1057 | ||
![]() |
38a817e887 | ||
![]() |
5e3d1b26e7 | ||
![]() |
4996b7e5f7 | ||
![]() |
2c8fddb554 | ||
![]() |
ae05011062 | ||
![]() |
50a6b7e154 | ||
![]() |
d0ce4113e0 | ||
![]() |
d55900b877 | ||
![]() |
2a73ab4693 | ||
![]() |
2aea220c6d | ||
![]() |
b0c305e852 | ||
![]() |
73a77d2a45 | ||
![]() |
3a011e7c04 | ||
![]() |
d48b75d862 | ||
![]() |
f6e26d5953 | ||
![]() |
7863780883 | ||
![]() |
c2ac9a26a2 | ||
![]() |
58e8f796d1 | ||
![]() |
ba0f4718e5 | ||
![]() |
6fb4daf03e | ||
![]() |
f6c34494a7 | ||
![]() |
1e4d284b30 | ||
![]() |
1f3406fd77 | ||
![]() |
398faf36fc | ||
![]() |
ce841d4196 | ||
![]() |
c77f8acf41 | ||
![]() |
212674f9df | ||
![]() |
283ced56d1 | ||
![]() |
530e57151d | ||
![]() |
1141c3f361 | ||
![]() |
49416d3372 | ||
![]() |
88ae60a4a0 | ||
![]() |
6651c80fb9 | ||
![]() |
6d6650d5f6 | ||
![]() |
6df252c99b | ||
![]() |
5881f05dbc | ||
![]() |
00eba3b223 | ||
![]() |
f7ab8d23a7 | ||
![]() |
85b596d20d | ||
![]() |
4d43f6b63d | ||
![]() |
ea1eb551a7 | ||
![]() |
5842944d1e | ||
![]() |
5781a0d51f | ||
![]() |
e5f48739a0 | ||
![]() |
d378c861f6 | ||
![]() |
9466bfdb00 | ||
![]() |
f02e8e0dc3 | ||
![]() |
c1ed87a44f | ||
![]() |
16169ca331 | ||
![]() |
26900e0766 | ||
![]() |
aa798604b3 | ||
![]() |
bb98fc5f65 | ||
![]() |
dc1918ad10 | ||
![]() |
ea632d0417 | ||
![]() |
648dc709fd | ||
![]() |
1a84f6a20e | ||
![]() |
96af953e6f | ||
![]() |
6db9e292ba | ||
![]() |
2e2362e2df | ||
![]() |
51dd95be3d | ||
![]() |
e16645b146 | ||
![]() |
0068f091bb | ||
![]() |
ad6efd2898 | ||
![]() |
86e380bb1c | ||
![]() |
58aacd4814 | ||
![]() |
ad07791bac | ||
![]() |
783090c2cd | ||
![]() |
41a3c7c89b | ||
![]() |
16cc7415c1 | ||
![]() |
98c5cf89ef | ||
![]() |
53e04e66cf | ||
![]() |
2a6e79acc8 | ||
![]() |
2da5e46386 | ||
![]() |
4dbf8d7969 | ||
![]() |
4a52fc27d4 | ||
![]() |
05cd34c8af | ||
![]() |
8a622181fc | ||
![]() |
13f38bf3a1 | ||
![]() |
16acc2d6ad | ||
![]() |
c2c9a953d3 | ||
![]() |
530f4a8b28 | ||
![]() |
8eb1dc4f62 | ||
![]() |
a2b87fe012 | ||
![]() |
3dcb973adb | ||
![]() |
8e8810cbaa | ||
![]() |
b0aeec4c43 | ||
![]() |
1ac298f6ff | ||
![]() |
6d5f4e92cc | ||
![]() |
416ad13aaf | ||
![]() |
a7e1299194 | ||
![]() |
a12e1fae72 | ||
![]() |
f525ac0af6 | ||
![]() |
58bf9c552b | ||
![]() |
22d257cd1f | ||
![]() |
f1bf1ddc54 | ||
![]() |
6015cc0e4a | ||
![]() |
f9926d77d5 | ||
![]() |
4f85dcecfc | ||
![]() |
30c31a3d4c | ||
![]() |
c64667d396 | ||
![]() |
9f6613fe05 | ||
![]() |
ea47af7034 | ||
![]() |
d46abeff01 | ||
![]() |
2b39697ffb | ||
![]() |
4b00a72ff5 | ||
![]() |
e590b2482e | ||
![]() |
eb7dd80410 | ||
![]() |
86338465fb | ||
![]() |
a41dbdd12c | ||
![]() |
1e10a438cd | ||
![]() |
ab34ea724d | ||
![]() |
fd8bfe1a80 | ||
![]() |
9043f45350 | ||
![]() |
5921e6d13e | ||
![]() |
ee2bfe2350 | ||
![]() |
0957a7ca8e | ||
![]() |
f4e75c7fb7 | ||
![]() |
fae0e3b405 | ||
![]() |
ef335517ce | ||
![]() |
e2be166e67 | ||
![]() |
37e34d92de | ||
![]() |
bd35030c59 | ||
![]() |
a82e3771ae | ||
![]() |
3115106dc1 | ||
![]() |
d623af9c41 | ||
![]() |
355a434a07 | ||
![]() |
8da2535a65 | ||
![]() |
5963dfe41b | ||
![]() |
c6dcaa0472 | ||
![]() |
21063a5c22 | ||
![]() |
ba2f51bed1 | ||
![]() |
bbf64b7e93 | ||
![]() |
3b6ce16f1c | ||
![]() |
46e6be319f | ||
![]() |
e6d6f21d33 | ||
![]() |
77b9b79a9e | ||
![]() |
f0016ad70c | ||
![]() |
054468ffc2 | ||
![]() |
607c1282e3 | ||
![]() |
3f4f4444f7 | ||
![]() |
54372b5618 | ||
![]() |
670a3f6c7f | ||
![]() |
35a4d3fb54 | ||
![]() |
fb81612ed1 | ||
![]() |
c5d622279c | ||
![]() |
b93f655039 | ||
![]() |
428ffb4729 | ||
![]() |
061f33fb05 | ||
![]() |
da058b915b | ||
![]() |
cf869b1356 | ||
![]() |
05e294fc81 | ||
![]() |
bd904d9e6b | ||
![]() |
a7ac719711 | ||
![]() |
370f6ddb3b | ||
![]() |
3861e84f89 | ||
![]() |
76001105b8 | ||
![]() |
d47cc9460b | ||
![]() |
fbeb03c377 | ||
![]() |
2b13fa4712 | ||
![]() |
98255f8e14 | ||
![]() |
eaeeb6447f | ||
![]() |
e6a9868e86 | ||
![]() |
f03592d17d | ||
![]() |
e5db44bc2b | ||
![]() |
ffad42615f | ||
![]() |
5576a073a5 | ||
![]() |
151d337f6c | ||
![]() |
74e89b0ee3 | ||
![]() |
945fb675e9 | ||
![]() |
7570945cf0 | ||
![]() |
d94d80a8c8 | ||
![]() |
8a0a49dd57 | ||
![]() |
66b2d90c50 | ||
![]() |
5723bd8dd8 | ||
![]() |
9b08ce1761 | ||
![]() |
a6248bec2d | ||
![]() |
b1f6f52486 | ||
![]() |
638d9970fd | ||
![]() |
5e8de4c1da | ||
![]() |
088bad9030 | ||
![]() |
cfa908243b | ||
![]() |
cabeb00632 | ||
![]() |
2426c01978 | ||
![]() |
80f2bee6e8 | ||
![]() |
7ec51758eb | ||
![]() |
9e93ae952a | ||
![]() |
829836ddf6 | ||
![]() |
1197a048bc | ||
![]() |
7289c4ea56 | ||
![]() |
55dadf0b00 | ||
![]() |
341815cc03 | ||
![]() |
d22b27afe7 | ||
![]() |
6ee2d023d5 | ||
![]() |
5010cc6f15 | ||
![]() |
383cced158 | ||
![]() |
a3d6967192 | ||
![]() |
c5881f75c9 | ||
![]() |
c4b7429e99 | ||
![]() |
b1eced3612 | ||
![]() |
9d5b07537d | ||
![]() |
122e4141b0 | ||
![]() |
be2de4f15d | ||
![]() |
92a920021d | ||
![]() |
72000cac36 | ||
![]() |
4510902677 | ||
![]() |
c2b9d2fa7b | ||
![]() |
cd38c39908 | ||
![]() |
9016a1e6df | ||
![]() |
627254d5a7 | ||
![]() |
ff31558252 | ||
![]() |
9454978264 | ||
![]() |
e2d25a7a09 | ||
![]() |
85f824f032 | ||
![]() |
1a48910e6b | ||
![]() |
bffd5829d0 | ||
![]() |
7e12bd1bef | ||
![]() |
af0817ab74 | ||
![]() |
fbf1a051a2 | ||
![]() |
7ecf7f704a | ||
![]() |
7b7a74d821 | ||
![]() |
e4acc33519 | ||
![]() |
2fd141d914 | ||
![]() |
aad814d342 | ||
![]() |
7e21aaec17 | ||
![]() |
6d5fdfe2e2 | ||
![]() |
5942cd6cd2 | ||
![]() |
5cd17e71e2 | ||
![]() |
2f2ecaa61e | ||
![]() |
aa858a35e2 | ||
![]() |
d658150d42 | ||
![]() |
c312149b35 | ||
![]() |
18a9a3df12 | ||
![]() |
0cdd3581c9 | ||
![]() |
cae79b811f | ||
![]() |
6d953babcb | ||
![]() |
975e5f3fd0 | ||
![]() |
6cfe92bed1 | ||
![]() |
a51c0850c8 | ||
![]() |
03415456bf | ||
![]() |
0309a0fae1 | ||
![]() |
b48910bb94 | ||
![]() |
9a89786dd3 | ||
![]() |
15a5261189 | ||
![]() |
f616da3b85 | ||
![]() |
0e9cf016ec | ||
![]() |
4481f12e32 | ||
![]() |
66efaedcbb | ||
![]() |
771c1fab92 | ||
![]() |
8d6e7ed477 | ||
![]() |
5d80511b9b | ||
![]() |
90f90dc9b4 | ||
![]() |
a58e8498aa | ||
![]() |
ca355d5855 | ||
![]() |
826322b610 | ||
![]() |
80ff5677ea | ||
![]() |
62c417cd51 | ||
![]() |
f27f25aa03 | ||
![]() |
285a4b5aef | ||
![]() |
47a2ded30d | ||
![]() |
5b502b1e1a | ||
![]() |
aff56077a8 | ||
![]() |
6e371ac5ac | ||
![]() |
1b69b89d2d | ||
![]() |
5a20c8e512 | ||
![]() |
4ca1503beb | ||
![]() |
567a7eb7f3 | ||
![]() |
20f27fe32f | ||
![]() |
1a50d6bb86 | ||
![]() |
0b16c2db03 | ||
![]() |
76ac888386 | ||
![]() |
7cfa05d7f2 | ||
![]() |
e3496d0485 | ||
![]() |
d2c33c0074 | ||
![]() |
9c5caecafa | ||
![]() |
64651d5a84 | ||
![]() |
4493236879 | ||
![]() |
ce643942ea | ||
![]() |
46d216b02f | ||
![]() |
133d43ae30 | ||
![]() |
27155cb7e3 | ||
![]() |
b55c413774 | ||
![]() |
69be86e16c | ||
![]() |
65f6b0881e | ||
![]() |
c2bede40c7 | ||
![]() |
33c2398de9 | ||
![]() |
64cfc43891 | ||
![]() |
6575c69409 | ||
![]() |
e3f4e0b775 | ||
![]() |
5be89bfda5 | ||
![]() |
e1b573adeb | ||
![]() |
0913c7aa9e | ||
![]() |
5297626816 | ||
![]() |
c075642d78 | ||
![]() |
253f1a44c1 | ||
![]() |
26837c8871 | ||
![]() |
c732d31edd | ||
![]() |
c2595f28fb | ||
![]() |
58656a63cf | ||
![]() |
f4ce178cfa | ||
![]() |
2c1cfc64d5 | ||
![]() |
4b84d93cee | ||
![]() |
0bfa347595 | ||
![]() |
c7a07b59fd | ||
![]() |
c7c737d8c9 | ||
![]() |
1126e668d1 | ||
![]() |
b71a94ebca | ||
![]() |
815137af19 | ||
![]() |
3c4fbfec54 | ||
![]() |
b3a8eb7e06 | ||
![]() |
1e5c64ff73 | ||
![]() |
a320bfa425 | ||
![]() |
1c4dfc3c6e | ||
![]() |
92ee4d33c3 | ||
![]() |
01c2f01d3f | ||
![]() |
b83e6a5d5b | ||
![]() |
16281b38e0 | ||
![]() |
ca11e116b6 | ||
![]() |
1f32e4a642 | ||
![]() |
4c01089de0 | ||
![]() |
47a416dd9b | ||
![]() |
1ab1bbdd70 | ||
![]() |
1e05cb168c | ||
![]() |
d0383c1edf | ||
![]() |
c8ee35692c | ||
![]() |
3e062a8021 | ||
![]() |
0eb17e7102 | ||
![]() |
a14796cf90 | ||
![]() |
84b1c1ce6c | ||
![]() |
27e0b65c2d | ||
![]() |
ef35576174 | ||
![]() |
804faf726b | ||
![]() |
a94e5d2e47 | ||
![]() |
54d817d656 | ||
![]() |
00aa6c1f6a | ||
![]() |
ed34815393 | ||
![]() |
ceb15716c3 | ||
![]() |
bc97f540de | ||
![]() |
4ae67c79e0 | ||
![]() |
be857e989b | ||
![]() |
232402197c | ||
![]() |
12cbe84166 | ||
![]() |
b7b0be141c | ||
![]() |
8f9f4d9b71 | ||
![]() |
b97b9b7a28 | ||
![]() |
cf23732da3 | ||
![]() |
4e6b1d54e8 | ||
![]() |
fec8f90b78 | ||
![]() |
bedd4dafc4 | ||
![]() |
0d92648d62 | ||
![]() |
07989bc2fa | ||
![]() |
09ff2cb9a3 | ||
![]() |
6aa2e60037 | ||
![]() |
9f3ef532b8 | ||
![]() |
654621b438 | ||
![]() |
f8cfa9e02d | ||
![]() |
7476aa5897 | ||
![]() |
1d00a59706 | ||
![]() |
82d4e5e456 | ||
![]() |
07b6a36aab | ||
![]() |
0ceb6cc143 | ||
![]() |
864a155ee8 | ||
![]() |
15e2e575ee | ||
![]() |
253c524131 | ||
![]() |
48c02b8554 | ||
![]() |
d8fef22c9a | ||
![]() |
d82103e02d | ||
![]() |
9e5b22931e | ||
![]() |
30cd361edb | ||
![]() |
48da0071bd | ||
![]() |
32037e3645 | ||
![]() |
4279038a62 | ||
![]() |
c8afe7503b | ||
![]() |
4dfae98250 | ||
![]() |
b44dca49f5 | ||
![]() |
3d5892a841 | ||
![]() |
365e06891a | ||
![]() |
0f436481b8 | ||
![]() |
a5510be0bf | ||
![]() |
45a3c95a66 | ||
![]() |
94cb13ce9d | ||
![]() |
fecc431b10 | ||
![]() |
c88a975bf0 | ||
![]() |
4ed26ab934 | ||
![]() |
76a8da20bf | ||
![]() |
9f05747681 | ||
![]() |
f0bb9bb953 | ||
![]() |
72c6ac09ac | ||
![]() |
07c38d6353 | ||
![]() |
dd1cafbc77 | ||
![]() |
77e30d7844 | ||
![]() |
1c4f772a14 | ||
![]() |
bfc3148e4f | ||
![]() |
85db76c86e | ||
![]() |
f6c0d97172 | ||
![]() |
67f846513b | ||
![]() |
ad4cf843dd | ||
![]() |
af79b1504f | ||
![]() |
caff72967e | ||
![]() |
4abd21bbca | ||
![]() |
a2770a1bff | ||
![]() |
30e2d15321 | ||
![]() |
ba7e8b8627 | ||
![]() |
58850e97ff | ||
![]() |
b35acabc56 | ||
![]() |
ef907f1bc4 | ||
![]() |
c25128917b | ||
![]() |
9db6e64666 | ||
![]() |
f51a29a2e1 | ||
![]() |
b90025a2d0 | ||
![]() |
03b5ae02e1 | ||
![]() |
70627b992a | ||
![]() |
e830cd9baa | ||
![]() |
05fb10390e | ||
![]() |
d2c802c9da | ||
![]() |
741c165a5b | ||
![]() |
53d3ede184 | ||
![]() |
a41d2f831d | ||
![]() |
e30b7d0fa4 | ||
![]() |
7222741863 | ||
![]() |
49cea717af | ||
![]() |
49a7b62155 | ||
![]() |
3abc88985a | ||
![]() |
764b007d2c | ||
![]() |
a5c0d3dfae | ||
![]() |
386070923f | ||
![]() |
e79015d877 | ||
![]() |
b574396751 | ||
![]() |
91400070a7 | ||
![]() |
23d02085e8 | ||
![]() |
a9459dca89 | ||
![]() |
bd08cd1983 | ||
![]() |
3ae91495c1 | ||
![]() |
7f9fa46271 | ||
![]() |
f3b02d9922 | ||
![]() |
f2be582299 | ||
![]() |
b2eb403f9b | ||
![]() |
7e6a5927d7 | ||
![]() |
2c4a3650c4 | ||
![]() |
5b125d4513 | ||
![]() |
66167aeb55 | ||
![]() |
9804269cd1 | ||
![]() |
3239c478a5 | ||
![]() |
8f2ca5761b | ||
![]() |
ef9669cdb6 | ||
![]() |
caddcaf807 | ||
![]() |
90707d661b | ||
![]() |
facb7226fe | ||
![]() |
b671f54cb7 | ||
![]() |
55f26c9c4c | ||
![]() |
18995b3561 | ||
![]() |
8e3fbdddc7 | ||
![]() |
c19df84bef | ||
![]() |
e8527ba723 | ||
![]() |
577b49df9d | ||
![]() |
5b83bd03f5 | ||
![]() |
73cbf6c33d | ||
![]() |
95ab3b99f8 | ||
![]() |
24dbf669a9 | ||
![]() |
8716590cef | ||
![]() |
10729f0362 | ||
![]() |
800f54f263 | ||
![]() |
e63a543c29 | ||
![]() |
0f08796e1b | ||
![]() |
d10c32bad6 | ||
![]() |
813ad6551c | ||
![]() |
5582b33d40 | ||
![]() |
79afc236c9 | ||
![]() |
1f83a75d5f | ||
![]() |
2a3baf5aec | ||
![]() |
f92ae3b232 | ||
![]() |
03523244ef | ||
![]() |
55e799b833 | ||
![]() |
10929e9ac8 | ||
![]() |
89757609c2 | ||
![]() |
0f1dea67b7 | ||
![]() |
38e035b95c | ||
![]() |
f695d4b9da | ||
![]() |
4fbe7f16f3 | ||
![]() |
dcc2cc5001 | ||
![]() |
cb271deb8f | ||
![]() |
d480e91196 | ||
![]() |
9880f9ebc7 | ||
![]() |
999ae678c2 | ||
![]() |
5f0eba694c | ||
![]() |
f893ba929a | ||
![]() |
d35b0423b6 | ||
![]() |
651a22d056 | ||
![]() |
8dee246854 | ||
![]() |
90db397ec6 | ||
![]() |
cf973ff41e | ||
![]() |
fd8de5b1ea | ||
![]() |
27772257a8 | ||
![]() |
5665db844e | ||
![]() |
21a9963a2b | ||
![]() |
99f260225a | ||
![]() |
e4054d684c | ||
![]() |
01af725d79 | ||
![]() |
db0f31adea | ||
![]() |
226c771735 | ||
![]() |
b18b070622 | ||
![]() |
e52ae28426 | ||
![]() |
6acd30eda1 | ||
![]() |
f170cc0354 | ||
![]() |
d9abae51b5 | ||
![]() |
ada67bd54e | ||
![]() |
f7f5d0efa6 | ||
![]() |
a2bdd64ad0 | ||
![]() |
48f4f21d28 | ||
![]() |
57b8ee37ec | ||
![]() |
71bf8eb332 | ||
![]() |
fb1e288580 | ||
![]() |
720c6dd3b0 | ||
![]() |
be6506da08 | ||
![]() |
4f6698e39f | ||
![]() |
cdcd22e6a6 | ||
![]() |
b1b3eb4406 | ||
![]() |
b0c6f5e56b | ||
![]() |
322d8a61c2 | ||
![]() |
d41c4730cd | ||
![]() |
b38bb47491 | ||
![]() |
6973691cce | ||
![]() |
96176589ca | ||
![]() |
a74740877a | ||
![]() |
1de4072a48 | ||
![]() |
cf2796b2af | ||
![]() |
85315b768c | ||
![]() |
76a147e58b | ||
![]() |
de88c6de8c | ||
![]() |
613b429540 | ||
![]() |
d9abb745a9 | ||
![]() |
702225f535 | ||
![]() |
612e6341a3 | ||
![]() |
70b566f746 | ||
![]() |
6dd69126ae | ||
![]() |
f3d6756fba | ||
![]() |
8d60506884 | ||
![]() |
9712ac109d | ||
![]() |
379b4f8cd3 | ||
![]() |
86d223fd93 | ||
![]() |
205106b566 | ||
![]() |
54099d8441 | ||
![]() |
7c8b501c40 | ||
![]() |
02bf0349ca | ||
![]() |
9d72d1fc81 | ||
![]() |
085e6da1f2 | ||
![]() |
1ce0eae931 | ||
![]() |
ff5c25fc8c | ||
![]() |
ef863bec7c | ||
![]() |
89117da57d | ||
![]() |
6170f4f56a | ||
![]() |
0b00f8f4f0 | ||
![]() |
a45c128c38 | ||
![]() |
fe26655653 | ||
![]() |
2277fb5a58 | ||
![]() |
b6ab3095ab | ||
![]() |
591466d16d | ||
![]() |
666c12f2e8 | ||
![]() |
8180226eb7 | ||
![]() |
0d5be4a730 | ||
![]() |
49a2e2f3fa | ||
![]() |
219563146d | ||
![]() |
0d01295e79 | ||
![]() |
b65e195c27 | ||
![]() |
f5717cca1c | ||
![]() |
a2c9f6792e | ||
![]() |
95c12c1840 | ||
![]() |
4de1cb0a09 | ||
![]() |
ec9ebd3026 | ||
![]() |
5ee9ad3e4f | ||
![]() |
9e2135e2c7 | ||
![]() |
116da276c7 | ||
![]() |
7c9ab8c0b6 | ||
![]() |
3a36d9b1ae | ||
![]() |
5fce21e269 | ||
![]() |
a46bfc8b5f | ||
![]() |
a860c29304 | ||
![]() |
cc90b45022 | ||
![]() |
78ae4c42f7 | ||
![]() |
d1292c59ea | ||
![]() |
1a87c730bc | ||
![]() |
7aa72f768f | ||
![]() |
81b9f2d4e0 | ||
![]() |
a03a745295 | ||
![]() |
ce8bf90663 | ||
![]() |
e2ae919a84 | ||
![]() |
9a0e44a731 | ||
![]() |
650c816a7b | ||
![]() |
c8bfbb9315 | ||
![]() |
80b32dd392 | ||
![]() |
e3352ea426 | ||
![]() |
faed7683be | ||
![]() |
46a39190a4 | ||
![]() |
a2f738772c | ||
![]() |
a1697ff21c | ||
![]() |
b9fdf68be3 | ||
![]() |
ec971d0473 | ||
![]() |
d462464af9 | ||
![]() |
423e0768f9 | ||
![]() |
0ef3a141a8 | ||
![]() |
d532913d56 | ||
![]() |
3205bb3bdf | ||
![]() |
b238ba054d | ||
![]() |
a8e13df249 | ||
![]() |
9dc77d94ed | ||
![]() |
714995877a | ||
![]() |
729d7a11cd | ||
![]() |
95cd86a541 | ||
![]() |
ca1b8344fa | ||
![]() |
e797295b82 | ||
![]() |
f216b322c2 | ||
![]() |
b97c19ca3c | ||
![]() |
d54c7ca27c | ||
![]() |
077de8dcaa | ||
![]() |
6f50e5671a | ||
![]() |
bab9bdc832 | ||
![]() |
8e0adbb0fb | ||
![]() |
6b175ae7e3 | ||
![]() |
9494633da3 | ||
![]() |
c754a5f391 | ||
![]() |
2b692c6fc8 | ||
![]() |
35b6fb1a6d | ||
![]() |
61566a34d1 | ||
![]() |
e14f4c94c2 | ||
![]() |
52bdb1a80c | ||
![]() |
88ee3bdb6d | ||
![]() |
8f8a99a645 | ||
![]() |
8d35f22daf | ||
![]() |
6f8ef9bbdc | ||
![]() |
e7ddf6ba8f | ||
![]() |
407a119b9a | ||
![]() |
96aa2451bf | ||
![]() |
7680aab04d | ||
![]() |
3aef26b229 | ||
![]() |
935141adc0 | ||
![]() |
4300733d0c | ||
![]() |
c284a091c0 | ||
![]() |
7e768bfe23 | ||
![]() |
938f388a0b | ||
![]() |
d22a6fb8ea | ||
![]() |
11142e2f05 | ||
![]() |
fe7fb488c0 | ||
![]() |
0929c5086a | ||
![]() |
eb3ed7719b | ||
![]() |
6c19a0f8c7 | ||
![]() |
a0ece589b0 | ||
![]() |
8165071edf | ||
![]() |
a667974378 | ||
![]() |
fe1f88ce5d | ||
![]() |
8f2715e437 | ||
![]() |
57a3223c77 | ||
![]() |
df82ac8ac4 | ||
![]() |
16adddc803 | ||
![]() |
d1ae82c5c2 | ||
![]() |
0098936347 | ||
![]() |
17b85f6400 | ||
![]() |
48be9c0fd1 | ||
![]() |
ce36c2d0ea | ||
![]() |
06c63ef4a4 | ||
![]() |
03d93a7d6e | ||
![]() |
a0005c8b3e | ||
![]() |
2220343c50 | ||
![]() |
68fbed996f | ||
![]() |
20a4d8949d | ||
![]() |
1029ecfd49 | ||
![]() |
4de2a3b09e | ||
![]() |
651407c88e | ||
![]() |
3e2e485f66 | ||
![]() |
10efaad224 | ||
![]() |
3dda02660c | ||
![]() |
0b4e8141b0 | ||
![]() |
16ab0efa59 | ||
![]() |
20ca1ec547 | ||
![]() |
a1c9ab237f | ||
![]() |
a65239f7f1 | ||
![]() |
a0622675fd | ||
![]() |
1e0b778097 | ||
![]() |
3b666fef77 | ||
![]() |
9291c98189 | ||
![]() |
f6dadd8c82 | ||
![]() |
022bb272e6 | ||
![]() |
edad1a41e8 | ||
![]() |
421e78a748 | ||
![]() |
b961df90a7 | ||
![]() |
0b26b7098a | ||
![]() |
b09566a9a9 | ||
![]() |
b869deab66 | ||
![]() |
3d552c3112 | ||
![]() |
2a2bf3bf55 | ||
![]() |
26863b8cdc | ||
![]() |
b0f7d07214 | ||
![]() |
30c6557a32 | ||
![]() |
e18e173089 | ||
![]() |
6a8bdbd4f6 | ||
![]() |
b5dec87a62 | ||
![]() |
e50d30876a | ||
![]() |
cbcd9ed67d | ||
![]() |
6bcc26b487 | ||
![]() |
56fcb3fee1 | ||
![]() |
557e1790dd | ||
![]() |
2e67697d36 | ||
![]() |
ca4500692f | ||
![]() |
5aba6bff09 | ||
![]() |
9ed74068b9 | ||
![]() |
be4685742c | ||
![]() |
86b0a38811 | ||
![]() |
3e528f0a9a | ||
![]() |
170d7b6922 | ||
![]() |
f05249a9ad | ||
![]() |
5957ed7af3 | ||
![]() |
8ed63893eb | ||
![]() |
ea9dd926bc | ||
![]() |
f31d3b531f | ||
![]() |
d851448c32 | ||
![]() |
65327d52a6 | ||
![]() |
2ea5ae59b2 | ||
![]() |
a04d09028b | ||
![]() |
dd9255cb81 | ||
![]() |
536d7ecd3e | ||
![]() |
f50aac08df | ||
![]() |
db0f1d2159 | ||
![]() |
d97b565d6c | ||
![]() |
d3071f13d8 | ||
![]() |
768407c1d7 | ||
![]() |
c5d18b03cd | ||
![]() |
5f05b44cde | ||
![]() |
beaa09e9b3 | ||
![]() |
d6960f537b | ||
![]() |
0918eab004 | ||
![]() |
9b16789a17 | ||
![]() |
157240351f | ||
![]() |
6ad3d45d60 | ||
![]() |
b715e4d426 | ||
![]() |
aad5e9e99f | ||
![]() |
2a104ad33f | ||
![]() |
851290ee89 | ||
![]() |
a8c6c55e3b | ||
![]() |
992a647424 | ||
![]() |
c22461a1b6 | ||
![]() |
23fefc3ab7 | ||
![]() |
0beb9f0b5f | ||
![]() |
d376f9e7a3 | ||
![]() |
07e7bcd30b | ||
![]() |
95e86cb649 | ||
![]() |
802e5591ce | ||
![]() |
26d5730ad2 | ||
![]() |
8c7554e081 | ||
![]() |
9f5d47c320 | ||
![]() |
4aa452ce63 | ||
![]() |
7ef81ae10f | ||
![]() |
3628292afa | ||
![]() |
8aa5ecde62 | ||
![]() |
2f149eac9d | ||
![]() |
fcfc705b87 | ||
![]() |
7bd5c010a1 | ||
![]() |
52168d8e61 | ||
![]() |
a3842d9228 | ||
![]() |
452c51bd16 | ||
![]() |
22bedd9957 | ||
![]() |
cb318c723d | ||
![]() |
9a81d3c28e | ||
![]() |
3ca59e3b7a | ||
![]() |
07a12bdf15 | ||
![]() |
62e81d8bf0 | ||
![]() |
e295a41caa | ||
![]() |
c545a80aa3 | ||
![]() |
56f1a0cb51 | ||
![]() |
7218b6da97 | ||
![]() |
83f9f2d387 | ||
![]() |
13a2e38385 | ||
![]() |
996d942387 | ||
![]() |
cc42eb9fab | ||
![]() |
44125be979 | ||
![]() |
c2e9cc9a51 | ||
![]() |
fcd10f2adc | ||
![]() |
93009c1eed | ||
![]() |
d875be60d4 | ||
![]() |
db48d4c576 | ||
![]() |
3293231ad2 | ||
![]() |
aa1f2d3b59 | ||
![]() |
f492b679e3 | ||
![]() |
7ca84322bd | ||
![]() |
e3257b8fa3 | ||
![]() |
0bcda5ded8 | ||
![]() |
7ec82c0891 | ||
![]() |
3241ac7dc2 | ||
![]() |
e974605fc8 | ||
![]() |
ce13380533 | ||
![]() |
e23e3acda3 | ||
![]() |
63ab9972da | ||
![]() |
feb4901620 | ||
![]() |
67788a1b1b | ||
![]() |
001faf9ed7 | ||
![]() |
7a464d8a6e | ||
![]() |
5acd1c7c1b | ||
![]() |
de14540374 | ||
![]() |
37e0c2667b | ||
![]() |
252abb41c3 | ||
![]() |
fb2af341d8 | ||
![]() |
931f5f9c27 | ||
![]() |
b5d04e575e | ||
![]() |
3d395601fe | ||
![]() |
6630ce646c | ||
![]() |
f5508eea1c | ||
![]() |
c8c460432f | ||
![]() |
18299dafd2 | ||
![]() |
817d09026e | ||
![]() |
d76b009390 | ||
![]() |
9effed3ce1 | ||
![]() |
c1bbfc5dcf | ||
![]() |
b6c9cfb76f | ||
![]() |
59ca7bbcf2 | ||
![]() |
52c8d5e999 | ||
![]() |
5851e7f1b7 | ||
![]() |
e05b3441de | ||
![]() |
0d6e79cb93 | ||
![]() |
76a102d901 | ||
![]() |
bbd4659fbf | ||
![]() |
0880420ef6 | ||
![]() |
2351c79282 | ||
![]() |
d6016fc798 | ||
![]() |
bc17291006 | ||
![]() |
ed6cb14c4d | ||
![]() |
08de8a04b8 | ||
![]() |
5c67de8b47 | ||
![]() |
38b0408b1a | ||
![]() |
9ccad7ea86 | ||
![]() |
4a4e810a14 | ||
![]() |
76d2df3bde | ||
![]() |
c02563d894 | ||
![]() |
574ec6780b | ||
![]() |
9e0f56982b | ||
![]() |
1c66daf12b | ||
![]() |
59d683849e | ||
![]() |
9946acb1a0 | ||
![]() |
83a760644d | ||
![]() |
25ccff8640 | ||
![]() |
5c4c5a7794 | ||
![]() |
cb6af97595 | ||
![]() |
c4407dccf6 | ||
![]() |
ecdea4c3c8 | ||
![]() |
26d6f302cf | ||
![]() |
ecf10622ef | ||
![]() |
0fb553675b | ||
![]() |
2080fde4f9 | ||
![]() |
d10e67ce09 | ||
![]() |
74fe7c586b | ||
![]() |
05188aed6d | ||
![]() |
865efb7752 | ||
![]() |
4782b4da07 | ||
![]() |
4693632c7d | ||
![]() |
4c4b571a88 | ||
![]() |
328c87995b | ||
![]() |
a1d10e7d4a | ||
![]() |
77d9a7e9d3 | ||
![]() |
981b090088 | ||
![]() |
6d1c788ee0 | ||
![]() |
02de773d5b | ||
![]() |
2a240d83fd | ||
![]() |
25cdf7916d | ||
![]() |
11b5983a0d | ||
![]() |
4964987245 | ||
![]() |
ed129d6074 | ||
![]() |
37e928d869 | ||
![]() |
06def8c11e | ||
![]() |
10571676a4 | ||
![]() |
af5160237d | ||
![]() |
b86842ba73 | ||
![]() |
bfc271e743 | ||
![]() |
ee88140fdd | ||
![]() |
51249a1dce | ||
![]() |
57ec9e6b13 | ||
![]() |
1324d17d87 | ||
![]() |
26b438a888 | ||
![]() |
70f3f98363 | ||
![]() |
71e4be2d5e | ||
![]() |
5740806a28 | ||
![]() |
9b50a1b7a6 | ||
![]() |
19caad832e | ||
![]() |
dd6ae13281 | ||
![]() |
077abbe961 | ||
![]() |
3d85dc1127 | ||
![]() |
e3ea5dd13c | ||
![]() |
714b2ecd9c | ||
![]() |
883937bfd7 | ||
![]() |
0ebe08d796 | ||
![]() |
36b4fff5c7 | ||
![]() |
0684c8c388 | ||
![]() |
67744c877d | ||
![]() |
45d8c945e2 | ||
![]() |
ee19307ea2 | ||
![]() |
2c1cd25be4 | ||
![]() |
6e65558ea4 | ||
![]() |
304324ebd0 | ||
![]() |
97cd06d2ba | ||
![]() |
df948065a3 | ||
![]() |
f92126b44f | ||
![]() |
e329f6cdf1 | ||
![]() |
2c96438d61 | ||
![]() |
41a9aac75d | ||
![]() |
8768168536 | ||
![]() |
325809fbbf | ||
![]() |
3dd47a9f5b | ||
![]() |
00f16ef8f0 | ||
![]() |
5e67aae83b | ||
![]() |
ae5c603c98 | ||
![]() |
c62aa3cb55 | ||
![]() |
7073cb6d5c | ||
![]() |
a495ad58d0 | ||
![]() |
569165371c | ||
![]() |
ea14fa5251 | ||
![]() |
4a02865697 | ||
![]() |
3a2a20cefd | ||
![]() |
f2f42de701 | ||
![]() |
6d60d4897c | ||
![]() |
9f71ce8083 | ||
![]() |
50f8f7da93 | ||
![]() |
d475344b51 | ||
![]() |
2630863409 | ||
![]() |
e120f4a3f7 | ||
![]() |
6aff4c986c | ||
![]() |
2e891b1634 | ||
![]() |
35a0c5d36f | ||
![]() |
82d786b94c | ||
![]() |
de49d602a1 | ||
![]() |
3cb6511b66 | ||
![]() |
1d3ae777d5 | ||
![]() |
dbb2ea39d2 | ||
![]() |
fb607332b9 | ||
![]() |
b956f627b0 | ||
![]() |
2ac64ab573 | ||
![]() |
482e00970c | ||
![]() |
d62340efb5 | ||
![]() |
eb0f35219c | ||
![]() |
243598ae50 | ||
![]() |
74c965d21d | ||
![]() |
30316179a0 | ||
![]() |
f16a1101e6 | ||
![]() |
0466f7a18a | ||
![]() |
97cf3b2079 | ||
![]() |
6542d75a6a | ||
![]() |
c6900c5d51 | ||
![]() |
0e9642ea3e | ||
![]() |
3ab2892066 | ||
![]() |
69b69aca6a | ||
![]() |
a05dbd2e5a | ||
![]() |
c1641f6fb8 | ||
![]() |
452c79f9a1 | ||
![]() |
37959fe31c | ||
![]() |
30f73f39a0 | ||
![]() |
fa613cd5fb | ||
![]() |
b8afb22902 | ||
![]() |
07e07fc7e8 | ||
![]() |
58f95c1891 | ||
![]() |
7ad8e3b3da | ||
![]() |
b9a548758a | ||
![]() |
a436caf2ad | ||
![]() |
5461f8a225 | ||
![]() |
78b747571c | ||
![]() |
0c6a9a189b | ||
![]() |
45d666ff2d | ||
![]() |
4143925322 | ||
![]() |
9be3d2ccaf | ||
![]() |
8be8a310d7 | ||
![]() |
b81c339922 | ||
![]() |
d3d103f141 | ||
![]() |
dd575ccb88 | ||
![]() |
a83c7c64b5 | ||
![]() |
c04ded6fd8 | ||
![]() |
f24c779737 | ||
![]() |
3cdd358fc8 | ||
![]() |
e11939b149 | ||
![]() |
a4ef0702c9 | ||
![]() |
548b6352ca | ||
![]() |
2658c16073 | ||
![]() |
49d0b6aa00 | ||
![]() |
e65f584197 | ||
![]() |
ce1bbda188 | ||
![]() |
846897fb4c | ||
![]() |
457e134261 | ||
![]() |
3e129763c7 | ||
![]() |
c49d086965 | ||
![]() |
df7bfc4efd | ||
![]() |
7fba1f9ed2 | ||
![]() |
3205d52331 | ||
![]() |
111960c530 | ||
![]() |
e1bc1a0129 | ||
![]() |
8b543a5fa9 | ||
![]() |
dc7a67a1d7 | ||
![]() |
350c20d6ab | ||
![]() |
b5f0cd7c70 | ||
![]() |
90488cd77a | ||
![]() |
5bbc59e87c | ||
![]() |
c02758213b | ||
![]() |
09c62d67c1 | ||
![]() |
3f3fa3044c | ||
![]() |
62673145fb | ||
![]() |
0baf73de5e | ||
![]() |
66a0783e7b | ||
![]() |
17144c45e5 | ||
![]() |
311c0ba4f1 | ||
![]() |
e293d23ae3 | ||
![]() |
93769d2608 | ||
![]() |
e7540563d0 | ||
![]() |
fc1047550e | ||
![]() |
dadc618719 | ||
![]() |
36f3bd2869 | ||
![]() |
fdcea983a4 | ||
![]() |
081534457c | ||
![]() |
94a6272a1d | ||
![]() |
d389e0ecf8 | ||
![]() |
17eb1c604f | ||
![]() |
99474aab06 | ||
![]() |
f3d3bf20de | ||
![]() |
3c999e9847 | ||
![]() |
692fa5f606 | ||
![]() |
752b8e79ff | ||
![]() |
3f82cf4ab3 | ||
![]() |
1549b9df74 | ||
![]() |
78ef87a952 | ||
![]() |
29ede48e0f | ||
![]() |
6349d25219 | ||
![]() |
830a450f00 | ||
![]() |
18f9ce9c0b | ||
![]() |
2471be0c78 | ||
![]() |
60cfd687dc | ||
![]() |
e06c61b95d | ||
![]() |
471eee0872 | ||
![]() |
20abd8a9f8 | ||
![]() |
88e5c471de | ||
![]() |
09086e574d | ||
![]() |
8d95c13e31 | ||
![]() |
c922cc4351 | ||
![]() |
a42f28c502 | ||
![]() |
b802f3a71f | ||
![]() |
f78f212a77 | ||
![]() |
22cbfd473b | ||
![]() |
e5973ef713 | ||
![]() |
5364a29b5f | ||
![]() |
49754d33fa | ||
![]() |
d7d95037be | ||
![]() |
515146d4a2 | ||
![]() |
b7540fab58 | ||
![]() |
88e6f8abf6 | ||
![]() |
3c4dadd905 | ||
![]() |
f18f997796 | ||
![]() |
3a1daf46ae | ||
![]() |
8dffea4a42 | ||
![]() |
3852a6c5cf | ||
![]() |
6493f51a29 | ||
![]() |
028f42e775 | ||
![]() |
eb1cc55f94 | ||
![]() |
fb864f1132 | ||
![]() |
8b8d988c07 | ||
![]() |
c2b5451fe4 | ||
![]() |
487d3a6262 | ||
![]() |
fe990b4cd2 | ||
![]() |
019c7e2f78 | ||
![]() |
1c64a4f145 | ||
![]() |
fc869aa203 | ||
![]() |
3a0ada9f46 | ||
![]() |
cc9980fc19 | ||
![]() |
7515d8af64 | ||
![]() |
5e7579c1fd | ||
![]() |
38af53f281 | ||
![]() |
a26bec5b00 | ||
![]() |
feb943b6df | ||
![]() |
059e37a41f | ||
![]() |
7ce67fd465 | ||
![]() |
6b8b8209f3 | ||
![]() |
fd1d12859d | ||
![]() |
efb00b2387 | ||
![]() |
9b2ca57038 | ||
![]() |
9694face16 | ||
![]() |
7ef14832d0 | ||
![]() |
33f7b58e6e | ||
![]() |
9e992da863 | ||
![]() |
c986a218c7 | ||
![]() |
8ee6312402 | ||
![]() |
3c86b12ef9 | ||
![]() |
02d09edd49 | ||
![]() |
55af3c3dd1 | ||
![]() |
f8f5a77744 | ||
![]() |
5b6956ff24 | ||
![]() |
f1c138eaed | ||
![]() |
caf43638de | ||
![]() |
b783d2e210 | ||
![]() |
9a40a5f019 | ||
![]() |
81a7b34101 | ||
![]() |
f124e2a889 | ||
![]() |
02b2bcafc5 | ||
![]() |
81a5fd377e | ||
![]() |
f875ae4abf | ||
![]() |
01fd400ec7 | ||
![]() |
c59420581c | ||
![]() |
0aa9462cea | ||
![]() |
bf2f6f84e5 | ||
![]() |
5126f01b57 | ||
![]() |
ec4814a76e | ||
![]() |
69b53d70c5 | ||
![]() |
02875f5a34 | ||
![]() |
29d8c4e08d | ||
![]() |
df203311fe | ||
![]() |
10f9b91c44 | ||
![]() |
cd861364a2 | ||
![]() |
90b52abc04 | ||
![]() |
093b726c52 | ||
![]() |
fd84fc9dbe | ||
![]() |
4353646b3a | ||
![]() |
7545e5312c | ||
![]() |
bd494ce9ec | ||
![]() |
ee3cf8e6d1 | ||
![]() |
df524fdc1f | ||
![]() |
f0c0cfee1d | ||
![]() |
cf1bf3c163 | ||
![]() |
b9d703fe25 | ||
![]() |
46f7e685b6 | ||
![]() |
8023331fca | ||
![]() |
597db7d4bd | ||
![]() |
773bd32cd0 | ||
![]() |
c7e3756de1 | ||
![]() |
64bf122c95 | ||
![]() |
a92b0411fd | ||
![]() |
728d61762a | ||
![]() |
d9783e2a4d | ||
![]() |
b6303d2c16 | ||
![]() |
dd673a62b5 | ||
![]() |
b7577038a0 | ||
![]() |
613b71d23b | ||
![]() |
0284100c2d | ||
![]() |
26cd470d31 | ||
![]() |
ebaf509a42 | ||
![]() |
646db73061 | ||
![]() |
e6df581909 | ||
![]() |
a8e12409b5 | ||
![]() |
b7c7e293f7 | ||
![]() |
fe85aff052 | ||
![]() |
12d8bcad6e | ||
![]() |
bbfc244f16 | ||
![]() |
e275a2736a | ||
![]() |
d2a8076596 | ||
![]() |
83344f748f | ||
![]() |
1d5dbc454d | ||
![]() |
16e2dc60aa | ||
![]() |
a6fd4a8472 | ||
![]() |
c25698dfa7 | ||
![]() |
2ab2064a72 | ||
![]() |
356c26ce84 | ||
![]() |
bc56dfbcb5 | ||
![]() |
daaeb36363 | ||
![]() |
a46a9cf0bf | ||
![]() |
cbf435169a | ||
![]() |
7d05f6c54a | ||
![]() |
8a8667d1f4 | ||
![]() |
b9b8b764db | ||
![]() |
4978af351d | ||
![]() |
b5d639652d | ||
![]() |
c4ebfaf7f6 | ||
![]() |
256266280d | ||
![]() |
f886b58529 | ||
![]() |
14fe93b9ab | ||
![]() |
a2fb0ceb7d | ||
![]() |
bc284ecf6d | ||
![]() |
6113f586c9 | ||
![]() |
6c0862248c | ||
![]() |
0e8f2a7c6c | ||
![]() |
a808f8bbd5 | ||
![]() |
9428d5638e | ||
![]() |
e00cd5e304 | ||
![]() |
3c04bf2742 | ||
![]() |
2ef6d450bc | ||
![]() |
628b0bffeb | ||
![]() |
27eaa566a5 | ||
![]() |
fb36646bd3 | ||
![]() |
304cc37618 | ||
![]() |
8239e8a581 | ||
![]() |
69e117d898 | ||
![]() |
c773ec8a30 | ||
![]() |
b4b49ee096 | ||
![]() |
cf5ab87db9 | ||
![]() |
deaff293d2 | ||
![]() |
dccdebd2c0 | ||
![]() |
2674d4f034 | ||
![]() |
cb529561e1 | ||
![]() |
1a1cf49c67 | ||
![]() |
757b61a010 | ||
![]() |
448dcbab46 | ||
![]() |
30fc5bbb09 | ||
![]() |
d3e14818df | ||
![]() |
864e242ed9 | ||
![]() |
8f18baea8f | ||
![]() |
130489a1a9 | ||
![]() |
88a5a2049b | ||
![]() |
15fb3e5328 | ||
![]() |
90b800b030 | ||
![]() |
be88ad2676 | ||
![]() |
dfadfc0f13 | ||
![]() |
5ae48c8012 | ||
![]() |
6f163111ce | ||
![]() |
3bcbd05252 | ||
![]() |
e0d2697618 | ||
![]() |
7340535b9a | ||
![]() |
c385355c2b | ||
![]() |
a119790697 | ||
![]() |
e392098e35 | ||
![]() |
a2d4d16867 | ||
![]() |
7f74a85400 | ||
![]() |
1fc9eaf360 | ||
![]() |
1898f9b183 | ||
![]() |
1fb03a755f | ||
![]() |
8a505e3b66 | ||
![]() |
45ecec5623 | ||
![]() |
b34dfcd72f | ||
![]() |
319aa39925 | ||
![]() |
08ac40dd48 | ||
![]() |
0a0dc25e15 | ||
![]() |
0557a15fa8 | ||
![]() |
c5fafdda11 | ||
![]() |
434d1fe225 | ||
![]() |
405769dc97 | ||
![]() |
ffa116bf44 | ||
![]() |
6b1d8cabf4 | ||
![]() |
20c21e9e65 | ||
![]() |
088743a155 | ||
![]() |
d1fba28936 | ||
![]() |
65064a6934 | ||
![]() |
c7f5b7ae82 | ||
![]() |
2b244165e2 | ||
![]() |
47682bc143 | ||
![]() |
0dcfb97824 | ||
![]() |
50af671e02 | ||
![]() |
1ef273c35d | ||
![]() |
91b9831548 | ||
![]() |
3241968626 | ||
![]() |
5dee65afcb | ||
![]() |
4108eabd0d | ||
![]() |
829a693128 | ||
![]() |
bf1e49fc4c | ||
![]() |
d1984c0dda | ||
![]() |
22bb28db62 | ||
![]() |
721b52a45b | ||
![]() |
edf4f98d41 | ||
![]() |
7321ea1603 | ||
![]() |
b80c2126a3 | ||
![]() |
70afed9122 | ||
![]() |
87479c32de | ||
![]() |
35f75563a7 | ||
![]() |
f5d6a9f428 | ||
![]() |
2d314efb98 | ||
![]() |
930bac3c8b | ||
![]() |
d457f66e8b | ||
![]() |
98ef1ba579 | ||
![]() |
dd8514a84d | ||
![]() |
7bcfeab85c | ||
![]() |
d1cd03302c | ||
![]() |
36669652bf | ||
![]() |
48234896a4 | ||
![]() |
d4c310433e | ||
![]() |
a9d82a64a8 | ||
![]() |
74b6b8cb62 | ||
![]() |
52e8a1aba3 | ||
![]() |
0f92523d28 | ||
![]() |
6934fc6510 | ||
![]() |
ad746be010 | ||
![]() |
97119f729a | ||
![]() |
e576f1b0c4 | ||
![]() |
1771293fcf | ||
![]() |
d872423a76 | ||
![]() |
6c12f65b2d | ||
![]() |
566f50ec66 | ||
![]() |
e702f9c317 | ||
![]() |
680f8086e7 | ||
![]() |
9216f000ad | ||
![]() |
cea6ef7a66 | ||
![]() |
3287daf4e4 | ||
![]() |
376b40b25f | ||
![]() |
b00ca90d15 | ||
![]() |
06ec83701c | ||
![]() |
2b61ec32d0 | ||
![]() |
08299dd85d | ||
![]() |
a2e7199ff5 | ||
![]() |
28d68c8a77 | ||
![]() |
999abb7016 | ||
![]() |
5af2658d88 | ||
![]() |
46fd55d100 | ||
![]() |
fd4d747927 | ||
![]() |
ec3ea965d0 | ||
![]() |
41e4438a05 | ||
![]() |
254e0ea132 | ||
![]() |
190e24b25d | ||
![]() |
3f22aa9638 | ||
![]() |
0e679841a4 | ||
![]() |
e512e658e6 | ||
![]() |
f7cea2f92e | ||
![]() |
de5689f5b2 | ||
![]() |
e6cf5a5984 | ||
![]() |
23e7ccb543 | ||
![]() |
b4d97d4a2b | ||
![]() |
8f90fe79c8 | ||
![]() |
eb0df5d5e9 | ||
![]() |
e75510309d | ||
![]() |
3425d01853 | ||
![]() |
606737f3b2 | ||
![]() |
bf3b5fbf8e | ||
![]() |
f2fb06e6f3 | ||
![]() |
e58ba44e3d | ||
![]() |
1e19ec6b9a | ||
![]() |
6ed637cfdd | ||
![]() |
24ae8249f9 |
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"qpdf": {
|
||||
"version": "11.3.0"
|
||||
},
|
||||
"jbig2enc": {
|
||||
"version": "0.29",
|
||||
"git_tag": "0.29"
|
||||
}
|
||||
}
|
15
.codecov.yml
@@ -1,3 +1,15 @@
|
||||
codecov:
|
||||
require_ci_to_pass: true
|
||||
# https://docs.codecov.com/docs/flags#recommended-automatic-flag-management
|
||||
# Require each flag to have 1 upload before notification
|
||||
flag_management:
|
||||
individual_flags:
|
||||
- name: backend
|
||||
paths:
|
||||
- src/
|
||||
- name: frontend
|
||||
paths:
|
||||
- src-ui/
|
||||
# https://docs.codecov.com/docs/pull-request-comments
|
||||
# codecov will only comment if coverage changes
|
||||
comment:
|
||||
@@ -8,12 +20,9 @@ coverage:
|
||||
default:
|
||||
# https://docs.codecov.com/docs/commit-status#threshold
|
||||
threshold: 1%
|
||||
# https://docs.codecov.com/docs/commit-status#only_pulls
|
||||
only_pulls: true
|
||||
patch:
|
||||
default:
|
||||
# For the changed lines only, target 75% covered, but
|
||||
# allow as low as 50%
|
||||
target: 75%
|
||||
threshold: 25%
|
||||
only_pulls: true
|
||||
|
3
.codespellrc
Normal file
@@ -0,0 +1,3 @@
|
||||
[codespell]
|
||||
write-changes = True
|
||||
ignore-words-list = criterias,afterall,valeu,ureue,equest,ure,assertIn
|
180
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,180 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim as main-app
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Buildx provided, must be defined to use though
|
||||
ARG TARGETARCH
|
||||
|
||||
# Can be workflow provided, defaults set for manual building
|
||||
ARG JBIG2ENC_VERSION=0.29
|
||||
ARG QPDF_VERSION=11.9.0
|
||||
ARG GS_VERSION=10.03.1
|
||||
|
||||
# Set Python environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# Ignore warning from Whitenoise
|
||||
PYTHONWARNINGS="ignore:::django.http.response:517" \
|
||||
PNGX_CONTAINERIZED=1
|
||||
|
||||
#
|
||||
# Begin installation and configuration
|
||||
# Order the steps below from least often changed to most
|
||||
#
|
||||
|
||||
# Packages need for running
|
||||
ARG RUNTIME_PACKAGES="\
|
||||
# General utils
|
||||
curl \
|
||||
# Docker specific
|
||||
gosu \
|
||||
# Timezones support
|
||||
tzdata \
|
||||
# fonts for text file thumbnail generation
|
||||
fonts-liberation \
|
||||
gettext \
|
||||
ghostscript \
|
||||
gnupg \
|
||||
icc-profiles-free \
|
||||
imagemagick \
|
||||
# PostgreSQL
|
||||
postgresql-client \
|
||||
# MySQL / MariaDB
|
||||
mariadb-client \
|
||||
# OCRmyPDF dependencies
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
tesseract-ocr-deu \
|
||||
tesseract-ocr-fra \
|
||||
tesseract-ocr-ita \
|
||||
tesseract-ocr-spa \
|
||||
unpaper \
|
||||
pngquant \
|
||||
jbig2dec \
|
||||
# lxml
|
||||
libxml2 \
|
||||
libxslt1.1 \
|
||||
# itself
|
||||
qpdf \
|
||||
# Mime type detection
|
||||
file \
|
||||
libmagic1 \
|
||||
media-types \
|
||||
zlib1g \
|
||||
# Barcode splitter
|
||||
libzbar0 \
|
||||
poppler-utils \
|
||||
htop \
|
||||
sudo"
|
||||
|
||||
# Install basic runtime packages.
|
||||
# These change very infrequently
|
||||
RUN set -eux \
|
||||
echo "Installing system packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES}
|
||||
|
||||
ARG PYTHON_PACKAGES="\
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-wheel \
|
||||
pipenv \
|
||||
ca-certificates"
|
||||
|
||||
RUN set -eux \
|
||||
echo "Installing python packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet ${PYTHON_PACKAGES}
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pre-built updates" \
|
||||
&& echo "Installing qpdf ${QPDF_VERSION}" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing Ghostscript ${GS_VERSION}" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
&& dpkg --install ./libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
&& dpkg --install ./libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing jbig2enc" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb
|
||||
|
||||
# setup docker-specific things
|
||||
# These change sometimes, but rarely
|
||||
WORKDIR /usr/src/paperless/src/docker/
|
||||
|
||||
COPY [ \
|
||||
"docker/imagemagick-policy.xml", \
|
||||
"./" \
|
||||
]
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Configuring ImageMagick" \
|
||||
&& mv imagemagick-policy.xml /etc/ImageMagick-6/policy.xml
|
||||
|
||||
# Packages needed only for building a few quick Python
|
||||
# dependencies
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
git \
|
||||
# https://www.psycopg.org/docs/install.html#prerequisites
|
||||
libpq-dev \
|
||||
# https://github.com/PyMySQL/mysqlclient#linux
|
||||
default-libmysqlclient-dev \
|
||||
pkg-config \
|
||||
pre-commit"
|
||||
|
||||
# hadolint ignore=DL3042
|
||||
RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
||||
set -eux \
|
||||
&& echo "Installing build system packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet ${BUILD_PACKAGES}
|
||||
|
||||
RUN set -eux \
|
||||
&& npm update npm -g
|
||||
|
||||
# add users, setup scripts
|
||||
# Mount the compiled frontend to expected location
|
||||
RUN set -eux \
|
||||
&& echo "Setting up user/group" \
|
||||
&& groupmod --new-name paperless node \
|
||||
&& usermod --login paperless --home /usr/src/paperless node \
|
||||
&& usermod -s /bin/bash paperless \
|
||||
&& echo "paperless ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \
|
||||
&& echo "Creating volume directories" \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/data \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/media \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/consume \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/export \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/.venv \
|
||||
&& echo "Adjusting all permissions" \
|
||||
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless
|
||||
# && echo "Collecting static files" \
|
||||
# && gosu paperless python3 manage.py collectstatic --clear --no-input --link \
|
||||
# && gosu paperless python3 manage.py compilemessages
|
||||
|
||||
VOLUME ["/usr/src/paperless/paperless-ngx/data", \
|
||||
"/usr/src/paperless/paperless-ngx/media", \
|
||||
"/usr/src/paperless/paperless-ngx/consume", \
|
||||
"/usr/src/paperless/paperless-ngx/export", \
|
||||
"/usr/src/paperless/paperless-ngx/.venv"]
|
117
.devcontainer/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Paperless NGX Development Environment
|
||||
|
||||
## Overview
|
||||
|
||||
Welcome to the Paperless NGX development environment! This setup uses VSCode DevContainers to provide a consistent and seamless development experience.
|
||||
|
||||
### What are DevContainers?
|
||||
|
||||
DevContainers are a feature in VSCode that allows you to develop within a Docker container. This ensures that your development environment is consistent across different machines and setups. By defining a containerized environment, you can eliminate the "works on my machine" problem.
|
||||
|
||||
### Advantages of DevContainers
|
||||
|
||||
- **Consistency**: Same environment for all developers.
|
||||
- **Isolation**: Separate development environment from your local machine.
|
||||
- **Reproducibility**: Easily recreate the environment on any machine.
|
||||
- **Pre-configured Tools**: Include all necessary tools and dependencies in the container.
|
||||
|
||||
## DevContainer Setup
|
||||
|
||||
The DevContainer configuration provides up all the necessary services for Paperless NGX, including:
|
||||
|
||||
- Redis
|
||||
- Gotenberg
|
||||
- Tika
|
||||
|
||||
Data is stored using Docker volumes to ensure persistence across container restarts.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
The setup includes debugging configurations (`launch.json`) and tasks (`tasks.json`) to help you manage and debug various parts of the project:
|
||||
|
||||
- **Backend Debugging:**
|
||||
- `manage.py runserver`
|
||||
- `manage.py document-consumer`
|
||||
- `celery`
|
||||
- **Maintenance Tasks:**
|
||||
- Create superuser
|
||||
- Run migrations
|
||||
- Recreate virtual environment (`.venv` with pipenv)
|
||||
- Compile frontend assets
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Step 1: Running the DevContainer
|
||||
|
||||
To start the DevContainer:
|
||||
|
||||
1. Open VSCode.
|
||||
2. Open the project folder.
|
||||
3. Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
4. Type and select `Dev Containers: Rebuild and Reopen in Container`.
|
||||
|
||||
VSCode will build and start the DevContainer environment.
|
||||
|
||||
### Step 2: Initial Setup
|
||||
|
||||
Once the DevContainer is up and running, perform the following steps:
|
||||
|
||||
1. **Compile Frontend Assets**:
|
||||
|
||||
- Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
- Select `Tasks: Run Task`.
|
||||
- Choose `Frontend Compile`.
|
||||
|
||||
2. **Run Database Migrations**:
|
||||
|
||||
- Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
- Select `Tasks: Run Task`.
|
||||
- Choose `Migrate Database`.
|
||||
|
||||
3. **Create Superuser**:
|
||||
- Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
- Select `Tasks: Run Task`.
|
||||
- Choose `Create Superuser`.
|
||||
|
||||
### Debugging and Running Services
|
||||
|
||||
You can start and debug backend services either as debugging sessions via `launch.json` or as tasks.
|
||||
|
||||
#### Using `launch.json`:
|
||||
|
||||
1. Press `F5` or go to the **Run and Debug** view in VSCode.
|
||||
2. Select the desired configuration:
|
||||
- `Runserver`
|
||||
- `Document Consumer`
|
||||
- `Celery`
|
||||
|
||||
#### Using Tasks:
|
||||
|
||||
1. Open the command palette:
|
||||
- **Windows/Linux**: `Ctrl+Shift+P`
|
||||
- **Mac**: `Cmd+Shift+P`
|
||||
2. Select `Tasks: Run Task`.
|
||||
3. Choose the desired task:
|
||||
- `Runserver`
|
||||
- `Document Consumer`
|
||||
- `Celery`
|
||||
|
||||
### Additional Maintenance Tasks
|
||||
|
||||
Additional tasks are available for common maintenance operations:
|
||||
|
||||
- **Recreate .venv**: For setting up the virtual environment using pipenv.
|
||||
- **Migrate Database**: To apply database migrations.
|
||||
- **Create Superuser**: To create an admin user for the application.
|
||||
|
||||
## Let's Get Started!
|
||||
|
||||
Follow the steps above to get your development environment up and running. Happy coding!
|
16
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Paperless Development",
|
||||
"dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml",
|
||||
"service": "paperless-development",
|
||||
"workspaceFolder": "/usr/src/paperless/paperless-ngx",
|
||||
"postCreateCommand": "/bin/bash -c pre-commit install && pipenv install --dev",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"mhutchie.git-graph",
|
||||
"ms-python.python"
|
||||
]
|
||||
}
|
||||
},
|
||||
"remoteUser": "paperless"
|
||||
}
|
84
.devcontainer/docker-compose.devcontainer.sqlite-tika.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
# Docker Compose file for developing Paperless NGX in VSCode DevContainers.
|
||||
# This file contains everything Paperless NGX needs to run.
|
||||
# Paperless supports amd64, arm, and arm64 hardware.
|
||||
# All compose files of Paperless configure it in the following way:
|
||||
#
|
||||
# - Paperless is (re)started on system boot if it was running before shutdown.
|
||||
# - Docker volumes for storing data are managed by Docker.
|
||||
# - Folders for importing and exporting files are created in the same directory
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8000.
|
||||
#
|
||||
# SQLite is used as the database. The SQLite file is stored in the data volume.
|
||||
#
|
||||
# In addition, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Apache Tika and Gotenberg servers are started with Paperless NGX and Paperless
|
||||
# is configured to use these services. These provide support for consuming
|
||||
# Office documents (Word, Excel, PowerPoint, and their LibreOffice counterparts).
|
||||
#
|
||||
# This file is intended only to be used through VSCOde devcontainers. See README.md
|
||||
# in the folder .devcontainer.
|
||||
|
||||
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:7
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
# No ports need to be exposed; the VSCode DevContainer plugin manages them.
|
||||
paperless-development:
|
||||
image: paperless-ngx
|
||||
build:
|
||||
context: ../ # Dockerfile cannot access files from parent directories if context is not set.
|
||||
dockerfile: ./.devcontainer/Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- broker
|
||||
- gotenberg
|
||||
- tika
|
||||
volumes:
|
||||
- ..:/usr/src/paperless/paperless-ngx:delegated
|
||||
- ../.devcontainer/vscode:/usr/src/paperless/paperless-ngx/.vscode:delegated # VSCode config files
|
||||
- pipenv:/usr/src/paperless/paperless-ngx/.venv # Pipenv environment persisted in volume
|
||||
- /usr/src/paperless/paperless-ngx/src/documents/static/frontend # Static frontend files exist only in container
|
||||
- /usr/src/paperless/paperless-ngx/src/.pytest_cache
|
||||
- /usr/src/paperless/paperless-ngx/.ruff_cache
|
||||
- /usr/src/paperless/paperless-ngx/htmlcov
|
||||
- /usr/src/paperless/paperless-ngx/.coverage
|
||||
- data:/usr/src/paperless/paperless-ngx/data
|
||||
- media:/usr/src/paperless/paperless-ngx/media
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_TIKA_ENABLED: 1
|
||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
PAPERLESS_STATICDIR: ./src/documents/static
|
||||
PAPERLESS_DEBUG: true
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: /bin/sh -c "chown -R paperless:paperless /usr/src/paperless/paperless-ngx/src/documents/static/frontend && chown -R paperless:paperless /usr/src/paperless/paperless-ngx/.ruff_cache && while sleep 1000; do :; done"
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.10
|
||||
restart: unless-stopped
|
||||
|
||||
# The Gotenberg Chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even JavaScript.
|
||||
command:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-javascript=true"
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
|
||||
tika:
|
||||
image: docker.io/apache/tika:latest
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
redisdata:
|
||||
pipenv:
|
43
.devcontainer/vscode/launch.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "manage.py runserver",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/manage.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": ["runserver"],
|
||||
"django": true
|
||||
},
|
||||
{
|
||||
"name": "manage.py document_consumer",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/manage.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"args": ["document_consumer"],
|
||||
"django": true
|
||||
},
|
||||
{
|
||||
"name": "celery",
|
||||
"type": "python",
|
||||
"cwd": "${workspaceFolder}/src",
|
||||
"request": "launch",
|
||||
"module": "celery",
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"PYTHONPATH": "${workspaceFolder}/src"
|
||||
},
|
||||
"args": [
|
||||
"-A",
|
||||
"paperless",
|
||||
"worker",
|
||||
"-l",
|
||||
"DEBUG"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
11
.devcontainer/vscode/settings.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"src"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"files.watcherExclude": {
|
||||
"**/.venv/**": true,
|
||||
"**/pytest_cache/**": true
|
||||
}
|
||||
}
|
136
.devcontainer/vscode/tasks.json
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "manage.py document_consumer",
|
||||
"type": "shell",
|
||||
"command": "pipenv run python manage.py document_consumer",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true,
|
||||
"revealProblems": "onProblem"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src"
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"label": "manage.py runserver",
|
||||
"type": "shell",
|
||||
"command": "pipenv run python manage.py runserver",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true,
|
||||
"revealProblems": "onProblem"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src"
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"label": "Maintenance: manage.py migrate",
|
||||
"type": "shell",
|
||||
"command": "pipenv run python manage.py migrate",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": true,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true,
|
||||
"revealProblems": "onProblem"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Maintenance: manage.py createsuperuser",
|
||||
"type": "shell",
|
||||
"command": "pipenv run python manage.py createsuperuser",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": true,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true,
|
||||
"revealProblems": "onProblem"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "compile frontend",
|
||||
"type": "shell",
|
||||
"command": "npm ci && ./node_modules/.bin/ng build --configuration production",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": true,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true,
|
||||
"revealProblems": "onProblem"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src-ui"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Maintenance: recreate .venv",
|
||||
"type": "shell",
|
||||
"command": "rm -R -v .venv/* || pipenv install --dev",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": true,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true,
|
||||
"revealProblems": "onProblem"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Celery Worker",
|
||||
"type": "shell",
|
||||
"command": "pipenv run celery --app paperless worker -l DEBUG",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": true,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": true,
|
||||
"revealProblems": "onProblem"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,21 +1,28 @@
|
||||
# Tool caches
|
||||
**/__pycache__
|
||||
/src-ui/.vscode
|
||||
/src-ui/node_modules
|
||||
/src-ui/dist
|
||||
**/.ruff_cache/
|
||||
**/.mypy_cache/
|
||||
# Virtual environment & similar
|
||||
.venv/
|
||||
./src-ui/node_modules
|
||||
./src-ui/dist
|
||||
# IDE folders
|
||||
.idea/
|
||||
.vscode/
|
||||
./src-ui/.vscode
|
||||
# VCS
|
||||
.git
|
||||
/export
|
||||
/consume
|
||||
/media
|
||||
/data
|
||||
/docs
|
||||
.pytest_cache
|
||||
/dist
|
||||
/scripts
|
||||
/resources
|
||||
# Test related
|
||||
**/.pytest_cache
|
||||
**/tests
|
||||
**/*.spec.ts
|
||||
**/htmlcov
|
||||
/src/.pytest_cache
|
||||
.idea
|
||||
.venv/
|
||||
.vscode/
|
||||
# Local folders
|
||||
./export
|
||||
./consume
|
||||
./media
|
||||
./data
|
||||
./docs
|
||||
./dist
|
||||
./scripts
|
||||
./resources
|
||||
|
1
.env
@@ -1,2 +1 @@
|
||||
COMPOSE_PROJECT_NAME=paperless
|
||||
export PROMPT="(pipenv-projectname)$P$G"
|
||||
|
14
.github/DISCUSSION_TEMPLATE/feature-requests.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
title: "[Feature Request] "
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what you would like to see.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: Other
|
||||
description: Add any other context or information about the feature request here.
|
35
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -6,14 +6,21 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Have a question? 👉 [Start a new discussion](https://github.com/paperless-ngx/paperless-ngx/discussions/new) or [ask in chat](https://matrix.to/#/#paperlessngx:matrix.org).
|
||||
### ⚠️ Please remember: issues are for *bugs*
|
||||
That is, something you believe affects every single user of Paperless-ngx, not just you. If you're not sure, start with one of the other options below.
|
||||
|
||||
Before opening an issue, please double check:
|
||||
Also, note that **Paperless-ngx does not perform OCR or archive file creation itself**, those are handled by other tools. Problems with OCR or archive versions of specific files should likely be raised 'upstream', see https://github.com/ocrmypdf/OCRmyPDF/issues or https://github.com/tesseract-ocr/tesseract/issues
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
#### Have a question? 👉 [Start a new discussion](https://github.com/paperless-ngx/paperless-ngx/discussions/new) or [ask in chat](https://matrix.to/#/#paperlessngx:matrix.org).
|
||||
|
||||
#### Before opening an issue, please double check:
|
||||
|
||||
- [The troubleshooting documentation](https://docs.paperless-ngx.com/troubleshooting/).
|
||||
- [The installation instructions](https://docs.paperless-ngx.com/setup/#installation).
|
||||
- [Existing issues and discussions](https://github.com/paperless-ngx/paperless-ngx/search?q=&type=issues).
|
||||
- Disable any customer container initialization scripts, if using any
|
||||
- Disable any custom container initialization scripts, if using
|
||||
|
||||
If you encounter issues while installing or configuring Paperless-ngx, please post in the ["Support" section of the discussions](https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=support).
|
||||
- type: textarea
|
||||
@@ -79,6 +86,12 @@ body:
|
||||
description: Note there are significant differences from the official image and linuxserver.io, please check if your issue is specific to the third-party image.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system-status
|
||||
attributes:
|
||||
label: System status
|
||||
description: If available, copy & paste the system status output from Settings > System Status > Copy
|
||||
render: json
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
@@ -90,8 +103,16 @@ body:
|
||||
attributes:
|
||||
label: Configuration changes
|
||||
description: Any configuration changes you made in `docker-compose.yml`, `docker-compose.env` or `paperless.conf`.
|
||||
- type: input
|
||||
id: other
|
||||
- type: checkboxes
|
||||
id: required-checks
|
||||
attributes:
|
||||
label: Other
|
||||
description: Any other relevant details.
|
||||
label: Please confirm the following
|
||||
options:
|
||||
- label: I believe this issue is a bug that affects all users of Paperless-ngx, not something specific to my installation.
|
||||
required: true
|
||||
- label: This issue is not about the OCR or archive creation of a specific file(s). Otherwise, please see above regarding OCR tools.
|
||||
required: true
|
||||
- label: I have already searched for relevant existing issues and discussions before opening this report.
|
||||
required: true
|
||||
- label: I have updated the title field above with a concise description.
|
||||
required: true
|
||||
|
20
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -8,7 +8,11 @@ Note: All PRs with code changes should be targeted to the `dev` branch, pure doc
|
||||
Please include a summary of the change and which issue is fixed (if any) and any relevant motivation / context. List any dependencies that are required for this change. If appropriate, please include an explanation of how your proposed change can be tested. Screenshots and / or videos can also be helpful if appropriate.
|
||||
-->
|
||||
|
||||
Fixes # (issue)
|
||||
<!--
|
||||
⚠️ Important: Pull requests that implement a new feature or enhancement *should almost always target an existing feature request* with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. If that is not currently the case, please open a feature request instead of this PR to gather feedback from both users and the project maintainers.
|
||||
-->
|
||||
|
||||
Closes #(issue or discussion)
|
||||
|
||||
## Type of change
|
||||
|
||||
@@ -17,14 +21,20 @@ What type of change does your PR introduce to Paperless-ngx?
|
||||
NOTE: Please check only one box!
|
||||
-->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Other (please explain)
|
||||
- [ ] Bug fix: non-breaking change which fixes an issue.
|
||||
- [ ] New feature / Enhancement: non-breaking change which adds functionality. _Please read the important note above._
|
||||
- [ ] Breaking change: fix or feature that would cause existing functionality to not work as expected.
|
||||
- [ ] Documentation only.
|
||||
- [ ] Other. Please explain:
|
||||
|
||||
## Checklist:
|
||||
|
||||
<!--
|
||||
NOTE: PRs that do not address the following will not be merged, please do not skip any relevant items.
|
||||
-->
|
||||
|
||||
- [ ] I have read & agree with the [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md).
|
||||
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
|
||||
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
|
||||
- [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
|
||||
|
43
.github/dependabot.yml
vendored
@@ -8,7 +8,7 @@ updates:
|
||||
target-branch: "dev"
|
||||
# Look for `package.json` and `lock` files in the `/src-ui` directory
|
||||
directory: "/src-ui"
|
||||
# Check the npm registry for updates every month
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
labels:
|
||||
@@ -17,6 +17,21 @@ updates:
|
||||
# Add reviewers
|
||||
reviewers:
|
||||
- "paperless-ngx/frontend"
|
||||
groups:
|
||||
frontend-angular-dependencies:
|
||||
patterns:
|
||||
- "@angular*"
|
||||
- "@ng-*"
|
||||
- "ngx-*"
|
||||
- "ng2-pdf-viewer"
|
||||
frontend-jest-dependencies:
|
||||
patterns:
|
||||
- "@types/jest"
|
||||
- "jest*"
|
||||
frontend-eslint-dependencies:
|
||||
patterns:
|
||||
- "@typescript-eslint*"
|
||||
- "eslint"
|
||||
|
||||
# Enable version updates for Python
|
||||
- package-ecosystem: "pip"
|
||||
@@ -32,8 +47,26 @@ updates:
|
||||
# Add reviewers
|
||||
reviewers:
|
||||
- "paperless-ngx/backend"
|
||||
ignore:
|
||||
- dependency-name: "uvicorn"
|
||||
groups:
|
||||
development:
|
||||
patterns:
|
||||
- "*pytest*"
|
||||
- "ruff"
|
||||
- "mkdocs-material"
|
||||
django:
|
||||
patterns:
|
||||
- "*django*"
|
||||
major-versions:
|
||||
update-types:
|
||||
- "major"
|
||||
small-changes:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# Enable updates for Github Actions
|
||||
# Enable updates for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
target-branch: "dev"
|
||||
directory: "/"
|
||||
@@ -46,3 +79,9 @@ updates:
|
||||
# Add reviewers
|
||||
reviewers:
|
||||
- "paperless-ngx/ci-cd"
|
||||
groups:
|
||||
actions:
|
||||
update-types:
|
||||
- "major"
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
488
.github/scripts/cleanup-tags.py
vendored
@@ -1,488 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
from typing import Dict
|
||||
from typing import Final
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from common import get_log_level
|
||||
from github import ContainerPackage
|
||||
from github import GithubBranchApi
|
||||
from github import GithubContainerRegistryApi
|
||||
|
||||
logger = logging.getLogger("cleanup-tags")
|
||||
|
||||
|
||||
class ImageProperties:
|
||||
"""
|
||||
Data class wrapping the properties of an entry in the image index
|
||||
manifests list. It is NOT an actual image with layers, etc
|
||||
|
||||
https://docs.docker.com/registry/spec/manifest-v2-2/
|
||||
https://github.com/opencontainers/image-spec/blob/main/manifest.md
|
||||
https://github.com/opencontainers/image-spec/blob/main/descriptor.md
|
||||
"""
|
||||
|
||||
def __init__(self, data: Dict) -> None:
|
||||
self._data = data
|
||||
# This is the sha256: digest string. Corresponds to GitHub API name
|
||||
# if the package is an untagged package
|
||||
self.digest = self._data["digest"]
|
||||
platform_data_os = self._data["platform"]["os"]
|
||||
platform_arch = self._data["platform"]["architecture"]
|
||||
platform_variant = self._data["platform"].get(
|
||||
"variant",
|
||||
"",
|
||||
)
|
||||
self.platform = f"{platform_data_os}/{platform_arch}{platform_variant}"
|
||||
|
||||
|
||||
class ImageIndex:
|
||||
"""
|
||||
Data class wrapping up logic for an OCI Image Index
|
||||
JSON data. Primary use is to access the manifests listing
|
||||
|
||||
See https://github.com/opencontainers/image-spec/blob/main/image-index.md
|
||||
"""
|
||||
|
||||
def __init__(self, package_url: str, tag: str) -> None:
|
||||
self.qualified_name = f"{package_url}:{tag}"
|
||||
logger.info(f"Getting image index for {self.qualified_name}")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[
|
||||
shutil.which("docker"),
|
||||
"buildx",
|
||||
"imagetools",
|
||||
"inspect",
|
||||
"--raw",
|
||||
self.qualified_name,
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
self._data = json.loads(proc.stdout)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(
|
||||
f"Failed to get image index for {self.qualified_name}: {e.stderr}",
|
||||
)
|
||||
raise e
|
||||
|
||||
@property
|
||||
def image_pointers(self) -> Iterator[ImageProperties]:
|
||||
for manifest_data in self._data["manifests"]:
|
||||
yield ImageProperties(manifest_data)
|
||||
|
||||
|
||||
class RegistryTagsCleaner:
|
||||
"""
|
||||
This is the base class for the image registry cleaning. Given a package
|
||||
name, it will keep all images which are tagged and all untagged images
|
||||
referred to by a manifest. This results in only images which have been untagged
|
||||
and cannot be referenced except by their SHA in being removed. None of these
|
||||
images should be referenced, so it is fine to delete them.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
package_name: str,
|
||||
repo_owner: str,
|
||||
repo_name: str,
|
||||
package_api: GithubContainerRegistryApi,
|
||||
branch_api: Optional[GithubBranchApi],
|
||||
):
|
||||
self.actually_delete = False
|
||||
self.package_api = package_api
|
||||
self.branch_api = branch_api
|
||||
self.package_name = package_name
|
||||
self.repo_owner = repo_owner
|
||||
self.repo_name = repo_name
|
||||
self.tags_to_delete: List[str] = []
|
||||
self.tags_to_keep: List[str] = []
|
||||
|
||||
# Get the information about all versions of the given package
|
||||
# These are active, not deleted, the default returned from the API
|
||||
self.all_package_versions = self.package_api.get_active_package_versions(
|
||||
self.package_name,
|
||||
)
|
||||
|
||||
# Get a mapping from a tag like "1.7.0" or "feature-xyz" to the ContainerPackage
|
||||
# tagged with it. It makes certain lookups easy
|
||||
self.all_pkgs_tags_to_version: Dict[str, ContainerPackage] = {}
|
||||
for pkg in self.all_package_versions:
|
||||
for tag in pkg.tags:
|
||||
self.all_pkgs_tags_to_version[tag] = pkg
|
||||
logger.info(
|
||||
f"Located {len(self.all_package_versions)} versions of package {self.package_name}",
|
||||
)
|
||||
|
||||
self.decide_what_tags_to_keep()
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
This method will delete image versions, based on the selected tags to delete.
|
||||
It behaves more like an unlinking than actual deletion. Removing the tag
|
||||
simply removes a pointer to an image, but the actual image data remains accessible
|
||||
if one has the sha256 digest of it.
|
||||
"""
|
||||
for tag_to_delete in self.tags_to_delete:
|
||||
package_version_info = self.all_pkgs_tags_to_version[tag_to_delete]
|
||||
|
||||
if self.actually_delete:
|
||||
logger.info(
|
||||
f"Deleting {tag_to_delete} (id {package_version_info.id})",
|
||||
)
|
||||
self.package_api.delete_package_version(
|
||||
package_version_info,
|
||||
)
|
||||
|
||||
else:
|
||||
logger.info(
|
||||
f"Would delete {tag_to_delete} (id {package_version_info.id})",
|
||||
)
|
||||
else:
|
||||
logger.info("No tags to delete")
|
||||
|
||||
def clean_untagged(self, is_manifest_image: bool):
|
||||
"""
|
||||
This method will delete untagged images, that is those which are not named. It
|
||||
handles if the image tag is actually a manifest, which points to images that look otherwise
|
||||
untagged.
|
||||
"""
|
||||
|
||||
def _clean_untagged_manifest():
|
||||
"""
|
||||
|
||||
Handles the deletion of untagged images, but where the package is a manifest, ie a multi
|
||||
arch image, which means some "untagged" images need to exist still.
|
||||
|
||||
Ok, bear with me, these are annoying.
|
||||
|
||||
Our images are multi-arch, so the manifest is more like a pointer to a sha256 digest.
|
||||
These images are untagged, but pointed to, and so should not be removed (or every pull fails).
|
||||
|
||||
So for each image getting kept, parse the manifest to find the digest(s) it points to. Then
|
||||
remove those from the list of untagged images. The final result is the untagged, not pointed to
|
||||
version which should be safe to remove.
|
||||
|
||||
Example:
|
||||
Tag: ghcr.io/paperless-ngx/paperless-ngx:1.7.1 refers to
|
||||
amd64: sha256:b9ed4f8753bbf5146547671052d7e91f68cdfc9ef049d06690b2bc866fec2690
|
||||
armv7: sha256:81605222df4ba4605a2ba4893276e5d08c511231ead1d5da061410e1bbec05c3
|
||||
arm64: sha256:374cd68db40734b844705bfc38faae84cc4182371de4bebd533a9a365d5e8f3b
|
||||
each of which appears as untagged image, but isn't really.
|
||||
|
||||
So from the list of untagged packages, remove those digests. Once all tags which
|
||||
are being kept are checked, the remaining untagged packages are actually untagged
|
||||
with no referrals in a manifest to them.
|
||||
"""
|
||||
# Simplify the untagged data, mapping name (which is a digest) to the version
|
||||
# At the moment, these are the images which APPEAR untagged.
|
||||
untagged_versions = {}
|
||||
for x in self.all_package_versions:
|
||||
if x.untagged:
|
||||
untagged_versions[x.name] = x
|
||||
|
||||
skips = 0
|
||||
|
||||
# Parse manifests to locate digests pointed to
|
||||
for tag in sorted(self.tags_to_keep):
|
||||
try:
|
||||
image_index = ImageIndex(
|
||||
f"ghcr.io/{self.repo_owner}/{self.package_name}",
|
||||
tag,
|
||||
)
|
||||
for manifest in image_index.image_pointers:
|
||||
|
||||
if manifest.digest in untagged_versions:
|
||||
logger.info(
|
||||
f"Skipping deletion of {manifest.digest},"
|
||||
f" referred to by {image_index.qualified_name}"
|
||||
f" for {manifest.platform}",
|
||||
)
|
||||
del untagged_versions[manifest.digest]
|
||||
skips += 1
|
||||
|
||||
except Exception as err:
|
||||
self.actually_delete = False
|
||||
logger.exception(err)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"Skipping deletion of {skips} packages referred to by a manifest",
|
||||
)
|
||||
|
||||
# Delete the untagged and not pointed at packages
|
||||
logger.info(f"Deleting untagged packages of {self.package_name}")
|
||||
for to_delete_name in untagged_versions:
|
||||
to_delete_version = untagged_versions[to_delete_name]
|
||||
|
||||
if self.actually_delete:
|
||||
logger.info(
|
||||
f"Deleting id {to_delete_version.id} named {to_delete_version.name}",
|
||||
)
|
||||
self.package_api.delete_package_version(
|
||||
to_delete_version,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Would delete {to_delete_name} (id {to_delete_version.id})",
|
||||
)
|
||||
|
||||
def _clean_untagged_non_manifest():
|
||||
"""
|
||||
If the package is not a multi-arch manifest, images without tags are safe to delete.
|
||||
"""
|
||||
|
||||
for package in self.all_package_versions:
|
||||
if package.untagged:
|
||||
if self.actually_delete:
|
||||
logger.info(
|
||||
f"Deleting id {package.id} named {package.name}",
|
||||
)
|
||||
self.package_api.delete_package_version(
|
||||
package,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Would delete {package.name} (id {package.id})",
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Not deleting tag {package.tags[0]} of package {self.package_name}",
|
||||
)
|
||||
|
||||
logger.info("Beginning untagged image cleaning")
|
||||
|
||||
if is_manifest_image:
|
||||
_clean_untagged_manifest()
|
||||
else:
|
||||
_clean_untagged_non_manifest()
|
||||
|
||||
def decide_what_tags_to_keep(self):
|
||||
"""
|
||||
This method holds the logic to delete what tags to keep and there fore
|
||||
what tags to delete.
|
||||
|
||||
By default, any image with at least 1 tag will be kept
|
||||
"""
|
||||
# By default, keep anything which is tagged
|
||||
self.tags_to_keep = list(set(self.all_pkgs_tags_to_version.keys()))
|
||||
|
||||
def check_remaining_tags_valid(self):
|
||||
"""
|
||||
Checks the non-deleted tags are still valid. The assumption is if the
|
||||
manifest is can be inspected and each image manifest if points to can be
|
||||
inspected, the image will still pull.
|
||||
|
||||
https://github.com/opencontainers/image-spec/blob/main/image-index.md
|
||||
"""
|
||||
logger.info("Beginning confirmation step")
|
||||
a_tag_failed = False
|
||||
for tag in sorted(self.tags_to_keep):
|
||||
|
||||
try:
|
||||
image_index = ImageIndex(
|
||||
f"ghcr.io/{self.repo_owner}/{self.package_name}",
|
||||
tag,
|
||||
)
|
||||
for manifest in image_index.image_pointers:
|
||||
logger.info(f"Checking {manifest.digest} for {manifest.platform}")
|
||||
|
||||
# This follows the pointer from the index to an actual image, layers and all
|
||||
# Note the format is @
|
||||
digest_name = f"ghcr.io/{self.repo_owner}/{self.package_name}@{manifest.digest}"
|
||||
|
||||
try:
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
shutil.which("docker"),
|
||||
"buildx",
|
||||
"imagetools",
|
||||
"inspect",
|
||||
"--raw",
|
||||
digest_name,
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to inspect digest: {e.stderr}")
|
||||
a_tag_failed = True
|
||||
except subprocess.CalledProcessError as e:
|
||||
a_tag_failed = True
|
||||
logger.error(f"Failed to inspect: {e.stderr}")
|
||||
continue
|
||||
|
||||
if a_tag_failed:
|
||||
raise Exception("At least one image tag failed to inspect")
|
||||
|
||||
|
||||
class MainImageTagsCleaner(RegistryTagsCleaner):
|
||||
def decide_what_tags_to_keep(self):
|
||||
"""
|
||||
Overrides the default logic for deciding what images to keep. Images tagged as "feature-"
|
||||
will be removed, if the corresponding branch no longer exists.
|
||||
"""
|
||||
|
||||
# Default to everything gets kept still
|
||||
super().decide_what_tags_to_keep()
|
||||
|
||||
# Locate the feature branches
|
||||
feature_branches = {}
|
||||
for branch in self.branch_api.get_branches(
|
||||
repo=self.repo_name,
|
||||
):
|
||||
if branch.name.startswith("feature-"):
|
||||
logger.debug(f"Found feature branch {branch.name}")
|
||||
feature_branches[branch.name] = branch
|
||||
|
||||
logger.info(f"Located {len(feature_branches)} feature branches")
|
||||
|
||||
if not len(feature_branches):
|
||||
# Our work here is done, delete nothing
|
||||
return
|
||||
|
||||
# Filter to packages which are tagged with feature-*
|
||||
packages_tagged_feature: List[ContainerPackage] = []
|
||||
for package in self.all_package_versions:
|
||||
if package.tag_matches("feature-"):
|
||||
packages_tagged_feature.append(package)
|
||||
|
||||
# Map tags like "feature-xyz" to a ContainerPackage
|
||||
feature_pkgs_tags_to_versions: Dict[str, ContainerPackage] = {}
|
||||
for pkg in packages_tagged_feature:
|
||||
for tag in pkg.tags:
|
||||
feature_pkgs_tags_to_versions[tag] = pkg
|
||||
|
||||
logger.info(
|
||||
f'Located {len(feature_pkgs_tags_to_versions)} versions of package {self.package_name} tagged "feature-"',
|
||||
)
|
||||
|
||||
# All the feature tags minus all the feature branches leaves us feature tags
|
||||
# with no corresponding branch
|
||||
self.tags_to_delete = list(
|
||||
set(feature_pkgs_tags_to_versions.keys()) - set(feature_branches.keys()),
|
||||
)
|
||||
|
||||
# All the tags minus the set of going to be deleted tags leaves us the
|
||||
# tags which will be kept around
|
||||
self.tags_to_keep = list(
|
||||
set(self.all_pkgs_tags_to_version.keys()) - set(self.tags_to_delete),
|
||||
)
|
||||
logger.info(
|
||||
f"Located {len(self.tags_to_delete)} versions of package {self.package_name} to delete",
|
||||
)
|
||||
|
||||
|
||||
class LibraryTagsCleaner(RegistryTagsCleaner):
|
||||
"""
|
||||
Exists for the off chance that someday, the installer library images
|
||||
will need their own logic
|
||||
"""
|
||||
|
||||
|
||||
def _main():
|
||||
parser = ArgumentParser(
|
||||
description="Using the GitHub API locate and optionally delete container"
|
||||
" tags which no longer have an associated feature branch",
|
||||
)
|
||||
|
||||
# Requires an affirmative command to actually do a delete
|
||||
parser.add_argument(
|
||||
"--delete",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="If provided, actually delete the container tags",
|
||||
)
|
||||
|
||||
# When a tagged image is updated, the previous version remains, but it no longer tagged
|
||||
# Add this option to remove them as well
|
||||
parser.add_argument(
|
||||
"--untagged",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="If provided, delete untagged containers as well",
|
||||
)
|
||||
|
||||
# If given, the package is assumed to be a multi-arch manifest. Cache packages are
|
||||
# not multi-arch, all other types are
|
||||
parser.add_argument(
|
||||
"--is-manifest",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="If provided, the package is assumed to be a multi-arch manifest following schema v2",
|
||||
)
|
||||
|
||||
# Allows configuration of log level for debugging
|
||||
parser.add_argument(
|
||||
"--loglevel",
|
||||
default="info",
|
||||
help="Configures the logging level",
|
||||
)
|
||||
|
||||
# Get the name of the package being processed this round
|
||||
parser.add_argument(
|
||||
"package",
|
||||
help="The package to process",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
level=get_log_level(args),
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
format="%(asctime)s %(levelname)-8s %(message)s",
|
||||
)
|
||||
|
||||
# Must be provided in the environment
|
||||
repo_owner: Final[str] = os.environ["GITHUB_REPOSITORY_OWNER"]
|
||||
repo: Final[str] = os.environ["GITHUB_REPOSITORY"]
|
||||
gh_token: Final[str] = os.environ["TOKEN"]
|
||||
|
||||
# Find all branches named feature-*
|
||||
# Note: Only relevant to the main application, but simpler to
|
||||
# leave in for all packages
|
||||
with GithubBranchApi(gh_token) as branch_api:
|
||||
with GithubContainerRegistryApi(gh_token, repo_owner) as container_api:
|
||||
if args.package in {"paperless-ngx", "paperless-ngx/builder/cache/app"}:
|
||||
cleaner = MainImageTagsCleaner(
|
||||
args.package,
|
||||
repo_owner,
|
||||
repo,
|
||||
container_api,
|
||||
branch_api,
|
||||
)
|
||||
else:
|
||||
cleaner = LibraryTagsCleaner(
|
||||
args.package,
|
||||
repo_owner,
|
||||
repo,
|
||||
container_api,
|
||||
None,
|
||||
)
|
||||
|
||||
# Set if actually doing a delete vs dry run
|
||||
cleaner.actually_delete = args.delete
|
||||
|
||||
# Clean images with tags
|
||||
cleaner.clean()
|
||||
|
||||
# Clean images which are untagged
|
||||
cleaner.clean_untagged(args.is_manifest)
|
||||
|
||||
# Verify remaining tags still pull
|
||||
if args.is_manifest:
|
||||
cleaner.check_remaining_tags_valid()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_main()
|
47
.github/scripts/common.py
vendored
@@ -1,47 +0,0 @@
|
||||
import logging
|
||||
|
||||
|
||||
def get_image_tag(
|
||||
repo_name: str,
|
||||
pkg_name: str,
|
||||
pkg_version: str,
|
||||
) -> str:
|
||||
"""
|
||||
Returns a string representing the normal image for a given package
|
||||
"""
|
||||
return f"ghcr.io/{repo_name.lower()}/builder/{pkg_name}:{pkg_version}"
|
||||
|
||||
|
||||
def get_cache_image_tag(
|
||||
repo_name: str,
|
||||
pkg_name: str,
|
||||
pkg_version: str,
|
||||
branch_name: str,
|
||||
) -> str:
|
||||
"""
|
||||
Returns a string representing the expected image cache tag for a given package
|
||||
|
||||
Registry type caching is utilized for the builder images, to allow fast
|
||||
rebuilds, generally almost instant for the same version
|
||||
"""
|
||||
return f"ghcr.io/{repo_name.lower()}/builder/cache/{pkg_name}:{pkg_version}"
|
||||
|
||||
|
||||
def get_log_level(args) -> int:
|
||||
"""
|
||||
Returns a logging level, based
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
levels = {
|
||||
"critical": logging.CRITICAL,
|
||||
"error": logging.ERROR,
|
||||
"warn": logging.WARNING,
|
||||
"warning": logging.WARNING,
|
||||
"info": logging.INFO,
|
||||
"debug": logging.DEBUG,
|
||||
}
|
||||
level = levels.get(args.loglevel.lower())
|
||||
if level is None:
|
||||
level = logging.INFO
|
||||
return level
|
91
.github/scripts/get-build-json.py
vendored
@@ -1,91 +0,0 @@
|
||||
"""
|
||||
This is a helper script for the mutli-stage Docker image builder.
|
||||
It provides a single point of configuration for package version control.
|
||||
The output JSON object is used by the CI workflow to determine what versions
|
||||
to build and pull into the final Docker image.
|
||||
|
||||
Python package information is obtained from the Pipfile.lock. As this is
|
||||
kept updated by dependabot, it usually will need no further configuration.
|
||||
The sole exception currently is pikepdf, which has a dependency on qpdf,
|
||||
and is configured here to use the latest version of qpdf built by the workflow.
|
||||
|
||||
Other package version information is configured directly below, generally by
|
||||
setting the version and Git information, if any.
|
||||
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from common import get_cache_image_tag
|
||||
from common import get_image_tag
|
||||
|
||||
|
||||
def _main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate a JSON object of information required to build the given package, based on the Pipfile.lock",
|
||||
)
|
||||
parser.add_argument(
|
||||
"package",
|
||||
help="The name of the package to generate JSON for",
|
||||
)
|
||||
|
||||
PIPFILE_LOCK_PATH: Final[Path] = Path("Pipfile.lock")
|
||||
BUILD_CONFIG_PATH: Final[Path] = Path(".build-config.json")
|
||||
|
||||
# Read the main config file
|
||||
build_json: Final = json.loads(BUILD_CONFIG_PATH.read_text())
|
||||
|
||||
# Read Pipfile.lock file
|
||||
pipfile_data: Final = json.loads(PIPFILE_LOCK_PATH.read_text())
|
||||
|
||||
args: Final = parser.parse_args()
|
||||
|
||||
# Read from environment variables set by GitHub Actions
|
||||
repo_name: Final[str] = os.environ["GITHUB_REPOSITORY"]
|
||||
branch_name: Final[str] = os.environ["GITHUB_REF_NAME"]
|
||||
|
||||
# Default output values
|
||||
version = None
|
||||
extra_config = {}
|
||||
|
||||
if args.package in pipfile_data["default"]:
|
||||
# Read the version from Pipfile.lock
|
||||
pkg_data = pipfile_data["default"][args.package]
|
||||
pkg_version = pkg_data["version"].split("==")[-1]
|
||||
version = pkg_version
|
||||
|
||||
# Any extra/special values needed
|
||||
if args.package == "pikepdf":
|
||||
extra_config["qpdf_version"] = build_json["qpdf"]["version"]
|
||||
|
||||
elif args.package in build_json:
|
||||
version = build_json[args.package]["version"]
|
||||
|
||||
else:
|
||||
raise NotImplementedError(args.package)
|
||||
|
||||
# The JSON object we'll output
|
||||
output = {
|
||||
"name": args.package,
|
||||
"version": version,
|
||||
"image_tag": get_image_tag(repo_name, args.package, version),
|
||||
"cache_tag": get_cache_image_tag(
|
||||
repo_name,
|
||||
args.package,
|
||||
version,
|
||||
branch_name,
|
||||
),
|
||||
}
|
||||
|
||||
# Add anything special a package may need
|
||||
output.update(extra_config)
|
||||
|
||||
# Output the JSON info to stdout
|
||||
print(json.dumps(output))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_main()
|
270
.github/scripts/github.py
vendored
@@ -1,270 +0,0 @@
|
||||
"""
|
||||
This module contains some useful classes for interacting with the Github API.
|
||||
The full documentation for the API can be found here: https://docs.github.com/en/rest
|
||||
|
||||
Mostly, this focusses on two areas, repo branches and repo packages, as the use case
|
||||
is cleaning up container images which are no longer referred to.
|
||||
|
||||
"""
|
||||
import functools
|
||||
import logging
|
||||
import re
|
||||
import urllib.parse
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger("github-api")
|
||||
|
||||
|
||||
class _GithubApiBase:
|
||||
"""
|
||||
A base class for interacting with the Github API. It
|
||||
will handle the session and setting authorization headers.
|
||||
"""
|
||||
|
||||
def __init__(self, token: str) -> None:
|
||||
self._token = token
|
||||
self._client: Optional[httpx.Client] = None
|
||||
|
||||
def __enter__(self) -> "_GithubApiBase":
|
||||
"""
|
||||
Sets up the required headers for auth and response
|
||||
type from the API
|
||||
"""
|
||||
self._client = httpx.Client()
|
||||
self._client.headers.update(
|
||||
{
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"Authorization": f"token {self._token}",
|
||||
},
|
||||
)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""
|
||||
Ensures the authorization token is cleaned up no matter
|
||||
the reason for the exit
|
||||
"""
|
||||
if "Accept" in self._client.headers:
|
||||
del self._client.headers["Accept"]
|
||||
if "Authorization" in self._client.headers:
|
||||
del self._client.headers["Authorization"]
|
||||
|
||||
# Close the session as well
|
||||
self._client.close()
|
||||
self._client = None
|
||||
|
||||
def _read_all_pages(self, endpoint):
|
||||
"""
|
||||
Helper function to read all pages of an endpoint, utilizing the
|
||||
next.url until exhausted. Assumes the endpoint returns a list
|
||||
"""
|
||||
internal_data = []
|
||||
|
||||
while True:
|
||||
resp = self._client.get(endpoint)
|
||||
if resp.status_code == 200:
|
||||
internal_data += resp.json()
|
||||
if "next" in resp.links:
|
||||
endpoint = resp.links["next"]["url"]
|
||||
else:
|
||||
logger.debug("Exiting pagination loop")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Request to {endpoint} return HTTP {resp.status_code}")
|
||||
resp.raise_for_status()
|
||||
|
||||
return internal_data
|
||||
|
||||
|
||||
class _EndpointResponse:
|
||||
"""
|
||||
For all endpoint JSON responses, store the full
|
||||
response data, for ease of extending later, if need be.
|
||||
"""
|
||||
|
||||
def __init__(self, data: Dict) -> None:
|
||||
self._data = data
|
||||
|
||||
|
||||
class GithubBranch(_EndpointResponse):
|
||||
"""
|
||||
Simple wrapper for a repository branch, only extracts name information
|
||||
for now.
|
||||
"""
|
||||
|
||||
def __init__(self, data: Dict) -> None:
|
||||
super().__init__(data)
|
||||
self.name = self._data["name"]
|
||||
|
||||
|
||||
class GithubBranchApi(_GithubApiBase):
|
||||
"""
|
||||
Wrapper around branch API.
|
||||
|
||||
See https://docs.github.com/en/rest/branches/branches
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, token: str) -> None:
|
||||
super().__init__(token)
|
||||
|
||||
self._ENDPOINT = "https://api.github.com/repos/{REPO}/branches"
|
||||
|
||||
def get_branches(self, repo: str) -> List[GithubBranch]:
|
||||
"""
|
||||
Returns all current branches of the given repository owned by the given
|
||||
owner or organization.
|
||||
"""
|
||||
# The environment GITHUB_REPOSITORY already contains the owner in the correct location
|
||||
endpoint = self._ENDPOINT.format(REPO=repo)
|
||||
internal_data = self._read_all_pages(endpoint)
|
||||
return [GithubBranch(branch) for branch in internal_data]
|
||||
|
||||
|
||||
class ContainerPackage(_EndpointResponse):
|
||||
"""
|
||||
Data class wrapping the JSON response from the package related
|
||||
endpoints
|
||||
"""
|
||||
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data)
|
||||
# This is a numerical ID, required for interactions with this
|
||||
# specific package, including deletion of it or restoration
|
||||
self.id: int = self._data["id"]
|
||||
|
||||
# A string name. This might be an actual name or it could be a
|
||||
# digest string like "sha256:"
|
||||
self.name: str = self._data["name"]
|
||||
|
||||
# URL to the package, including its ID, can be used for deletion
|
||||
# or restoration without needing to build up a URL ourselves
|
||||
self.url: str = self._data["url"]
|
||||
|
||||
# The list of tags applied to this image. Maybe an empty list
|
||||
self.tags: List[str] = self._data["metadata"]["container"]["tags"]
|
||||
|
||||
@functools.cached_property
|
||||
def untagged(self) -> bool:
|
||||
"""
|
||||
Returns True if the image has no tags applied to it, False otherwise
|
||||
"""
|
||||
return len(self.tags) == 0
|
||||
|
||||
@functools.cache
|
||||
def tag_matches(self, pattern: str) -> bool:
|
||||
"""
|
||||
Returns True if the image has at least one tag which matches the given regex,
|
||||
False otherwise
|
||||
"""
|
||||
return any(re.match(pattern, tag) is not None for tag in self.tags)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Package {self.name}"
|
||||
|
||||
|
||||
class GithubContainerRegistryApi(_GithubApiBase):
|
||||
"""
|
||||
Class wrapper to deal with the Github packages API. This class only deals with
|
||||
container type packages, the only type published by paperless-ngx.
|
||||
"""
|
||||
|
||||
def __init__(self, token: str, owner_or_org: str) -> None:
|
||||
super().__init__(token)
|
||||
self._owner_or_org = owner_or_org
|
||||
if self._owner_or_org == "paperless-ngx":
|
||||
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-an-organization
|
||||
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
|
||||
# https://docs.github.com/en/rest/packages#delete-package-version-for-an-organization
|
||||
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
|
||||
else:
|
||||
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-the-authenticated-user
|
||||
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
|
||||
# https://docs.github.com/en/rest/packages#delete-a-package-version-for-the-authenticated-user
|
||||
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
|
||||
self._PACKAGE_VERSION_RESTORE_ENDPOINT = (
|
||||
f"{self._PACKAGE_VERSION_DELETE_ENDPOINT}/restore"
|
||||
)
|
||||
|
||||
def get_active_package_versions(
|
||||
self,
|
||||
package_name: str,
|
||||
) -> List[ContainerPackage]:
|
||||
"""
|
||||
Returns all the versions of a given package (container images) from
|
||||
the API
|
||||
"""
|
||||
|
||||
package_type: str = "container"
|
||||
# Need to quote this for slashes in the name
|
||||
package_name = urllib.parse.quote(package_name, safe="")
|
||||
|
||||
endpoint = self._PACKAGES_VERSIONS_ENDPOINT.format(
|
||||
ORG=self._owner_or_org,
|
||||
PACKAGE_TYPE=package_type,
|
||||
PACKAGE_NAME=package_name,
|
||||
)
|
||||
|
||||
pkgs = []
|
||||
|
||||
for data in self._read_all_pages(endpoint):
|
||||
pkgs.append(ContainerPackage(data))
|
||||
|
||||
return pkgs
|
||||
|
||||
def get_deleted_package_versions(
|
||||
self,
|
||||
package_name: str,
|
||||
) -> List[ContainerPackage]:
|
||||
package_type: str = "container"
|
||||
# Need to quote this for slashes in the name
|
||||
package_name = urllib.parse.quote(package_name, safe="")
|
||||
|
||||
endpoint = (
|
||||
self._PACKAGES_VERSIONS_ENDPOINT.format(
|
||||
ORG=self._owner_or_org,
|
||||
PACKAGE_TYPE=package_type,
|
||||
PACKAGE_NAME=package_name,
|
||||
)
|
||||
+ "?state=deleted"
|
||||
)
|
||||
|
||||
pkgs = []
|
||||
|
||||
for data in self._read_all_pages(endpoint):
|
||||
pkgs.append(ContainerPackage(data))
|
||||
|
||||
return pkgs
|
||||
|
||||
def delete_package_version(self, package_data: ContainerPackage):
|
||||
"""
|
||||
Deletes the given package version from the GHCR
|
||||
"""
|
||||
resp = self._client.delete(package_data.url)
|
||||
if resp.status_code != 204:
|
||||
logger.warning(
|
||||
f"Request to delete {package_data.url} returned HTTP {resp.status_code}",
|
||||
)
|
||||
|
||||
def restore_package_version(
|
||||
self,
|
||||
package_name: str,
|
||||
package_data: ContainerPackage,
|
||||
):
|
||||
package_type: str = "container"
|
||||
endpoint = self._PACKAGE_VERSION_RESTORE_ENDPOINT.format(
|
||||
ORG=self._owner_or_org,
|
||||
PACKAGE_TYPE=package_type,
|
||||
PACKAGE_NAME=package_name,
|
||||
PACKAGE_VERSION_ID=package_data.id,
|
||||
)
|
||||
|
||||
resp = self._client.post(endpoint)
|
||||
if resp.status_code != 204:
|
||||
logger.warning(
|
||||
f"Request to delete {endpoint} returned HTTP {resp.status_code}",
|
||||
)
|
23
.github/stale.yml
vendored
@@ -1,23 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: [cant-reproduce]
|
||||
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
|
||||
# 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
|
||||
|
||||
# See https://github.com/marketplace/stale for more info on the app
|
||||
# and https://github.com/probot/stale for the configuration docs
|
444
.github/workflows/ci.yml
vendored
@@ -16,41 +16,47 @@ on:
|
||||
env:
|
||||
# This is the version of pipenv all the steps will use
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PIP_ENV_VERSION: "2023.3.20"
|
||||
# This is the default version of Python to use in most steps
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PYTHON_VERSION: "3.9"
|
||||
DEFAULT_PIP_ENV_VERSION: "2024.0.1"
|
||||
# This is the default version of Python to use in most steps which aren't specific
|
||||
DEFAULT_PYTHON_VERSION: "3.10"
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
# We want to run on external PRs, but not on our own internal PRs as they'll be run
|
||||
# by the push to the branch. Without this if check, checks are duplicated since
|
||||
# internal PRs match both the push and pull_request events.
|
||||
if:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name !=
|
||||
github.repository
|
||||
|
||||
name: Linting Checks
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
-
|
||||
name: Check files
|
||||
uses: pre-commit/action@v3.0.0
|
||||
uses: pre-commit/action@v3.0.1
|
||||
|
||||
documentation:
|
||||
name: "Build Documentation"
|
||||
name: "Build & Deploy Documentation"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
cache: "pipenv"
|
||||
@@ -58,7 +64,7 @@ jobs:
|
||||
-
|
||||
name: Install pipenv
|
||||
run: |
|
||||
pip install --user pipenv==${DEFAULT_PIP_ENV_VERSION}
|
||||
pip install --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }}
|
||||
-
|
||||
name: Install dependencies
|
||||
run: |
|
||||
@@ -71,63 +77,44 @@ jobs:
|
||||
name: Make documentation
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs build --config-file ./mkdocs.yml
|
||||
-
|
||||
name: Deploy documentation
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME"
|
||||
git config --global user.name "${{ github.actor }}"
|
||||
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs gh-deploy --force --no-history
|
||||
-
|
||||
name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: documentation
|
||||
path: site/
|
||||
|
||||
documentation-deploy:
|
||||
name: "Deploy Documentation"
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs:
|
||||
- documentation
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CUSTOM_DOMAIN: docs.paperless-ngx.com
|
||||
CONFIG_FILE: mkdocs.yml
|
||||
EXTRA_PACKAGES: build-base
|
||||
retention-days: 7
|
||||
|
||||
tests-backend:
|
||||
name: "Tests (${{ matrix.python-version }})"
|
||||
name: "Backend Tests (Python ${{ matrix.python-version }})"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.8', '3.9', '3.10']
|
||||
python-version: ['3.9', '3.10', '3.11']
|
||||
fail-fast: false
|
||||
env:
|
||||
# Enable Tika end to end testing
|
||||
TIKA_LIVE: 1
|
||||
# Enable paperless_mail testing against real server
|
||||
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
||||
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
|
||||
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
|
||||
# Enable Gotenberg end to end testing
|
||||
GOTENBERG_LIVE: 1
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Start containers
|
||||
run: |
|
||||
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml pull --quiet
|
||||
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml up --detach
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml up --detach
|
||||
-
|
||||
name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
cache: "pipenv"
|
||||
@@ -135,7 +122,7 @@ jobs:
|
||||
-
|
||||
name: Install pipenv
|
||||
run: |
|
||||
pip install --user pipenv==${DEFAULT_PIP_ENV_VERSION}
|
||||
pip install --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }}
|
||||
-
|
||||
name: Install system dependencies
|
||||
run: |
|
||||
@@ -156,131 +143,184 @@ jobs:
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run pip list
|
||||
-
|
||||
name: Tests
|
||||
env:
|
||||
PAPERLESS_CI_TEST: 1
|
||||
# Enable paperless_mail testing against real server
|
||||
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
||||
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
|
||||
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
|
||||
run: |
|
||||
cd src/
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run pytest -ra
|
||||
-
|
||||
name: Upload coverage to Codecov
|
||||
name: Upload coverage
|
||||
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }}
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: backend-coverage-report
|
||||
path: src/coverage.xml
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
-
|
||||
name: Stop containers
|
||||
if: always()
|
||||
run: |
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml logs
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
|
||||
|
||||
install-frontend-depedendencies:
|
||||
name: "Install Frontend Dependencies"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
-
|
||||
name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
~/.cache
|
||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
|
||||
-
|
||||
name: Install dependencies
|
||||
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
|
||||
run: cd src-ui && npm ci
|
||||
-
|
||||
name: Install Playwright
|
||||
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
|
||||
run: cd src-ui && npx playwright install --with-deps
|
||||
|
||||
tests-frontend:
|
||||
name: "Frontend Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- install-frontend-depedendencies
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
shard-index: [1, 2, 3, 4]
|
||||
shard-count: [4]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
-
|
||||
name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
~/.cache
|
||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
|
||||
- name: Re-link Angular cli
|
||||
run: cd src-ui && npm link @angular/cli
|
||||
-
|
||||
name: Linting checks
|
||||
run: cd src-ui && npm run lint
|
||||
-
|
||||
name: Run Jest unit tests
|
||||
run: cd src-ui && npm run test -- --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||
-
|
||||
name: Upload Jest coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jest-coverage-report-${{ matrix.shard-index }}
|
||||
path: |
|
||||
src-ui/coverage/coverage-final.json
|
||||
src-ui/coverage/lcov.info
|
||||
src-ui/coverage/clover.xml
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
-
|
||||
name: Run Playwright e2e tests
|
||||
run: cd src-ui && npx playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||
-
|
||||
name: Upload Playwright test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-${{ matrix.shard-index }}
|
||||
path: src-ui/playwright-report
|
||||
retention-days: 7
|
||||
|
||||
tests-coverage-upload:
|
||||
name: "Upload Coverage"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- tests-backend
|
||||
- tests-frontend
|
||||
steps:
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Download frontend jest coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: src-ui/coverage/
|
||||
pattern: jest-coverage-report-*
|
||||
-
|
||||
name: Download frontend playwright coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: src-ui/coverage/
|
||||
pattern: playwright-report-*
|
||||
merge-multiple: true
|
||||
-
|
||||
name: Upload frontend coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: frontend
|
||||
directory: src-ui/coverage/
|
||||
# dont include backend coverage files here
|
||||
files: '!coverage.xml'
|
||||
-
|
||||
name: Download backend coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: backend-coverage-report
|
||||
path: src/
|
||||
-
|
||||
name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# future expansion
|
||||
flags: backend
|
||||
-
|
||||
name: Stop containers
|
||||
if: always()
|
||||
run: |
|
||||
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml logs
|
||||
docker compose --file ${GITHUB_WORKSPACE}/docker/compose/docker-compose.ci-test.yml down
|
||||
directory: src/
|
||||
|
||||
tests-frontend:
|
||||
name: "Tests Frontend"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
-
|
||||
name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- run: cd src-ui && npm ci
|
||||
- run: cd src-ui && npm run lint
|
||||
- run: cd src-ui && npm run test
|
||||
- run: cd src-ui && npm run e2e:ci
|
||||
|
||||
prepare-docker-build:
|
||||
name: Prepare Docker Pipeline Data
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- documentation
|
||||
- tests-backend
|
||||
- tests-frontend
|
||||
steps:
|
||||
-
|
||||
name: Set ghcr repository name
|
||||
id: set-ghcr-repository
|
||||
run: |
|
||||
ghcr_name=$(echo "${GITHUB_REPOSITORY}" | awk '{ print tolower($0) }')
|
||||
echo "repository=${ghcr_name}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
-
|
||||
name: Setup qpdf image
|
||||
id: qpdf-setup
|
||||
run: |
|
||||
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py qpdf)
|
||||
|
||||
echo ${build_json}
|
||||
|
||||
echo "qpdf-json=${build_json}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Setup psycopg2 image
|
||||
id: psycopg2-setup
|
||||
run: |
|
||||
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py psycopg2)
|
||||
|
||||
echo ${build_json}
|
||||
|
||||
echo "psycopg2-json=${build_json}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Setup pikepdf image
|
||||
id: pikepdf-setup
|
||||
run: |
|
||||
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py pikepdf)
|
||||
|
||||
echo ${build_json}
|
||||
|
||||
echo "pikepdf-json=${build_json}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Setup jbig2enc image
|
||||
id: jbig2enc-setup
|
||||
run: |
|
||||
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py jbig2enc)
|
||||
|
||||
echo ${build_json}
|
||||
|
||||
echo "jbig2enc-json=${build_json}" >> $GITHUB_OUTPUT
|
||||
|
||||
outputs:
|
||||
|
||||
ghcr-repository: ${{ steps.set-ghcr-repository.outputs.repository }}
|
||||
|
||||
qpdf-json: ${{ steps.qpdf-setup.outputs.qpdf-json }}
|
||||
|
||||
pikepdf-json: ${{ steps.pikepdf-setup.outputs.pikepdf-json }}
|
||||
|
||||
psycopg2-json: ${{ steps.psycopg2-setup.outputs.psycopg2-json }}
|
||||
|
||||
jbig2enc-json: ${{ steps.jbig2enc-setup.outputs.jbig2enc-json}}
|
||||
|
||||
# build and push image to docker hub.
|
||||
build-docker-image:
|
||||
name: Build Docker image for ${{ github.ref_name }}
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
needs:
|
||||
- prepare-docker-build
|
||||
- tests-backend
|
||||
- tests-frontend
|
||||
steps:
|
||||
-
|
||||
name: Check pushing to Docker Hub
|
||||
id: docker-hub
|
||||
id: push-other-places
|
||||
# Only push to Dockerhub from the main repo AND the ref is either:
|
||||
# main
|
||||
# dev
|
||||
@@ -288,22 +328,29 @@ jobs:
|
||||
# a tag
|
||||
# Otherwise forks would require a Docker Hub account and secrets setup
|
||||
run: |
|
||||
if [[ ${{ needs.prepare-docker-build.outputs.ghcr-repository }} == "paperless-ngx/paperless-ngx" && ( ${{ github.ref_name }} == "main" || ${{ github.ref_name }} == "dev" || ${{ github.ref_name }} == "beta" || ${{ startsWith(github.ref, 'refs/tags/v') }} == "true" ) ]] ; then
|
||||
if [[ ${{ github.repository_owner }} == "paperless-ngx" && ( ${{ github.ref_name }} == "dev" || ${{ github.ref_name }} == "beta" || ${{ startsWith(github.ref, 'refs/tags/v') }} == "true" ) ]] ; then
|
||||
echo "Enabling DockerHub image push"
|
||||
echo "enable=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Not pushing to DockerHub"
|
||||
echo "enable=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
-
|
||||
name: Set ghcr repository name
|
||||
id: set-ghcr-repository
|
||||
run: |
|
||||
ghcr_name=$(echo "${{ github.repository }}" | awk '{ print tolower($0) }')
|
||||
echo "Name is ${ghcr_name}"
|
||||
echo "ghcr-repository=${ghcr_name}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Gather Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}
|
||||
name=paperlessngx/paperless-ngx,enable=${{ steps.docker-hub.outputs.enable }}
|
||||
name=quay.io/paperlessngx/paperless-ngx,enable=${{ steps.docker-hub.outputs.enable }}
|
||||
ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}
|
||||
name=paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }}
|
||||
name=quay.io/paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }}
|
||||
tags: |
|
||||
# Tag branches with branch name
|
||||
type=ref,event=branch
|
||||
@@ -313,60 +360,61 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
# If https://github.com/docker/buildx/issues/1044 is resolved,
|
||||
# the append input with a native arm64 arch could be used to
|
||||
# significantly speed up building
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm64
|
||||
-
|
||||
name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2
|
||||
name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
# Don't attempt to login is not pushing to Docker Hub
|
||||
if: steps.docker-hub.outputs.enable == 'true'
|
||||
if: steps.push-other-places.outputs.enable == 'true'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Login to Quay.io
|
||||
uses: docker/login-action@v2
|
||||
# Don't attempt to login is not pushing to Docker Hub
|
||||
if: steps.docker-hub.outputs.enable == 'true'
|
||||
uses: docker/login-action@v3
|
||||
# Don't attempt to login is not pushing to Quay.io
|
||||
if: steps.push-other-places.outputs.enable == 'true'
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||
build-args: |
|
||||
JBIG2ENC_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).version }}
|
||||
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
|
||||
PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }}
|
||||
PSYCOPG2_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }}
|
||||
# Get cache layers from this branch, then dev, then main
|
||||
PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }}
|
||||
# Get cache layers from this branch, then dev
|
||||
# This allows new branches to get at least some cache benefits, generally from dev
|
||||
cache-from: |
|
||||
type=registry,ref=ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
|
||||
type=registry,ref=ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}/builder/cache/app:dev
|
||||
type=registry,ref=ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}/builder/cache/app:main
|
||||
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
|
||||
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:dev
|
||||
cache-to: |
|
||||
type=registry,mode=max,ref=ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
|
||||
type=registry,mode=max,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
|
||||
-
|
||||
name: Inspect image
|
||||
run: |
|
||||
@@ -378,23 +426,26 @@ jobs:
|
||||
docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/
|
||||
-
|
||||
name: Upload frontend artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-compiled
|
||||
path: src/documents/static/frontend/
|
||||
retention-days: 7
|
||||
|
||||
build-release:
|
||||
name: "Build Release"
|
||||
needs:
|
||||
- build-docker-image
|
||||
- documentation
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
cache: "pipenv"
|
||||
@@ -402,11 +453,17 @@ jobs:
|
||||
-
|
||||
name: Install pipenv + tools
|
||||
run: |
|
||||
pip install --upgrade --user pipenv==${DEFAULT_PIP_ENV_VERSION} setuptools wheel
|
||||
pip install --upgrade --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }} setuptools wheel
|
||||
-
|
||||
name: Install Python dependencies
|
||||
run: |
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} sync --dev
|
||||
-
|
||||
name: Patch whitenoise
|
||||
run: |
|
||||
curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch
|
||||
patch -d $(pipenv --venv)/lib/python3.10/site-packages --verbose -p2 < 484.patch
|
||||
rm 484.patch
|
||||
-
|
||||
name: Install system dependencies
|
||||
run: |
|
||||
@@ -414,13 +471,13 @@ jobs:
|
||||
sudo apt-get install -qq --no-install-recommends gettext liblept5
|
||||
-
|
||||
name: Download frontend artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-compiled
|
||||
path: src/documents/static/frontend/
|
||||
-
|
||||
name: Download documentation artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: documentation
|
||||
path: docs/_build/html/
|
||||
@@ -486,12 +543,14 @@ jobs:
|
||||
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
||||
-
|
||||
name: Upload release artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release
|
||||
path: dist/paperless-ngx.tar.xz
|
||||
retention-days: 7
|
||||
|
||||
publish-release:
|
||||
name: "Publish Release"
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
||||
@@ -503,7 +562,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Download release artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release
|
||||
path: ./
|
||||
@@ -520,7 +579,7 @@ jobs:
|
||||
-
|
||||
name: Create Release and Changelog
|
||||
id: create-release
|
||||
uses: release-drafter/release-drafter@v5
|
||||
uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
name: Paperless-ngx ${{ steps.get_version.outputs.version }}
|
||||
tag: ${{ steps.get_version.outputs.version }}
|
||||
@@ -541,6 +600,7 @@ jobs:
|
||||
asset_content_type: application/x-xz
|
||||
|
||||
append-changelog:
|
||||
name: "Append Changelog"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- publish-release
|
||||
@@ -548,12 +608,12 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
cache: "pipenv"
|
||||
@@ -561,7 +621,7 @@ jobs:
|
||||
-
|
||||
name: Install pipenv + tools
|
||||
run: |
|
||||
pip install --upgrade --user pipenv==${DEFAULT_PIP_ENV_VERSION} setuptools wheel
|
||||
pip install --upgrade --user pipenv==${{ env.DEFAULT_PIP_ENV_VERSION }} setuptools wheel
|
||||
-
|
||||
name: Append Changelog to docs
|
||||
id: append-Changelog
|
||||
@@ -582,12 +642,12 @@ jobs:
|
||||
git push origin ${{ needs.publish-release.outputs.version }}-changelog
|
||||
-
|
||||
name: Create Pull Request
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
const result = await github.rest.pulls.create({
|
||||
title: '[Documentation] Add ${{ needs.publish-release.outputs.version }} changelog',
|
||||
title: 'Documentation: Add ${{ needs.publish-release.outputs.version }} changelog',
|
||||
owner,
|
||||
repo,
|
||||
head: '${{ needs.publish-release.outputs.version }}-changelog',
|
||||
|
85
.github/workflows/cleanup-tags.yml
vendored
@@ -12,9 +12,6 @@ on:
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/cleanup-tags.yml"
|
||||
- ".github/scripts/cleanup-tags.py"
|
||||
- ".github/scripts/github.py"
|
||||
- ".github/scripts/common.py"
|
||||
|
||||
concurrency:
|
||||
group: registry-tags-cleanup
|
||||
@@ -26,58 +23,48 @@ jobs:
|
||||
if: github.repository_owner == 'paperless-ngx'
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- primary-name: "paperless-ngx"
|
||||
cache-name: "paperless-ngx/builder/cache/app"
|
||||
|
||||
- primary-name: "paperless-ngx/builder/qpdf"
|
||||
cache-name: "paperless-ngx/builder/cache/qpdf"
|
||||
|
||||
- primary-name: "paperless-ngx/builder/pikepdf"
|
||||
cache-name: "paperless-ngx/builder/cache/pikepdf"
|
||||
|
||||
- primary-name: "paperless-ngx/builder/jbig2enc"
|
||||
cache-name: "paperless-ngx/builder/cache/jbig2enc"
|
||||
|
||||
- primary-name: "paperless-ngx/builder/psycopg2"
|
||||
cache-name: "paperless-ngx/builder/cache/psycopg2"
|
||||
primary-name: ["paperless-ngx", "paperless-ngx/builder/cache/app"]
|
||||
env:
|
||||
# Requires a personal access token with the OAuth scope delete:packages
|
||||
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
-
|
||||
name: Install Python libraries
|
||||
run: |
|
||||
python -m pip install httpx docker
|
||||
#
|
||||
# Clean up primary package
|
||||
#
|
||||
-
|
||||
name: Cleanup for package "${{ matrix.primary-name }}"
|
||||
name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
run: |
|
||||
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --untagged --is-manifest --delete "${{ matrix.primary-name }}"
|
||||
#
|
||||
# Clean up registry cache package
|
||||
#
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
is_org: "true"
|
||||
package_name: "${{ matrix.primary-name }}"
|
||||
scheme: "branch"
|
||||
repo_name: "paperless-ngx"
|
||||
match_regex: "(feature|fix)"
|
||||
do_delete: "true"
|
||||
|
||||
cleanup-untagged-images:
|
||||
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
||||
if: github.repository_owner == 'paperless-ngx'
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- cleanup-images
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
primary-name: ["paperless-ngx", "paperless-ngx/builder/cache/app"]
|
||||
env:
|
||||
# Requires a personal access token with the OAuth scope delete:packages
|
||||
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
|
||||
steps:
|
||||
-
|
||||
name: Cleanup for package "${{ matrix.cache-name }}"
|
||||
name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
run: |
|
||||
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --untagged --delete "${{ matrix.cache-name }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.8.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
is_org: "true"
|
||||
package_name: "${{ matrix.primary-name }}"
|
||||
do_delete: "true"
|
||||
|
6
.github/workflows/codeql-analysis.yml
vendored
@@ -38,11 +38,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -51,4 +51,4 @@ jobs:
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
34
.github/workflows/crowdin.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Crowdin Action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '2 */12 * * *'
|
||||
push:
|
||||
paths: [
|
||||
'src/locale/**',
|
||||
'src-ui/messages.xlf',
|
||||
'src-ui/src/locale/**'
|
||||
]
|
||||
branches: [ dev ]
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
name: Crowdin Sync
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
crowdin_branch_name: 'dev'
|
||||
localization_branch_name: l10n_dev
|
||||
pull_request_labels: 'skip-changelog, translation'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
310
.github/workflows/installer-library.yml
vendored
@@ -1,310 +0,0 @@
|
||||
# This workflow will run to update the installer library of
|
||||
# Docker images. These are the images which provide updated wheels
|
||||
# .deb installation packages or maybe just some compiled library
|
||||
|
||||
name: Build Image Library
|
||||
|
||||
on:
|
||||
push:
|
||||
# Must match one of these branches AND one of the paths
|
||||
# to be triggered
|
||||
branches:
|
||||
- "main"
|
||||
- "dev"
|
||||
- "library-*"
|
||||
- "feature-*"
|
||||
paths:
|
||||
# Trigger the workflow if a Dockerfile changed
|
||||
- "docker-builders/**"
|
||||
# Trigger if a package was updated
|
||||
- ".build-config.json"
|
||||
- "Pipfile.lock"
|
||||
# Also trigger on workflow changes related to the library
|
||||
- ".github/workflows/installer-library.yml"
|
||||
- ".github/workflows/reusable-workflow-builder.yml"
|
||||
- ".github/scripts/**"
|
||||
|
||||
# Set a workflow level concurrency group so primary workflow
|
||||
# can wait for this to complete if needed
|
||||
# DO NOT CHANGE without updating main workflow group
|
||||
concurrency:
|
||||
group: build-installer-library
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
prepare-docker-build:
|
||||
name: Prepare Docker Image Version Data
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Set ghcr repository name
|
||||
id: set-ghcr-repository
|
||||
run: |
|
||||
ghcr_name=$(echo "${GITHUB_REPOSITORY}" | awk '{ print tolower($0) }')
|
||||
echo "repository=${ghcr_name}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
-
|
||||
name: Install jq
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq
|
||||
-
|
||||
name: Setup qpdf image
|
||||
id: qpdf-setup
|
||||
run: |
|
||||
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py qpdf)
|
||||
|
||||
echo ${build_json}
|
||||
|
||||
echo "qpdf-json=${build_json}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Setup psycopg2 image
|
||||
id: psycopg2-setup
|
||||
run: |
|
||||
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py psycopg2)
|
||||
|
||||
echo ${build_json}
|
||||
|
||||
echo "psycopg2-json=${build_json}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Setup pikepdf image
|
||||
id: pikepdf-setup
|
||||
run: |
|
||||
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py pikepdf)
|
||||
|
||||
echo ${build_json}
|
||||
|
||||
echo "pikepdf-json=${build_json}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Setup jbig2enc image
|
||||
id: jbig2enc-setup
|
||||
run: |
|
||||
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py jbig2enc)
|
||||
|
||||
echo ${build_json}
|
||||
|
||||
echo "jbig2enc-json=${build_json}" >> $GITHUB_OUTPUT
|
||||
-
|
||||
name: Setup other versions
|
||||
id: cache-bust-setup
|
||||
run: |
|
||||
pillow_version=$(jq -r '.default.pillow.version | gsub("=";"")' Pipfile.lock)
|
||||
lxml_version=$(jq -r '.default.lxml.version | gsub("=";"")' Pipfile.lock)
|
||||
|
||||
echo "Pillow is ${pillow_version}"
|
||||
echo "lxml is ${lxml_version}"
|
||||
|
||||
echo "pillow-version=${pillow_version}" >> $GITHUB_OUTPUT
|
||||
echo "lxml-version=${lxml_version}" >> $GITHUB_OUTPUT
|
||||
|
||||
outputs:
|
||||
|
||||
ghcr-repository: ${{ steps.set-ghcr-repository.outputs.repository }}
|
||||
|
||||
qpdf-json: ${{ steps.qpdf-setup.outputs.qpdf-json }}
|
||||
|
||||
pikepdf-json: ${{ steps.pikepdf-setup.outputs.pikepdf-json }}
|
||||
|
||||
psycopg2-json: ${{ steps.psycopg2-setup.outputs.psycopg2-json }}
|
||||
|
||||
jbig2enc-json: ${{ steps.jbig2enc-setup.outputs.jbig2enc-json }}
|
||||
|
||||
pillow-version: ${{ steps.cache-bust-setup.outputs.pillow-version }}
|
||||
|
||||
lxml-version: ${{ steps.cache-bust-setup.outputs.lxml-version }}
|
||||
|
||||
build-qpdf-debs:
|
||||
name: qpdf
|
||||
needs:
|
||||
- prepare-docker-build
|
||||
uses: ./.github/workflows/reusable-workflow-builder.yml
|
||||
with:
|
||||
dockerfile: ./docker-builders/Dockerfile.qpdf
|
||||
build-platforms: linux/amd64
|
||||
build-json: ${{ needs.prepare-docker-build.outputs.qpdf-json }}
|
||||
build-args: |
|
||||
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
|
||||
|
||||
build-jbig2enc:
|
||||
name: jbig2enc
|
||||
needs:
|
||||
- prepare-docker-build
|
||||
uses: ./.github/workflows/reusable-workflow-builder.yml
|
||||
with:
|
||||
dockerfile: ./docker-builders/Dockerfile.jbig2enc
|
||||
build-json: ${{ needs.prepare-docker-build.outputs.jbig2enc-json }}
|
||||
build-args: |
|
||||
JBIG2ENC_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).version }}
|
||||
|
||||
build-psycopg2-wheel:
|
||||
name: psycopg2
|
||||
needs:
|
||||
- prepare-docker-build
|
||||
uses: ./.github/workflows/reusable-workflow-builder.yml
|
||||
with:
|
||||
dockerfile: ./docker-builders/Dockerfile.psycopg2
|
||||
build-json: ${{ needs.prepare-docker-build.outputs.psycopg2-json }}
|
||||
build-args: |
|
||||
PSYCOPG2_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }}
|
||||
|
||||
build-pikepdf-wheel:
|
||||
name: pikepdf
|
||||
needs:
|
||||
- prepare-docker-build
|
||||
- build-qpdf-debs
|
||||
uses: ./.github/workflows/reusable-workflow-builder.yml
|
||||
with:
|
||||
dockerfile: ./docker-builders/Dockerfile.pikepdf
|
||||
build-json: ${{ needs.prepare-docker-build.outputs.pikepdf-json }}
|
||||
build-args: |
|
||||
REPO=${{ needs.prepare-docker-build.outputs.ghcr-repository }}
|
||||
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
|
||||
PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }}
|
||||
PILLOW_VERSION=${{ needs.prepare-docker-build.outputs.pillow-version }}
|
||||
LXML_VERSION=${{ needs.prepare-docker-build.outputs.lxml-version }}
|
||||
|
||||
commit-binary-files:
|
||||
name: Store installers
|
||||
needs:
|
||||
- prepare-docker-build
|
||||
- build-qpdf-debs
|
||||
- build-jbig2enc
|
||||
- build-psycopg2-wheel
|
||||
- build-pikepdf-wheel
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: binary-library
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
-
|
||||
name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -qq --no-install-recommends tree
|
||||
-
|
||||
name: Extract qpdf files
|
||||
run: |
|
||||
version=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
|
||||
tag=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).image_tag }}
|
||||
|
||||
docker pull --quiet ${tag}
|
||||
docker create --name qpdf-extract ${tag}
|
||||
|
||||
mkdir --parents qpdf/${version}/amd64
|
||||
docker cp qpdf-extract:/usr/src/qpdf/${version}/amd64 qpdf/${version}
|
||||
|
||||
mkdir --parents qpdf/${version}/arm64
|
||||
docker cp qpdf-extract:/usr/src/qpdf/${version}/arm64 qpdf/${version}
|
||||
|
||||
mkdir --parents qpdf/${version}/armv7
|
||||
docker cp qpdf-extract:/usr/src/qpdf/${version}/armv7 qpdf/${version}
|
||||
-
|
||||
name: Extract psycopg2 files
|
||||
run: |
|
||||
version=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }}
|
||||
tag=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).image_tag }}
|
||||
|
||||
docker pull --quiet --platform linux/amd64 ${tag}
|
||||
docker create --platform linux/amd64 --name psycopg2-extract ${tag}
|
||||
mkdir --parents psycopg2/${version}/amd64
|
||||
docker cp psycopg2-extract:/usr/src/wheels/ psycopg2/${version}/amd64
|
||||
mv psycopg2/${version}/amd64/wheels/* psycopg2/${version}/amd64
|
||||
rm -r psycopg2/${version}/amd64/wheels/
|
||||
docker rm psycopg2-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm64 ${tag}
|
||||
docker create --platform linux/arm64 --name psycopg2-extract ${tag}
|
||||
mkdir --parents psycopg2/${version}/arm64
|
||||
docker cp psycopg2-extract:/usr/src/wheels/ psycopg2/${version}/arm64
|
||||
mv psycopg2/${version}/arm64/wheels/* psycopg2/${version}/arm64
|
||||
rm -r psycopg2/${version}/arm64/wheels/
|
||||
docker rm psycopg2-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm/v7 ${tag}
|
||||
docker create --platform linux/arm/v7 --name psycopg2-extract ${tag}
|
||||
mkdir --parents psycopg2/${version}/armv7
|
||||
docker cp psycopg2-extract:/usr/src/wheels/ psycopg2/${version}/armv7
|
||||
mv psycopg2/${version}/armv7/wheels/* psycopg2/${version}/armv7
|
||||
rm -r psycopg2/${version}/armv7/wheels/
|
||||
docker rm psycopg2-extract
|
||||
-
|
||||
name: Extract pikepdf files
|
||||
run: |
|
||||
version=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }}
|
||||
tag=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).image_tag }}
|
||||
|
||||
docker pull --quiet --platform linux/amd64 ${tag}
|
||||
docker create --platform linux/amd64 --name pikepdf-extract ${tag}
|
||||
mkdir --parents pikepdf/${version}/amd64
|
||||
docker cp pikepdf-extract:/usr/src/wheels/ pikepdf/${version}/amd64
|
||||
mv pikepdf/${version}/amd64/wheels/* pikepdf/${version}/amd64
|
||||
rm -r pikepdf/${version}/amd64/wheels/
|
||||
docker rm pikepdf-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm64 ${tag}
|
||||
docker create --platform linux/arm64 --name pikepdf-extract ${tag}
|
||||
mkdir --parents pikepdf/${version}/arm64
|
||||
docker cp pikepdf-extract:/usr/src/wheels/ pikepdf/${version}/arm64
|
||||
mv pikepdf/${version}/arm64/wheels/* pikepdf/${version}/arm64
|
||||
rm -r pikepdf/${version}/arm64/wheels/
|
||||
docker rm pikepdf-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm/v7 ${tag}
|
||||
docker create --platform linux/arm/v7 --name pikepdf-extract ${tag}
|
||||
mkdir --parents pikepdf/${version}/armv7
|
||||
docker cp pikepdf-extract:/usr/src/wheels/ pikepdf/${version}/armv7
|
||||
mv pikepdf/${version}/armv7/wheels/* pikepdf/${version}/armv7
|
||||
rm -r pikepdf/${version}/armv7/wheels/
|
||||
docker rm pikepdf-extract
|
||||
-
|
||||
name: Extract jbig2enc files
|
||||
run: |
|
||||
version=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).version }}
|
||||
tag=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).image_tag }}
|
||||
|
||||
docker pull --quiet --platform linux/amd64 ${tag}
|
||||
docker create --platform linux/amd64 --name jbig2enc-extract ${tag}
|
||||
mkdir --parents jbig2enc/${version}/amd64
|
||||
docker cp jbig2enc-extract:/usr/src/jbig2enc/build jbig2enc/${version}/amd64/
|
||||
mv jbig2enc/${version}/amd64/build/* jbig2enc/${version}/amd64/
|
||||
docker rm jbig2enc-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm64 ${tag}
|
||||
docker create --platform linux/arm64 --name jbig2enc-extract ${tag}
|
||||
mkdir --parents jbig2enc/${version}/arm64
|
||||
docker cp jbig2enc-extract:/usr/src/jbig2enc/build jbig2enc/${version}/arm64
|
||||
mv jbig2enc/${version}/arm64/build/* jbig2enc/${version}/arm64/
|
||||
docker rm jbig2enc-extract
|
||||
|
||||
docker pull --quiet --platform linux/arm/v7 ${tag}
|
||||
docker create --platform linux/arm/v7 --name jbig2enc-extract ${tag}
|
||||
mkdir --parents jbig2enc/${version}/armv7
|
||||
docker cp jbig2enc-extract:/usr/src/jbig2enc/build jbig2enc/${version}/armv7
|
||||
mv jbig2enc/${version}/armv7/build/* jbig2enc/${version}/armv7/
|
||||
docker rm jbig2enc-extract
|
||||
-
|
||||
name: Show file structure
|
||||
run: |
|
||||
tree .
|
||||
-
|
||||
name: Commit files
|
||||
run: |
|
||||
git config --global user.name "github-actions"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add pikepdf/ qpdf/ psycopg2/ jbig2enc/
|
||||
git commit -m "Updating installer packages" || true
|
||||
git push origin || true
|
32
.github/workflows/project-actions.yml
vendored
@@ -1,10 +1,6 @@
|
||||
name: Project Automations
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
pull_request_target: #_target allows access to secrets
|
||||
types:
|
||||
- opened
|
||||
@@ -16,25 +12,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
todo: Todo
|
||||
done: Done
|
||||
in_progress: In Progress
|
||||
|
||||
jobs:
|
||||
issue_opened_or_reopened:
|
||||
name: issue_opened_or_reopened
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
||||
steps:
|
||||
- name: Add issue to project and set status to ${{ env.todo }}
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.1.0
|
||||
with:
|
||||
gh_token: ${{ secrets.GH_TOKEN }}
|
||||
organization: paperless-ngx
|
||||
project_id: 2
|
||||
resource_node_id: ${{ github.event.issue.node_id }}
|
||||
status_value: ${{ env.todo }} # Target status
|
||||
pr_opened_or_reopened:
|
||||
name: pr_opened_or_reopened
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -43,15 +21,7 @@ jobs:
|
||||
pull-requests: write
|
||||
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
||||
steps:
|
||||
- name: Add PR to project and set status to "Needs Review"
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.1.0
|
||||
with:
|
||||
gh_token: ${{ secrets.GH_TOKEN }}
|
||||
organization: paperless-ngx
|
||||
project_id: 2
|
||||
resource_node_id: ${{ github.event.pull_request.node_id }}
|
||||
status_value: "Needs Review" # Target status
|
||||
- name: Label PR with release-drafter
|
||||
uses: release-drafter/release-drafter@v5
|
||||
uses: release-drafter/release-drafter@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
281
.github/workflows/repo-maintenance.yml
vendored
Normal file
@@ -0,0 +1,281 @@
|
||||
name: 'Repository Maintenance'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
name: 'Stale'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
days-before-stale: 7
|
||||
days-before-close: 14
|
||||
any-of-labels: 'stale,cant-reproduce,not a bug'
|
||||
stale-issue-label: stale
|
||||
stale-pr-label: stale
|
||||
stale-issue-message: >
|
||||
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. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
|
||||
lock-threads:
|
||||
name: 'Lock Old Threads'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
issue-inactive-days: '30'
|
||||
pr-inactive-days: '30'
|
||||
discussion-inactive-days: '30'
|
||||
log-output: true
|
||||
issue-comment: >
|
||||
This issue has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new discussion or issue for related concerns.
|
||||
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
|
||||
pr-comment: >
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new discussion or issue for related concerns.
|
||||
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
|
||||
discussion-comment: >
|
||||
This discussion has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new discussion for related concerns.
|
||||
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
|
||||
close-answered-discussions:
|
||||
name: 'Close Answered Discussions'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const query = `query($owner:String!, $name:String!) {
|
||||
repository(owner:$owner, name:$name){
|
||||
discussions(first:100, answered:true, states:[OPEN]) {
|
||||
nodes {
|
||||
id,
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
const variables = {
|
||||
owner: context.repo.owner,
|
||||
name: context.repo.repo,
|
||||
}
|
||||
const result = await github.graphql(query, variables)
|
||||
|
||||
console.log(`Found ${result.repository.discussions.nodes.length} open answered discussions`)
|
||||
|
||||
for (const discussion of result.repository.discussions.nodes) {
|
||||
console.log(`Closing discussion #${discussion.number} (${discussion.id})`)
|
||||
|
||||
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
|
||||
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
const commentVariables = {
|
||||
discussion: discussion.id,
|
||||
body: 'This discussion has been automatically closed because it was marked as answered. Please see our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.',
|
||||
}
|
||||
await github.graphql(addCommentMutation, commentVariables)
|
||||
|
||||
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) {
|
||||
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
const closeVariables = {
|
||||
discussion: discussion.id,
|
||||
reason: "RESOLVED",
|
||||
}
|
||||
await github.graphql(closeDiscussionMutation, closeVariables)
|
||||
|
||||
await sleep(1000)
|
||||
}
|
||||
close-outdated-discussions:
|
||||
name: 'Close Outdated Discussions'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const CUTOFF_DAYS = 180;
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - CUTOFF_DAYS);
|
||||
|
||||
const query = `query(
|
||||
$owner:String!,
|
||||
$name:String!,
|
||||
$supportCategory:ID!,
|
||||
$generalCategory:ID!,
|
||||
) {
|
||||
supportDiscussions: repository(owner:$owner, name:$name){
|
||||
discussions(
|
||||
categoryId:$supportCategory,
|
||||
last:50,
|
||||
answered:false,
|
||||
states:[OPEN],
|
||||
) {
|
||||
nodes {
|
||||
id,
|
||||
number,
|
||||
updatedAt
|
||||
}
|
||||
},
|
||||
},
|
||||
generalDiscussions: repository(owner:$owner, name:$name){
|
||||
discussions(
|
||||
categoryId:$generalCategory,
|
||||
last:50,
|
||||
states:[OPEN],
|
||||
) {
|
||||
nodes {
|
||||
id,
|
||||
number,
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
const variables = {
|
||||
owner: context.repo.owner,
|
||||
name: context.repo.repo,
|
||||
supportCategory: "DIC_kwDOG1Zs184CBKWK",
|
||||
generalCategory: "DIC_kwDOG1Zs184CBKWJ"
|
||||
}
|
||||
const result = await github.graphql(query, variables);
|
||||
const combinedDiscussions = [
|
||||
...result.supportDiscussions.discussions.nodes,
|
||||
...result.generalDiscussions.discussions.nodes,
|
||||
]
|
||||
|
||||
console.log(`Checking ${combinedDiscussions.length} open discussions`);
|
||||
|
||||
for (const discussion of combinedDiscussions) {
|
||||
if (new Date(discussion.updatedAt) < cutoff) {
|
||||
console.log(`Closing outdated discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt}`);
|
||||
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
|
||||
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
const commentVariables = {
|
||||
discussion: discussion.id,
|
||||
body: 'This discussion has been automatically closed due to inactivity. Please see our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.',
|
||||
}
|
||||
await github.graphql(addCommentMutation, commentVariables);
|
||||
|
||||
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) {
|
||||
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
const closeVariables = {
|
||||
discussion: discussion.id,
|
||||
reason: "OUTDATED",
|
||||
}
|
||||
await github.graphql(closeDiscussionMutation, closeVariables);
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
close-unsupported-feature-requests:
|
||||
name: 'Close Unsupported Feature Requests'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const CUTOFF_1_DAYS = 180;
|
||||
const CUTOFF_1_COUNT = 5;
|
||||
const CUTOFF_2_DAYS = 365;
|
||||
const CUTOFF_2_COUNT = 10;
|
||||
|
||||
const cutoff1Date = new Date();
|
||||
cutoff1Date.setDate(cutoff1Date.getDate() - CUTOFF_1_DAYS);
|
||||
const cutoff2Date = new Date();
|
||||
cutoff2Date.setDate(cutoff2Date.getDate() - CUTOFF_2_DAYS);
|
||||
|
||||
const query = `query(
|
||||
$owner:String!,
|
||||
$name:String!,
|
||||
$featureRequestsCategory:ID!,
|
||||
) {
|
||||
repository(owner:$owner, name:$name){
|
||||
discussions(
|
||||
categoryId:$featureRequestsCategory,
|
||||
last:100,
|
||||
states:[OPEN],
|
||||
) {
|
||||
nodes {
|
||||
id,
|
||||
number,
|
||||
updatedAt,
|
||||
upvoteCount,
|
||||
}
|
||||
},
|
||||
}
|
||||
}`;
|
||||
const variables = {
|
||||
owner: context.repo.owner,
|
||||
name: context.repo.repo,
|
||||
featureRequestsCategory: "DIC_kwDOG1Zs184CBNr4"
|
||||
}
|
||||
const result = await github.graphql(query, variables);
|
||||
|
||||
for (const discussion of result.repository.discussions.nodes) {
|
||||
const discussionDate = new Date(discussion.updatedAt);
|
||||
if ((discussionDate < cutoff1Date && discussion.upvoteCount < CUTOFF_1_COUNT) ||
|
||||
(discussionDate < cutoff2Date && discussion.upvoteCount < CUTOFF_2_COUNT)) {
|
||||
console.log(`Closing discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt} with votes ${discussion.upvoteCount}`);
|
||||
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
|
||||
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
const commentVariables = {
|
||||
discussion: discussion.id,
|
||||
body: 'This discussion has been automatically closed due to lack of community support. Please see our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.',
|
||||
}
|
||||
await github.graphql(addCommentMutation, commentVariables);
|
||||
|
||||
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) {
|
||||
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
const closeVariables = {
|
||||
discussion: discussion.id,
|
||||
reason: "OUTDATED",
|
||||
}
|
||||
await github.graphql(closeDiscussionMutation, closeVariables);
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
57
.github/workflows/reusable-workflow-builder.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: Reusable Image Builder
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
dockerfile:
|
||||
required: true
|
||||
type: string
|
||||
build-json:
|
||||
required: true
|
||||
type: string
|
||||
build-args:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
build-platforms:
|
||||
required: false
|
||||
default: linux/amd64,linux/arm64,linux/arm/v7
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ fromJSON(inputs.build-json).name }}-${{ fromJSON(inputs.build-json).version }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
name: Build ${{ fromJSON(inputs.build-json).name }} @ ${{ fromJSON(inputs.build-json).version }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Build ${{ fromJSON(inputs.build-json).name }}
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
tags: ${{ fromJSON(inputs.build-json).image_tag }}
|
||||
platforms: ${{ inputs.build-platforms }}
|
||||
build-args: ${{ inputs.build-args }}
|
||||
push: true
|
||||
cache-from: type=registry,ref=${{ fromJSON(inputs.build-json).cache_tag }}
|
||||
cache-to: type=registry,mode=max,ref=${{ fromJSON(inputs.build-json).cache_tag }}
|
3
.gitignore
vendored
@@ -22,6 +22,7 @@ var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
/src/paperless_mail/templates/node_modules
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
@@ -65,6 +66,8 @@ target/
|
||||
.vscode
|
||||
/src-ui/.vscode
|
||||
/docs/.vscode
|
||||
.vscode-server
|
||||
*CommandMarker
|
||||
|
||||
# Other stuff that doesn't belong
|
||||
.virtualenv
|
||||
|
@@ -5,12 +5,14 @@
|
||||
repos:
|
||||
# General hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: check-json
|
||||
exclude: "tsconfig.*json"
|
||||
- id: check-yaml
|
||||
args:
|
||||
- "--unsafe"
|
||||
- id: check-toml
|
||||
- id: check-executables-have-shebangs
|
||||
- id: end-of-file-fixer
|
||||
@@ -26,8 +28,17 @@ repos:
|
||||
- svg
|
||||
- id: check-case-conflict
|
||||
- id: detect-private-key
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: "v2.7.1"
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
|
||||
exclude_types:
|
||||
- pofile
|
||||
- json
|
||||
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: 'v3.3.3'
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or:
|
||||
@@ -36,17 +47,14 @@ repos:
|
||||
- markdown
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.259'
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.6.4'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
- id: ruff-format
|
||||
# Dockerfile hooks
|
||||
- repo: https://github.com/AleksaC/hadolint-py
|
||||
rev: v2.10.0
|
||||
rev: v2.12.0.3
|
||||
hooks:
|
||||
- id: hadolint
|
||||
# Shell script hooks
|
||||
@@ -57,6 +65,6 @@ repos:
|
||||
args:
|
||||
- "--tab"
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: "v0.9.0.2"
|
||||
rev: "v0.10.0.1"
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
|
20
.prettierrc
@@ -1,4 +1,16 @@
|
||||
# https://prettier.io/docs/en/options.html#semicolons
|
||||
semi: false
|
||||
# https://prettier.io/docs/en/options.html#quotes
|
||||
singleQuote: true
|
||||
{
|
||||
# https://prettier.io/docs/en/options.html#semicolons
|
||||
"semi": false,
|
||||
# https://prettier.io/docs/en/options.html#quotes
|
||||
"singleQuote": true,
|
||||
# https://prettier.io/docs/en/options.html#trailing-commas
|
||||
"trailingComma": "es5",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["index.md", "administration.md"],
|
||||
"options": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
3.8.16
|
||||
3.9.19
|
||||
|
46
.ruff.toml
@@ -1,23 +1,47 @@
|
||||
# https://beta.ruff.rs/docs/settings/
|
||||
# https://beta.ruff.rs/docs/rules/
|
||||
select = ["F", "E", "W", "UP", "COM", "DJ", "EXE", "ISC", "ICN", "G201", "INP", "PIE", "RSE", "SIM", "TID", "PLC", "PLE", "RUF"]
|
||||
# TODO PTH
|
||||
ignore = ["DJ001", "SIM105"]
|
||||
fix = true
|
||||
line-length = 88
|
||||
respect-gitignore = true
|
||||
src = ["src"]
|
||||
target-version = "py38"
|
||||
format = "grouped"
|
||||
target-version = "py39"
|
||||
output-format = "grouped"
|
||||
show-fixes = true
|
||||
|
||||
[per-file-ignores]
|
||||
# https://docs.astral.sh/ruff/settings/
|
||||
# https://docs.astral.sh/ruff/rules/
|
||||
[lint]
|
||||
extend-select = [
|
||||
"W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
|
||||
"I", # https://docs.astral.sh/ruff/rules/#isort-i
|
||||
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
|
||||
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
||||
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
||||
"EXE", # https://docs.astral.sh/ruff/rules/#flake8-executable-exe
|
||||
"ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc
|
||||
"ICN", # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn
|
||||
"G201", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g
|
||||
"INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp
|
||||
"PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie
|
||||
"Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q
|
||||
"RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse
|
||||
"T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20
|
||||
"SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
|
||||
"TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
|
||||
"TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch
|
||||
"PLC", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||
"PLE", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
|
||||
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
|
||||
]
|
||||
# TODO PTH https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
|
||||
ignore = ["DJ001", "SIM105", "RUF012"]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
|
||||
"docker/wait-for-redis.py" = ["INP001"]
|
||||
"docker/wait-for-redis.py" = ["INP001", "T201"]
|
||||
"*/tests/*.py" = ["E501", "SIM117"]
|
||||
"*/migrations/*.py" = ["E501", "SIM"]
|
||||
"*/migrations/*.py" = ["E501", "SIM", "T201"]
|
||||
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001"]
|
||||
"src/documents/models.py" = ["SIM115"]
|
||||
|
||||
[isort]
|
||||
[lint.isort]
|
||||
force-single-line = true
|
||||
|
@@ -5,7 +5,7 @@
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
identity and expression, level of experience, education, socioeconomic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
|
@@ -11,7 +11,7 @@ If you want to implement something big:
|
||||
|
||||
## Python
|
||||
|
||||
Paperless supports python 3.8 and 3.9. We format Python code with [Black](https://github.com/psf/black).
|
||||
Paperless supports python 3.9 - 3.11 at this time. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
|
||||
|
||||
## Branches
|
||||
|
||||
@@ -45,7 +45,7 @@ Examples of `non-trivial` PRs might include:
|
||||
|
||||
- Additional features
|
||||
- Large changes to many distinct files
|
||||
- Breaking or depreciation of existing features
|
||||
- Breaking or deprecation of existing features
|
||||
|
||||
Our community review process for `non-trivial` PRs is the following:
|
||||
|
||||
@@ -58,6 +58,13 @@ Our community review process for `non-trivial` PRs is the following:
|
||||
|
||||
This process might be slow as community members have different schedules and time to dedicate to the Paperless project. However it ensures community code reviews are as brilliantly thorough as they once were with @jonaswinkler.
|
||||
|
||||
# AI-Generated Code
|
||||
|
||||
This project does not specifically prohibit the use of AI-generated code _during the process_ of creating a PR, however:
|
||||
|
||||
1. Any code present in the final PR that was generated using AI sources should be clearly attributed as such and must not violate copyright protections.
|
||||
2. We will not accept PRs that are entirely or mostly AI-derived.
|
||||
|
||||
# Translating Paperless-ngx
|
||||
|
||||
Some notes about translation:
|
||||
@@ -87,7 +94,7 @@ The following files need to be changed:
|
||||
|
||||
- src-ui/angular.json (under the _projects/paperless-ui/i18n/locales_ JSON key)
|
||||
- src/paperless/settings.py (in the _LANGUAGES_ array)
|
||||
- src-ui/src/app/services/settings.service.ts (inside the _getLanguageOptions_ method)
|
||||
- src-ui/src/app/services/settings.service.ts (inside the _LANGUAGE_OPTIONS_ array)
|
||||
- src-ui/src/app/app.module.ts (import locale from _angular/common/locales_ and call _registerLocaleData_)
|
||||
|
||||
Please add the language in the correct order, alphabetically by locale.
|
||||
@@ -130,3 +137,19 @@ All team members are notified when mentioned or assigned to a relevant issue or
|
||||
We are not overly strict with inviting people to the organization. If you have read the [team permissions](#permissions) and think having additional access would enhance your contributions, please reach out to an [admin](#structure) of the team.
|
||||
|
||||
The admins occasionally invite contributors directly if we believe having them on a team will accelerate their work.
|
||||
|
||||
# Automatic Repository Maintenance
|
||||
|
||||
The Paperless-ngx team appreciates all effort and interest from the community in filing bug reports, creating feature requests, sharing ideas and helping other
|
||||
community members. That said, in an effort to keep the repository organized and managebale the project uses automatic handling of certain areas:
|
||||
|
||||
- Issues that cannot be reproduced will be marked 'stale' after 7 days of inactivity and closed after 14 further days of inactivity.
|
||||
- Issues, pull requests and discussions that are closed will be locked after 30 days of inactivity.
|
||||
- Discussions with a marked answer will be automatically closed.
|
||||
- Discussions in the 'General' or 'Support' categories will be closed after 180 days of inactivity.
|
||||
- Feature requests that do not meet the following thresholds will be closed: 5 "up-votes" after 180 days of inactivity or 10 "up-votes" after 365 days.
|
||||
|
||||
In all cases, threads can be re-opened by project maintainers and, of course, users can always create a new discussion for related concerns.
|
||||
Finally, remember that all information remains searchable and 'closed' feature requests can still serve as inspiration for new features.
|
||||
|
||||
Thank you all for your contributions.
|
||||
|
194
Dockerfile
@@ -1,18 +1,28 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
# syntax=docker/dockerfile:1
|
||||
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md
|
||||
|
||||
# Stage: compile-frontend
|
||||
# Purpose: Compiles the frontend
|
||||
# Notes:
|
||||
# - Does NPM stuff with Typescript and such
|
||||
FROM --platform=$BUILDPLATFORM node:16-bullseye-slim AS compile-frontend
|
||||
FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim AS compile-frontend
|
||||
|
||||
COPY ./src-ui /src/src-ui
|
||||
|
||||
WORKDIR /src/src-ui
|
||||
RUN set -eux \
|
||||
&& npm update npm -g \
|
||||
&& npm ci --omit=optional
|
||||
&& npm ci
|
||||
|
||||
ARG PNGX_TAG_VERSION=
|
||||
# Add the tag to the environment file if its a tagged dev build
|
||||
RUN set -eux && \
|
||||
case "${PNGX_TAG_VERSION}" in \
|
||||
dev|fix*|feature*) \
|
||||
sed -i -E "s/version: '([0-9\.]+)'/version: '\1 #${PNGX_TAG_VERSION}'/g" /src/src-ui/src/environments/environment.prod.ts \
|
||||
;; \
|
||||
esac
|
||||
|
||||
RUN set -eux \
|
||||
&& ./node_modules/.bin/ng build --configuration production
|
||||
|
||||
@@ -21,7 +31,7 @@ RUN set -eux \
|
||||
# Comments:
|
||||
# - pipenv dependencies are not left in the final image
|
||||
# - pipenv can't touch the final image somehow
|
||||
FROM --platform=$BUILDPLATFORM python:3.9-slim-bullseye as pipenv-base
|
||||
FROM --platform=$BUILDPLATFORM docker.io/python:3.11-alpine AS pipenv-base
|
||||
|
||||
WORKDIR /usr/src/pipenv
|
||||
|
||||
@@ -29,7 +39,7 @@ COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pipenv" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.3.20 \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2024.0.1 \
|
||||
&& echo "Generating requirement.txt" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
@@ -37,7 +47,7 @@ RUN set -eux \
|
||||
# Purpose: The final image
|
||||
# Comments:
|
||||
# - Don't leave anything extra in here
|
||||
FROM python:3.9-slim-bullseye as main-app
|
||||
FROM docker.io/python:3.11-slim-bookworm AS main-app
|
||||
|
||||
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
|
||||
LABEL org.opencontainers.image.documentation="https://docs.paperless-ngx.com/"
|
||||
@@ -47,6 +57,21 @@ LABEL org.opencontainers.image.licenses="GPL-3.0-only"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Buildx provided, must be defined to use though
|
||||
ARG TARGETARCH
|
||||
|
||||
# Can be workflow provided, defaults set for manual building
|
||||
ARG JBIG2ENC_VERSION=0.29
|
||||
ARG QPDF_VERSION=11.9.0
|
||||
ARG GS_VERSION=10.03.1
|
||||
|
||||
# Set Python environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# Ignore warning from Whitenoise
|
||||
PYTHONWARNINGS="ignore:::django.http.response:517" \
|
||||
PNGX_CONTAINERIZED=1
|
||||
|
||||
#
|
||||
# Begin installation and configuration
|
||||
# Order the steps below from least often changed to most
|
||||
@@ -67,23 +92,10 @@ ARG RUNTIME_PACKAGES="\
|
||||
gnupg \
|
||||
icc-profiles-free \
|
||||
imagemagick \
|
||||
# Image processing
|
||||
liblept5 \
|
||||
liblcms2-2 \
|
||||
libtiff5 \
|
||||
libfreetype6 \
|
||||
libwebp6 \
|
||||
libopenjp2-7 \
|
||||
libimagequant0 \
|
||||
libraqm0 \
|
||||
libjpeg62-turbo \
|
||||
# PostgreSQL
|
||||
libpq5 \
|
||||
postgresql-client \
|
||||
# MySQL / MariaDB
|
||||
mariadb-client \
|
||||
# For Numpy
|
||||
libatlas3-base \
|
||||
# OCRmyPDF dependencies
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
@@ -93,11 +105,12 @@ ARG RUNTIME_PACKAGES="\
|
||||
tesseract-ocr-spa \
|
||||
unpaper \
|
||||
pngquant \
|
||||
# pikepdf / qpdf
|
||||
jbig2dec \
|
||||
# lxml
|
||||
libxml2 \
|
||||
libxslt1.1 \
|
||||
libgnutls30 \
|
||||
# itself
|
||||
qpdf \
|
||||
# Mime type detection
|
||||
file \
|
||||
libmagic1 \
|
||||
@@ -105,9 +118,7 @@ ARG RUNTIME_PACKAGES="\
|
||||
zlib1g \
|
||||
# Barcode splitter
|
||||
libzbar0 \
|
||||
poppler-utils \
|
||||
# RapidFuzz on armv7
|
||||
libatomic1"
|
||||
poppler-utils"
|
||||
|
||||
# Install basic runtime packages.
|
||||
# These change very infrequently
|
||||
@@ -115,7 +126,37 @@ RUN set -eux \
|
||||
echo "Installing system packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& echo "Installing pre-built updates" \
|
||||
&& echo "Installing qpdf ${QPDF_VERSION}" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing Ghostscript ${GS_VERSION}" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
&& dpkg --install ./libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
&& dpkg --install ./libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing jbig2enc" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Cleaning up image layer" \
|
||||
&& rm --force --verbose *.deb \
|
||||
&& rm --recursive --force --verbose /var/lib/apt/lists/* \
|
||||
&& echo "Installing supervisor" \
|
||||
&& python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor==4.2.5
|
||||
|
||||
@@ -162,47 +203,10 @@ RUN set -eux \
|
||||
&& chmod 755 /usr/local/bin/paperless_cmd.sh \
|
||||
&& mv flower-conditional.sh /usr/local/bin/flower-conditional.sh \
|
||||
&& chmod 755 /usr/local/bin/flower-conditional.sh \
|
||||
&& echo "Installing managment commands" \
|
||||
&& echo "Installing management commands" \
|
||||
&& chmod +x install_management_commands.sh \
|
||||
&& ./install_management_commands.sh
|
||||
|
||||
# Buildx provided, must be defined to use though
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
# Workflow provided, defaults set for manual building
|
||||
ARG JBIG2ENC_VERSION=0.29
|
||||
ARG QPDF_VERSION=11.3.0
|
||||
ARG PIKEPDF_VERSION=7.1.1
|
||||
ARG PSYCOPG2_VERSION=2.9.5
|
||||
|
||||
# Install the built packages from the installer library images
|
||||
# These change sometimes
|
||||
RUN set -eux \
|
||||
&& echo "Getting binaries" \
|
||||
&& mkdir paperless-ngx \
|
||||
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/paperless-ngx/archive/ba28a1e16c27d121b644b4f6bdb78855a2850561.tar.gz \
|
||||
&& tar -xf paperless-ngx.tar.gz --directory paperless-ngx --strip-components=1 \
|
||||
&& cd paperless-ngx \
|
||||
# Setting a specific revision ensures we know what this installed
|
||||
# and ensures cache breaking on changes
|
||||
&& echo "Installing jbig2enc" \
|
||||
&& cp ./jbig2enc/${JBIG2ENC_VERSION}/${TARGETARCH}${TARGETVARIANT}/jbig2 /usr/local/bin/ \
|
||||
&& cp ./jbig2enc/${JBIG2ENC_VERSION}/${TARGETARCH}${TARGETVARIANT}/libjbig2enc* /usr/local/lib/ \
|
||||
&& echo "Installing qpdf" \
|
||||
&& apt-get install --yes --no-install-recommends ./qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/libqpdf29_*.deb \
|
||||
&& apt-get install --yes --no-install-recommends ./qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/qpdf_*.deb \
|
||||
&& echo "Installing pikepdf and dependencies" \
|
||||
&& python3 -m pip install --no-cache-dir ./pikepdf/${PIKEPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/*.whl \
|
||||
&& python3 -m pip list \
|
||||
&& echo "Installing psycopg2" \
|
||||
&& python3 -m pip install --no-cache-dir ./psycopg2/${PSYCOPG2_VERSION}/${TARGETARCH}${TARGETVARIANT}/psycopg2*.whl \
|
||||
&& python3 -m pip list \
|
||||
&& echo "Cleaning up image layer" \
|
||||
&& cd ../ \
|
||||
&& rm -rf paperless-ngx \
|
||||
&& rm paperless-ngx.tar.gz
|
||||
|
||||
WORKDIR /usr/src/paperless/src/
|
||||
|
||||
# Python dependencies
|
||||
@@ -214,44 +218,70 @@ COPY --from=pipenv-base /usr/src/pipenv/requirements.txt ./
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
git \
|
||||
# https://www.psycopg.org/docs/install.html#prerequisites
|
||||
libpq-dev \
|
||||
# https://github.com/PyMySQL/mysqlclient#linux
|
||||
default-libmysqlclient-dev \
|
||||
python3-dev"
|
||||
pkg-config"
|
||||
|
||||
RUN set -eux \
|
||||
# hadolint ignore=DL3042
|
||||
RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
||||
set -eux \
|
||||
&& echo "Installing build system packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
||||
&& echo "Installing Python requirements" \
|
||||
&& python3 -m pip install --default-timeout=1000 --no-cache-dir --requirement requirements.txt \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output psycopg_c-3.2.1-cp311-cp311-linux_x86_64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.1/psycopg_c-3.2.1-cp311-cp311-linux_x86_64.whl \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output psycopg_c-3.2.1-cp311-cp311-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.1/psycopg_c-3.2.1-cp311-cp311-linux_aarch64.whl \
|
||||
&& python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \
|
||||
&& echo "Patching whitenoise for compression speedup" \
|
||||
&& curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch \
|
||||
&& patch -d /usr/local/lib/python3.11/site-packages --verbose -p2 < 484.patch \
|
||||
&& rm 484.patch \
|
||||
&& echo "Installing NLTK data" \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" punkt \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" punkt_tab \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
&& apt-get --yes purge ${BUILD_PACKAGES} \
|
||||
&& apt-get --yes autoremove --purge \
|
||||
&& apt-get clean --yes \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /tmp/* \
|
||||
&& rm -rf /var/tmp/* \
|
||||
&& rm -rf /var/cache/apt/archives/* \
|
||||
&& truncate -s 0 /var/log/*log
|
||||
&& rm --recursive --force --verbose *.whl \
|
||||
&& rm --recursive --force --verbose /var/lib/apt/lists/* \
|
||||
&& rm --recursive --force --verbose /tmp/* \
|
||||
&& rm --recursive --force --verbose /var/tmp/* \
|
||||
&& rm --recursive --force --verbose /var/cache/apt/archives/* \
|
||||
&& truncate --size 0 /var/log/*log
|
||||
|
||||
# copy backend
|
||||
COPY ./src ./
|
||||
COPY --chown=1000:1000 ./src ./
|
||||
|
||||
# copy frontend
|
||||
COPY --from=compile-frontend /src/src/documents/static/frontend/ ./documents/static/frontend/
|
||||
COPY --from=compile-frontend --chown=1000:1000 /src/src/documents/static/frontend/ ./documents/static/frontend/
|
||||
|
||||
# add users, setup scripts
|
||||
# Mount the compiled frontend to expected location
|
||||
RUN set -eux \
|
||||
&& addgroup --gid 1000 paperless \
|
||||
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
|
||||
&& chown -R paperless:paperless /usr/src/paperless \
|
||||
&& gosu paperless python3 manage.py collectstatic --clear --no-input --link \
|
||||
&& gosu paperless python3 manage.py compilemessages
|
||||
&& echo "Setting up user/group" \
|
||||
&& addgroup --gid 1000 paperless \
|
||||
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
|
||||
&& echo "Creating volume directories" \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/data \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/media \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/consume \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/export \
|
||||
&& echo "Creating gnupg directory" \
|
||||
&& mkdir -m700 --verbose /usr/src/paperless/.gnupg \
|
||||
&& echo "Adjusting all permissions" \
|
||||
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \
|
||||
&& echo "Collecting static files" \
|
||||
&& gosu paperless python3 manage.py collectstatic --clear --no-input --link \
|
||||
&& gosu paperless python3 manage.py compilemessages
|
||||
|
||||
VOLUME ["/usr/src/paperless/data", \
|
||||
"/usr/src/paperless/media", \
|
||||
@@ -263,3 +293,5 @@ ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["/usr/local/bin/paperless_cmd.sh"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=5 CMD [ "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000" ]
|
||||
|
100
Pipfile
@@ -3,82 +3,81 @@ url = "https://pypi.python.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[[source]]
|
||||
url = "https://www.piwheels.org/simple"
|
||||
verify_ssl = true
|
||||
name = "piwheels"
|
||||
|
||||
[packages]
|
||||
dateparser = "~=1.1"
|
||||
django = "~=4.1"
|
||||
django-cors-headers = "*"
|
||||
dateparser = "~=1.2"
|
||||
# WARNING: django does not use semver.
|
||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||
django = "~=4.2.15"
|
||||
django-allauth = {extras = ["socialaccount"], version = "*"}
|
||||
django-auditlog = "*"
|
||||
django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
django-guardian = "*"
|
||||
django-cors-headers = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=22.1"
|
||||
djangorestframework = "~=3.14"
|
||||
django-filter = "~=24.3"
|
||||
django-guardian = "*"
|
||||
django-multiselectfield = "*"
|
||||
django-soft-delete = "*"
|
||||
djangorestframework = "==3.15.2"
|
||||
djangorestframework-guardian = "*"
|
||||
django-ipware = "*"
|
||||
drf-writable-nested = "*"
|
||||
bleach = "*"
|
||||
celery = {extras = ["redis"], version = "*"}
|
||||
channels = "~=4.1"
|
||||
channels-redis = "*"
|
||||
concurrent-log-handler = "*"
|
||||
filelock = "*"
|
||||
flower = "*"
|
||||
gotenberg-client = "*"
|
||||
gunicorn = "*"
|
||||
imap-tools = "*"
|
||||
inotifyrecursive = "~=0.3"
|
||||
langdetect = "*"
|
||||
mysqlclient = "*"
|
||||
nltk = "*"
|
||||
ocrmypdf = "~=15.4"
|
||||
pathvalidate = "*"
|
||||
pillow = "~=9.4"
|
||||
pikepdf = "*"
|
||||
python-gnupg = "*"
|
||||
python-dotenv = "*"
|
||||
pdf2image = "*"
|
||||
psycopg = {version = "*", extras = ["c"]}
|
||||
python-dateutil = "*"
|
||||
python-dotenv = "*"
|
||||
python-gnupg = "*"
|
||||
python-ipware = "*"
|
||||
python-magic = "*"
|
||||
psycopg2 = "*"
|
||||
pyzbar = "*"
|
||||
rapidfuzz = "*"
|
||||
redis = {extras = ["hiredis"], version = "*"}
|
||||
scikit-learn = "~=1.2"
|
||||
numpy = "*"
|
||||
whitenoise = "~=6.3"
|
||||
watchdog = "~=2.2"
|
||||
whoosh="~=2.7"
|
||||
inotifyrecursive = "~=0.3"
|
||||
ocrmypdf = "~=14.0"
|
||||
tqdm = "*"
|
||||
tika = "*"
|
||||
# TODO: This will sadly also install daphne+dependencies,
|
||||
# which an ASGI server we don't need. Adds about 15MB image size.
|
||||
channels = "~=3.0"
|
||||
channels-redis = "*"
|
||||
uvicorn = {extras = ["standard"], version = "*"}
|
||||
concurrent-log-handler = "*"
|
||||
"pdfminer.six" = "*"
|
||||
pyzbar = "*"
|
||||
mysqlclient = "*"
|
||||
celery = {extras = ["redis"], version = "*"}
|
||||
scikit-learn = "~=1.5"
|
||||
setproctitle = "*"
|
||||
nltk = "*"
|
||||
pdf2image = "*"
|
||||
flower = "*"
|
||||
bleach = "*"
|
||||
tika-client = "*"
|
||||
tqdm = "*"
|
||||
# See https://github.com/paperless-ngx/paperless-ngx/issues/5494
|
||||
uvicorn = {extras = ["standard"], version = "==0.25.0"}
|
||||
watchdog = "~=4.0"
|
||||
whitenoise = "~=6.7"
|
||||
whoosh = "~=2.7"
|
||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||
#
|
||||
# Packages locked due to issues (try to check if these are fixed in a release every so often)
|
||||
#
|
||||
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
|
||||
scipy = "==1.8.1"
|
||||
|
||||
[dev-packages]
|
||||
coveralls = "*"
|
||||
# Linting
|
||||
pre-commit = "*"
|
||||
ruff = "*"
|
||||
# Testing
|
||||
factory-boy = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-django = "*"
|
||||
pytest-httpx = "*"
|
||||
pytest-env = "*"
|
||||
pytest-sugar = "*"
|
||||
pytest-xdist = "*"
|
||||
black = "*"
|
||||
pre-commit = "*"
|
||||
pytest-mock = "*"
|
||||
pytest-rerunfailures = "*"
|
||||
imagehash = "*"
|
||||
daphne = "*"
|
||||
# Documentation
|
||||
mkdocs-material = "*"
|
||||
ruff = "*"
|
||||
mkdocs-glightbox = "*"
|
||||
|
||||
[typing-dev]
|
||||
mypy = "*"
|
||||
@@ -90,12 +89,9 @@ celery-types = "*"
|
||||
django-stubs = {extras= ["compatible-mypy"], version="*"}
|
||||
types-dateparser = "*"
|
||||
types-bleach = "*"
|
||||
types-humanfriendly = "*"
|
||||
types-redis = "*"
|
||||
types-tqdm = "*"
|
||||
types-Markdown = "*"
|
||||
types-Pygments = "*"
|
||||
types-backports = "*"
|
||||
types-colorama = "*"
|
||||
types-psycopg2 = "*"
|
||||
types-setuptools = "*"
|
||||
|
6340
Pipfile.lock
generated
73
README.md
@@ -6,8 +6,11 @@
|
||||
[](https://demo.paperless-ngx.com)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png#gh-light-mode-only" width="50%" />
|
||||
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/White%20logo%20-%20no%20background.png#gh-dark-mode-only" width="50%" />
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/resources/logo/web/png/White%20logo%20-%20no%20background.png" width="50%">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
|
||||
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<!-- omit in toc -->
|
||||
@@ -16,10 +19,9 @@
|
||||
|
||||
Paperless-ngx is a document management system that transforms your physical documents into a searchable online archive so you can keep, well, _less paper_.
|
||||
|
||||
Paperless-ngx forked from [paperless-ng](https://github.com/jonaswinkler/paperless-ng) to continue the great work and distribute responsibility of supporting and advancing the project among a team of people. [Consider joining us!](#community-support) Discussion of this transition can be found in issues
|
||||
[#1599](https://github.com/jonaswinkler/paperless-ng/issues/1599) and [#1632](https://github.com/jonaswinkler/paperless-ng/issues/1632).
|
||||
Paperless-ngx is the official successor to the original [Paperless](https://github.com/the-paperless-project/paperless) & [Paperless-ng](https://github.com/jonaswinkler/paperless-ng) projects and is designed to distribute the responsibility of advancing and supporting the project among a team of people. [Consider joining us!](#community-support)
|
||||
|
||||
A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. _Note: demo content is reset frequently and confidential information should not be uploaded._
|
||||
Thanks to the generous folks at [DigitalOcean](https://m.do.co/c/8d70b916d462), a demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. _Note: demo content is reset frequently and confidential information should not be uploaded._
|
||||
|
||||
- [Features](#features)
|
||||
- [Getting started](#getting-started)
|
||||
@@ -28,48 +30,40 @@ A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com)
|
||||
- [Translation](#translation)
|
||||
- [Feature Requests](#feature-requests)
|
||||
- [Bugs](#bugs)
|
||||
- [Affiliated Projects](#affiliated-projects)
|
||||
- [Related Projects](#related-projects)
|
||||
- [Important Note](#important-note)
|
||||
|
||||
<p align="right">This project is supported by:<br/>
|
||||
<a href="https://m.do.co/c/8d70b916d462" style="padding-top: 4px; display: block;">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_white.svg" width="140px">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="140px">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_black_.svg" width="140px">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Features
|
||||
|
||||

|
||||

|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards.png">
|
||||
<img src="https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docs/assets/screenshots/documents-smallcards.png">
|
||||
</picture>
|
||||
|
||||
- Organize and index your scanned documents with tags, correspondents, types, and more.
|
||||
- Performs OCR on your documents, adds selectable text to image only documents and adds tags, correspondents and document types to your documents.
|
||||
- Supports PDF documents, images, plain text files, and Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents).
|
||||
- Office document support is optional and provided by Apache Tika (see [configuration](https://docs.paperless-ngx.com/configuration/#tika))
|
||||
- Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely.
|
||||
- Single page application front end.
|
||||
- Includes a dashboard that shows basic statistics and has document upload.
|
||||
- Filtering by tags, correspondents, types, and more.
|
||||
- Customizable views can be saved and displayed on the dashboard.
|
||||
- Full text search helps you find what you need.
|
||||
- Auto completion suggests relevant words from your documents.
|
||||
- Results are sorted by relevance to your search query.
|
||||
- Highlighting shows you which parts of the document matched the query.
|
||||
- Searching for similar documents ("More like this")
|
||||
- Email processing: Paperless adds documents from your email accounts.
|
||||
- Configure multiple accounts and filters for each account.
|
||||
- When adding documents from mail, paperless can move these mail to a new folder, mark them as read, flag them as important or delete them.
|
||||
- Machine learning powered document matching.
|
||||
- Paperless-ngx learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless.
|
||||
- Optimized for multi core systems: Paperless-ngx consumes multiple documents in parallel.
|
||||
- The integrated sanity checker makes sure that your document archive is in good health.
|
||||
- [More screenshots are available in the documentation](https://docs.paperless-ngx.com/#screenshots).
|
||||
A full list of [features](https://docs.paperless-ngx.com/#features) and [screenshots](https://docs.paperless-ngx.com/#screenshots) are available in the [documentation](https://docs.paperless-ngx.com/).
|
||||
|
||||
# Getting started
|
||||
|
||||
The easiest way to deploy paperless is docker-compose. The files in the [`/docker/compose` directory](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose) are configured to pull the image from Github Packages.
|
||||
The easiest way to deploy paperless is `docker compose`. The files in the [`/docker/compose` directory](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose) are configured to pull the image from GitHub Packages.
|
||||
|
||||
If you'd like to jump right in, you can configure a docker-compose environment with our install script:
|
||||
If you'd like to jump right in, you can configure a `docker compose` environment with our install script:
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -L https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
||||
```
|
||||
|
||||
Alternatively, you can install the dependencies and setup apache and a database server yourself. The [documentation](https://docs.paperless-ngx.com/setup/#installation) has a step by step guide on how to do it.
|
||||
More details and step-by-step guides for alternative installation methods can be found in [the documentation](https://docs.paperless-ngx.com/setup/#installation).
|
||||
|
||||
Migrating from Paperless-ng is easy, just drop in the new docker image! See the [documentation on migrating](https://docs.paperless-ngx.com/setup/#migrating-to-paperless-ngx) for more details.
|
||||
|
||||
@@ -85,7 +79,7 @@ If you feel like contributing to the project, please do! Bug fixes, enhancements
|
||||
|
||||
## Community Support
|
||||
|
||||
People interested in continuing the work on paperless-ngx are encouraged to reach out here on github and in the [Matrix Room](https://matrix.to/#/#paperless:adnidor.de). If you would like to contribute to the project on an ongoing basis there are multiple [teams](https://github.com/orgs/paperless-ngx/people) (frontend, ci/cd, etc) that could use your help so please reach out!
|
||||
People interested in continuing the work on paperless-ngx are encouraged to reach out here on github and in the [Matrix Room](https://matrix.to/#/#paperless:matrix.org). If you would like to contribute to the project on an ongoing basis there are multiple [teams](https://github.com/orgs/paperless-ngx/people) (frontend, ci/cd, etc) that could use your help so please reach out!
|
||||
|
||||
## Translation
|
||||
|
||||
@@ -99,16 +93,9 @@ Feature requests can be submitted via [GitHub Discussions](https://github.com/pa
|
||||
|
||||
For bugs please [open an issue](https://github.com/paperless-ngx/paperless-ngx/issues) or [start a discussion](https://github.com/paperless-ngx/paperless-ngx/discussions) if you have questions.
|
||||
|
||||
# Affiliated Projects
|
||||
# Related Projects
|
||||
|
||||
Paperless has been around for a while now, and people have built tools that interact with it. If you're one of them, please reach out and we can add your project to the list. Current projects include:
|
||||
|
||||
- **Mobile**
|
||||
- [Paperless App](https://github.com/bauerj/paperless_app): An Android/iOS application for Paperless-ngx.
|
||||
- [Paperless Mobile](https://github.com/astubenbord/paperless-mobile): A modern, feature rich Android app for Paperless-ngx.
|
||||
- [Paperless Share](https://github.com/qcasey/paperless_share): Share any files from your Android application with Paperless-ngx. Very simple, but works with all mobile scanning apps that allow you to share scanned documents.
|
||||
- **Desktop**
|
||||
- [Scan to Paperless](https://github.com/sbrunner/scan-to-paperless): Scan and prepare (crop, deskew, OCR, ...) your documents for use in Paperless-ngx.
|
||||
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects) for a user-maintained list of related projects and software that is compatible with Paperless-ngx.
|
||||
|
||||
# Important Note
|
||||
|
||||
|
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
The Paperless-ngx team and community take security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
||||
|
||||
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/paperless-ngx/paperless-ngx/security/advisories/new) tab.
|
||||
|
||||
The team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Helper script for building the Docker image locally.
|
||||
# Parses and provides the nessecary versions of other images to Docker
|
||||
# before passing in the rest of script args.
|
||||
|
||||
# First Argument: The Dockerfile to build
|
||||
# Other Arguments: Additional arguments to docker build
|
||||
|
||||
# Example Usage:
|
||||
# ./build-docker-image.sh Dockerfile -t paperless-ngx:my-awesome-feature
|
||||
|
||||
set -eu
|
||||
|
||||
if ! command -v jq &> /dev/null ; then
|
||||
echo "jq required"
|
||||
exit 1
|
||||
elif [ ! -f "$1" ]; then
|
||||
echo "$1 is not a file, please provide the Dockerfile"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the branch name (used for caching)
|
||||
branch_name=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Parse eithe Pipfile.lock or the .build-config.json
|
||||
jbig2enc_version=$(jq -r '.jbig2enc.version' .build-config.json)
|
||||
qpdf_version=$(jq -r '.qpdf.version' .build-config.json)
|
||||
psycopg2_version=$(jq -r '.default.psycopg2.version | gsub("=";"")' Pipfile.lock)
|
||||
pikepdf_version=$(jq -r '.default.pikepdf.version | gsub("=";"")' Pipfile.lock)
|
||||
pillow_version=$(jq -r '.default.pillow.version | gsub("=";"")' Pipfile.lock)
|
||||
lxml_version=$(jq -r '.default.lxml.version | gsub("=";"")' Pipfile.lock)
|
||||
|
||||
base_filename="$(basename -- "${1}")"
|
||||
build_args_str=""
|
||||
cache_from_str=""
|
||||
|
||||
case "${base_filename}" in
|
||||
|
||||
*.jbig2enc)
|
||||
build_args_str="--build-arg JBIG2ENC_VERSION=${jbig2enc_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/jbig2enc:${jbig2enc_version}"
|
||||
;;
|
||||
|
||||
*.psycopg2)
|
||||
build_args_str="--build-arg PSYCOPG2_VERSION=${psycopg2_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/psycopg2:${psycopg2_version}"
|
||||
;;
|
||||
|
||||
*.qpdf)
|
||||
build_args_str="--build-arg QPDF_VERSION=${qpdf_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/qpdf:${qpdf_version}"
|
||||
;;
|
||||
|
||||
*.pikepdf)
|
||||
build_args_str="--build-arg QPDF_VERSION=${qpdf_version} --build-arg PIKEPDF_VERSION=${pikepdf_version} --build-arg PILLOW_VERSION=${pillow_version} --build-arg LXML_VERSION=${lxml_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/pikepdf:${pikepdf_version}"
|
||||
;;
|
||||
|
||||
Dockerfile)
|
||||
build_args_str="--build-arg QPDF_VERSION=${qpdf_version} --build-arg PIKEPDF_VERSION=${pikepdf_version} --build-arg PSYCOPG2_VERSION=${psycopg2_version} --build-arg JBIG2ENC_VERSION=${jbig2enc_version}"
|
||||
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:${branch_name} --cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:dev"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unable to match ${base_filename}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
read -r -a build_args_arr <<< "${build_args_str}"
|
||||
read -r -a cache_from_arr <<< "${cache_from_str}"
|
||||
|
||||
set -eux
|
||||
|
||||
docker buildx build --file "${1}" \
|
||||
--progress=plain \
|
||||
--output=type=docker \
|
||||
"${cache_from_arr[@]}" \
|
||||
"${build_args_arr[@]}" \
|
||||
"${@:2}" .
|
@@ -1,4 +1,6 @@
|
||||
commit_message: '[ci skip]'
|
||||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /src/locale/en_US/LC_MESSAGES/django.po
|
||||
translation: /src/locale/%locale_with_underscore%/LC_MESSAGES/django.po
|
||||
|
@@ -1,48 +0,0 @@
|
||||
# This Dockerfile compiles the jbig2enc library
|
||||
# Inputs:
|
||||
# - JBIG2ENC_VERSION - the Git tag to checkout and build
|
||||
|
||||
FROM debian:bullseye-slim as main
|
||||
|
||||
LABEL org.opencontainers.image.description="A intermediate image with jbig2enc built"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG JBIG2ENC_VERSION
|
||||
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
automake \
|
||||
libtool \
|
||||
libleptonica-dev \
|
||||
zlib1g-dev \
|
||||
git \
|
||||
ca-certificates"
|
||||
|
||||
WORKDIR /usr/src/jbig2enc
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing build tools" \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
|
||||
&& echo "Building jbig2enc" \
|
||||
&& git clone --quiet --branch $JBIG2ENC_VERSION https://github.com/agl/jbig2enc . \
|
||||
&& ./autogen.sh \
|
||||
&& ./configure \
|
||||
&& make \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./pkg-list.txt \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& echo "Moving files around" \
|
||||
&& mkdir build \
|
||||
# Unlink a symlink that causes problems
|
||||
&& unlink ./src/.libs/libjbig2enc.la \
|
||||
# Move what the link pointed to
|
||||
&& mv ./src/libjbig2enc.la ./build/ \
|
||||
# Move the shared library .so files
|
||||
&& mv ./src/.libs/libjbig2enc* ./build/ \
|
||||
# And move the cli binary
|
||||
&& mv ./src/jbig2 ./build/ \
|
||||
&& mv ./pkg-list.txt ./build/
|
@@ -1,118 +0,0 @@
|
||||
# This Dockerfile builds the pikepdf wheel
|
||||
# Inputs:
|
||||
# - REPO - Docker repository to pull qpdf from
|
||||
# - QPDF_VERSION - The image qpdf version to copy .deb files from
|
||||
# - PIKEPDF_VERSION - Version of pikepdf to build wheel for
|
||||
|
||||
# Default to pulling from the main repo registry when manually building
|
||||
ARG REPO="paperless-ngx/paperless-ngx"
|
||||
|
||||
# This does nothing, except provide a name for a copy below
|
||||
ARG QPDF_VERSION
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/${REPO}/builder/qpdf:${QPDF_VERSION} as qpdf-builder
|
||||
|
||||
#
|
||||
# Stage: builder
|
||||
# Purpose:
|
||||
# - Build the pikepdf wheel
|
||||
# - Build any dependent wheels which can't be found
|
||||
#
|
||||
FROM python:3.9-slim-bullseye as builder
|
||||
|
||||
LABEL org.opencontainers.image.description="A intermediate image with pikepdf wheel built"
|
||||
|
||||
# Buildx provided
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
# Workflow provided
|
||||
ARG QPDF_VERSION
|
||||
ARG PIKEPDF_VERSION
|
||||
# These are not used, but will still bust the cache if one changes
|
||||
# Otherwise, the main image will try to build thing (and fail)
|
||||
ARG PILLOW_VERSION
|
||||
ARG LXML_VERSION
|
||||
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
# qpdf requirement - https://github.com/qpdf/qpdf#crypto-providers
|
||||
libgnutls28-dev \
|
||||
# lxml requrements - https://lxml.de/installation.html
|
||||
libxml2-dev \
|
||||
libxslt1-dev \
|
||||
# Pillow requirements - https://pillow.readthedocs.io/en/stable/installation.html#external-libraries
|
||||
# JPEG functionality
|
||||
libjpeg62-turbo-dev \
|
||||
# conpressed PNG
|
||||
zlib1g-dev \
|
||||
# compressed TIFF
|
||||
libtiff-dev \
|
||||
# type related services
|
||||
libfreetype-dev \
|
||||
# color management
|
||||
liblcms2-dev \
|
||||
# WebP format
|
||||
libwebp-dev \
|
||||
# JPEG 2000
|
||||
libopenjp2-7-dev \
|
||||
# improved color quantization
|
||||
libimagequant-dev \
|
||||
# complex text layout support
|
||||
libraqm-dev"
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
COPY --from=qpdf-builder /usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/*.deb ./
|
||||
|
||||
# As this is an base image for a multi-stage final image
|
||||
# the added size of the install is basically irrelevant
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing build tools" \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
|
||||
&& echo "Installing qpdf" \
|
||||
&& dpkg --install libqpdf29_*.deb \
|
||||
&& dpkg --install libqpdf-dev_*.deb \
|
||||
&& echo "Installing Python tools" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade \
|
||||
pip \
|
||||
wheel \
|
||||
# https://pikepdf.readthedocs.io/en/latest/installation.html#requirements
|
||||
pybind11 \
|
||||
&& echo "Building pikepdf wheel ${PIKEPDF_VERSION}" \
|
||||
&& mkdir wheels \
|
||||
&& python3 -m pip wheel \
|
||||
# Build the package at the required version
|
||||
pikepdf==${PIKEPDF_VERSION} \
|
||||
# Look to piwheels for additional pre-built wheels
|
||||
--extra-index-url https://www.piwheels.org/simple \
|
||||
# Output the *.whl into this directory
|
||||
--wheel-dir wheels \
|
||||
# Do not use a binary packge for the package being built
|
||||
--no-binary=pikepdf \
|
||||
# Do use binary packages for dependencies
|
||||
--prefer-binary \
|
||||
# Don't cache build files
|
||||
--no-cache-dir \
|
||||
&& ls -ahl wheels \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./wheels/pkg-list.txt \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#
|
||||
# Stage: package
|
||||
# Purpose: Holds the compiled .whl files in a tiny image to pull
|
||||
#
|
||||
FROM alpine:3.17 as package
|
||||
|
||||
WORKDIR /usr/src/wheels/
|
||||
|
||||
COPY --from=builder /usr/src/wheels/*.whl ./
|
||||
COPY --from=builder /usr/src/wheels/pkg-list.txt ./
|
@@ -1,66 +0,0 @@
|
||||
# This Dockerfile builds the psycopg2 wheel
|
||||
# Inputs:
|
||||
# - PSYCOPG2_VERSION - Version to build
|
||||
|
||||
#
|
||||
# Stage: builder
|
||||
# Purpose:
|
||||
# - Build the psycopg2 wheel
|
||||
#
|
||||
FROM python:3.9-slim-bullseye as builder
|
||||
|
||||
LABEL org.opencontainers.image.description="A intermediate image with psycopg2 wheel built"
|
||||
|
||||
ARG PSYCOPG2_VERSION
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
# https://www.psycopg.org/docs/install.html#prerequisites
|
||||
libpq-dev"
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
# As this is an base image for a multi-stage final image
|
||||
# the added size of the install is basically irrelevant
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing build tools" \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
|
||||
&& echo "Installing Python tools" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pip wheel \
|
||||
&& echo "Building psycopg2 wheel ${PSYCOPG2_VERSION}" \
|
||||
&& cd /usr/src \
|
||||
&& mkdir wheels \
|
||||
&& python3 -m pip wheel \
|
||||
# Build the package at the required version
|
||||
psycopg2==${PSYCOPG2_VERSION} \
|
||||
# Output the *.whl into this directory
|
||||
--wheel-dir wheels \
|
||||
# Do not use a binary packge for the package being built
|
||||
--no-binary=psycopg2 \
|
||||
# Do use binary packages for dependencies
|
||||
--prefer-binary \
|
||||
# Don't cache build files
|
||||
--no-cache-dir \
|
||||
&& ls -ahl wheels/ \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./wheels/pkg-list.txt \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||
&& apt-get -y autoremove --purge \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#
|
||||
# Stage: package
|
||||
# Purpose: Holds the compiled .whl files in a tiny image to pull
|
||||
#
|
||||
FROM alpine:3.17 as package
|
||||
|
||||
WORKDIR /usr/src/wheels/
|
||||
|
||||
COPY --from=builder /usr/src/wheels/*.whl ./
|
||||
COPY --from=builder /usr/src/wheels/pkg-list.txt ./
|
@@ -1,156 +0,0 @@
|
||||
#
|
||||
# Stage: pre-build
|
||||
# Purpose:
|
||||
# - Installs common packages
|
||||
# - Sets common environment variables related to dpkg
|
||||
# - Aquires the qpdf source from bookwork
|
||||
# Useful Links:
|
||||
# - https://qpdf.readthedocs.io/en/stable/installation.html#system-requirements
|
||||
# - https://wiki.debian.org/Multiarch/HOWTO
|
||||
# - https://wiki.debian.org/CrossCompiling
|
||||
#
|
||||
|
||||
FROM debian:bullseye-slim as pre-build
|
||||
|
||||
ARG QPDF_VERSION
|
||||
|
||||
ARG COMMON_BUILD_PACKAGES="\
|
||||
cmake \
|
||||
debhelper\
|
||||
debian-keyring \
|
||||
devscripts \
|
||||
dpkg-dev \
|
||||
equivs \
|
||||
packaging-dev \
|
||||
libtool"
|
||||
|
||||
ENV DEB_BUILD_OPTIONS="terse nocheck nodoc parallel=2"
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing common packages" \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${COMMON_BUILD_PACKAGES} \
|
||||
&& echo "Getting qpdf source" \
|
||||
&& echo "deb-src http://deb.debian.org/debian/ bookworm main" > /etc/apt/sources.list.d/bookworm-src.list \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get source --yes --quiet qpdf=${QPDF_VERSION}-1/bookworm
|
||||
|
||||
#
|
||||
# Stage: amd64-builder
|
||||
# Purpose: Builds qpdf for x86_64 (native build)
|
||||
#
|
||||
FROM pre-build as amd64-builder
|
||||
|
||||
ARG AMD64_BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
libjpeg62-turbo-dev:amd64 \
|
||||
libgnutls28-dev:amd64 \
|
||||
zlib1g-dev:amd64"
|
||||
|
||||
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Beginning amd64" \
|
||||
&& echo "Install amd64 packages" \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${AMD64_BUILD_PACKAGES} \
|
||||
&& echo "Building amd64" \
|
||||
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean \
|
||||
&& echo "Removing debug files" \
|
||||
&& rm -f ../libqpdf29-dbgsym* \
|
||||
&& rm -f ../qpdf-dbgsym* \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
|
||||
#
|
||||
# Stage: armhf-builder
|
||||
# Purpose:
|
||||
# - Sets armhf specific environment
|
||||
# - Builds qpdf for armhf (cross compile)
|
||||
#
|
||||
FROM pre-build as armhf-builder
|
||||
|
||||
ARG ARMHF_PACKAGES="\
|
||||
crossbuild-essential-armhf \
|
||||
libjpeg62-turbo-dev:armhf \
|
||||
libgnutls28-dev:armhf \
|
||||
zlib1g-dev:armhf"
|
||||
|
||||
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
|
||||
|
||||
ENV CXX="/usr/bin/arm-linux-gnueabihf-g++" \
|
||||
CC="/usr/bin/arm-linux-gnueabihf-gcc"
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Beginning armhf" \
|
||||
&& echo "Install armhf packages" \
|
||||
&& dpkg --add-architecture armhf \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${ARMHF_PACKAGES} \
|
||||
&& echo "Building armhf" \
|
||||
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean --host-arch armhf \
|
||||
&& echo "Removing debug files" \
|
||||
&& rm -f ../libqpdf29-dbgsym* \
|
||||
&& rm -f ../qpdf-dbgsym* \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
|
||||
|
||||
#
|
||||
# Stage: aarch64-builder
|
||||
# Purpose:
|
||||
# - Sets aarch64 specific environment
|
||||
# - Builds qpdf for aarch64 (cross compile)
|
||||
#
|
||||
FROM pre-build as aarch64-builder
|
||||
|
||||
ARG ARM64_PACKAGES="\
|
||||
crossbuild-essential-arm64 \
|
||||
libjpeg62-turbo-dev:arm64 \
|
||||
libgnutls28-dev:arm64 \
|
||||
zlib1g-dev:arm64"
|
||||
|
||||
ENV CXX="/usr/bin/aarch64-linux-gnu-g++" \
|
||||
CC="/usr/bin/aarch64-linux-gnu-gcc"
|
||||
|
||||
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Beginning arm64" \
|
||||
&& echo "Install arm64 packages" \
|
||||
&& dpkg --add-architecture arm64 \
|
||||
&& apt-get update --quiet \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${ARM64_PACKAGES} \
|
||||
&& echo "Building arm64" \
|
||||
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean --host-arch arm64 \
|
||||
&& echo "Removing debug files" \
|
||||
&& rm -f ../libqpdf29-dbgsym* \
|
||||
&& rm -f ../qpdf-dbgsym* \
|
||||
&& echo "Gathering package data" \
|
||||
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
|
||||
|
||||
#
|
||||
# Stage: package
|
||||
# Purpose: Holds the compiled .deb files in arch/variant specific folders
|
||||
#
|
||||
FROM alpine:3.17 as package
|
||||
|
||||
LABEL org.opencontainers.image.description="A image with qpdf installers stored in architecture & version specific folders"
|
||||
|
||||
ARG QPDF_VERSION
|
||||
|
||||
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/amd64
|
||||
|
||||
COPY --from=amd64-builder /usr/src/*.deb ./
|
||||
COPY --from=amd64-builder /usr/src/pkg-list.txt ./
|
||||
|
||||
# Note this is ${TARGETARCH}${TARGETVARIANT} for armv7
|
||||
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/armv7
|
||||
|
||||
COPY --from=armhf-builder /usr/src/*.deb ./
|
||||
COPY --from=armhf-builder /usr/src/pkg-list.txt ./
|
||||
|
||||
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/arm64
|
||||
|
||||
COPY --from=aarch64-builder /usr/src/*.deb ./
|
||||
COPY --from=aarch64-builder /usr/src/pkg-list.txt ./
|
@@ -1,57 +0,0 @@
|
||||
# Installer Library
|
||||
|
||||
This folder contains the Dockerfiles for building certain installers or libraries, which are then pulled into the main image.
|
||||
|
||||
## [jbig2enc](https://github.com/agl/jbig2enc)
|
||||
|
||||
### Why
|
||||
|
||||
JBIG is an image coding which can achieve better compression of images for PDFs.
|
||||
|
||||
### What
|
||||
|
||||
The Docker image builds a shared library file and utility, which is copied into the correct location in the final image.
|
||||
|
||||
### Updating
|
||||
|
||||
1. Ensure the given qpdf version is present in [Debian bookworm](https://packages.debian.org/bookworm/qpdf)
|
||||
2. Update `.build-config.json` to the given version
|
||||
3. If the Debian specific version has incremented, update `Dockerfile.qpdf`
|
||||
|
||||
See Also:
|
||||
|
||||
- [OCRMyPDF Documentation](https://ocrmypdf.readthedocs.io/en/latest/jbig2.html)
|
||||
|
||||
## [psycopg2](https://www.psycopg.org/)
|
||||
|
||||
### Why
|
||||
|
||||
The pre-built wheels of psycopg2 are built on Debian 9, which provides a quite old version of libpq-dev. This causes issue with authentication methods.
|
||||
|
||||
### What
|
||||
|
||||
The image builds psycopg2 wheels on Debian 10 and places the produced wheels into `/usr/src/wheels/`.
|
||||
|
||||
See Also:
|
||||
|
||||
- [Issue 266](https://github.com/paperless-ngx/paperless-ngx/issues/266)
|
||||
|
||||
## [qpdf](https://qpdf.readthedocs.io/en/stable/index.html)
|
||||
|
||||
### Why
|
||||
|
||||
qpdf and it's library provide tools to read, manipulate and fix up PDFs. Version 11 is also required by `pikepdf` 6+ and Debian 9 does not provide above version 10.
|
||||
|
||||
### What
|
||||
|
||||
The Docker image cross compiles .deb installers for each supported architecture of the main image. The installers are placed in `/usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/`
|
||||
|
||||
## [pikepdf](https://pikepdf.readthedocs.io/en/latest/)
|
||||
|
||||
### Why
|
||||
|
||||
Required by OCRMyPdf, this is a general purpose library for PDF manipulation in Python via the qpdf libraries.
|
||||
|
||||
### What
|
||||
|
||||
The built wheels are placed into `/usr/src/wheels/`
|
@@ -1,12 +1,11 @@
|
||||
# docker-compose file for running paperless testing with actual gotenberg
|
||||
# Docker Compose file for running paperless testing with actual gotenberg
|
||||
# and Tika containers for a more end to end test of the Tika related functionality
|
||||
# Can be used locally or by the CI to start the nessecary containers with the
|
||||
# Can be used locally or by the CI to start the necessary containers with the
|
||||
# correct networking for the tests
|
||||
|
||||
version: "3.7"
|
||||
services:
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
image: docker.io/gotenberg/gotenberg:8.7
|
||||
hostname: gotenberg
|
||||
container_name: gotenberg
|
||||
network_mode: host
|
||||
@@ -17,8 +16,10 @@ services:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-javascript=true"
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
- "--log-level=warn"
|
||||
- "--log-format=text"
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
image: docker.io/apache/tika:latest
|
||||
hostname: tika
|
||||
container_name: tika
|
||||
network_mode: host
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# docker compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8000.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), MariaDB is used as the database server.
|
||||
@@ -23,14 +23,13 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:7
|
||||
@@ -39,7 +38,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/mariadb:10
|
||||
image: docker.io/library/mariadb:11
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- dbdata:/var/lib/mysql
|
||||
@@ -60,11 +59,6 @@ services:
|
||||
- tika
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
@@ -83,7 +77,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
image: docker.io/gotenberg/gotenberg:8.7
|
||||
restart: unless-stopped
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
# want to allow external content like tracking pixels or even javascript.
|
||||
@@ -93,7 +87,7 @@ services:
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
image: docker.io/apache/tika:latest
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# Docker Compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8000.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), MariaDB is used as the database server.
|
||||
@@ -19,14 +19,13 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:7
|
||||
@@ -35,7 +34,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/mariadb:10
|
||||
image: docker.io/library/mariadb:11
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- dbdata:/var/lib/mysql
|
||||
@@ -54,11 +53,6 @@ services:
|
||||
- broker
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
@@ -73,7 +67,6 @@ services:
|
||||
PAPERLESS_DBPASS: paperless # only needed if non-default password
|
||||
PAPERLESS_DBPORT: 3306
|
||||
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# Docker Compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8010.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), PostgreSQL is used as the database server.
|
||||
@@ -18,7 +18,7 @@
|
||||
# To install and update paperless with this file, do the following:
|
||||
#
|
||||
# - Open portainer Stacks list and click 'Add stack'
|
||||
# - Paste the contents of this file and assign a name, e.g. 'Paperless'
|
||||
# - Paste the contents of this file and assign a name, e.g. 'paperless'
|
||||
# - Click 'Deploy the stack' and wait for it to be deployed
|
||||
# - Open the list of containers, select paperless_webserver_1
|
||||
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
||||
@@ -28,7 +28,6 @@
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:7
|
||||
@@ -37,7 +36,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:13
|
||||
image: docker.io/library/postgres:16
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
@@ -54,11 +53,6 @@ services:
|
||||
- broker
|
||||
ports:
|
||||
- "8010:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the docker container registry.
|
||||
# Docker Compose file for running paperless from the docker container registry.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8000.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), PostgreSQL is used as the database server.
|
||||
@@ -23,14 +23,13 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:7
|
||||
@@ -39,7 +38,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:13
|
||||
image: docker.io/library/postgres:16
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
@@ -58,11 +57,6 @@ services:
|
||||
- tika
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
@@ -77,7 +71,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
image: docker.io/gotenberg/gotenberg:8.7
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
@@ -88,7 +82,7 @@ services:
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
image: docker.io/apache/tika:latest
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# Docker Compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# as this file and mounted to the correct folders inside the container.
|
||||
# - Paperless listens on port 8000.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Instead of SQLite (default), PostgreSQL is used as the database server.
|
||||
@@ -19,14 +19,13 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:7
|
||||
@@ -35,7 +34,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:13
|
||||
image: docker.io/library/postgres:16
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
@@ -52,11 +51,6 @@ services:
|
||||
- broker
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
@@ -67,7 +61,6 @@ services:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_DBHOST: db
|
||||
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the docker container registry.
|
||||
# Docker Compose file for running paperless from the docker container registry.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
# All compose files of paperless configure paperless in the following way:
|
||||
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# SQLite is used as the database. The SQLite file is stored in the data volume.
|
||||
#
|
||||
# In addition to that, this docker-compose file adds the following optional
|
||||
# In addition to that, this Docker Compose file adds the following optional
|
||||
# configurations:
|
||||
#
|
||||
# - Apache Tika and Gotenberg servers are started with paperless and paperless
|
||||
@@ -23,14 +23,13 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:7
|
||||
@@ -47,11 +46,6 @@ services:
|
||||
- tika
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
@@ -65,7 +59,7 @@ services:
|
||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||
|
||||
gotenberg:
|
||||
image: docker.io/gotenberg/gotenberg:7.8
|
||||
image: docker.io/gotenberg/gotenberg:8.7
|
||||
restart: unless-stopped
|
||||
|
||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||
@@ -76,7 +70,7 @@ services:
|
||||
- "--chromium-allow-list=file:///tmp/.*"
|
||||
|
||||
tika:
|
||||
image: ghcr.io/paperless-ngx/tika:latest
|
||||
image: docker.io/apache/tika:latest
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# docker-compose file for running paperless from the Docker Hub.
|
||||
# Docker Compose file for running paperless from the Docker Hub.
|
||||
# This file contains everything paperless needs to run.
|
||||
# Paperless supports amd64, arm and arm64 hardware.
|
||||
#
|
||||
@@ -16,14 +16,13 @@
|
||||
#
|
||||
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
|
||||
# and '.env' into a folder.
|
||||
# - Run 'docker-compose pull'.
|
||||
# - Run 'docker-compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker-compose up -d'.
|
||||
# - Run 'docker compose pull'.
|
||||
# - Run 'docker compose run --rm webserver createsuperuser' to create a user.
|
||||
# - Run 'docker compose up -d'.
|
||||
#
|
||||
# For more extensive installation and update instructions, refer to the
|
||||
# documentation.
|
||||
|
||||
version: "3.4"
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:7
|
||||
@@ -38,11 +37,6 @@ services:
|
||||
- broker
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
@@ -52,7 +46,6 @@ services:
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
@@ -10,8 +10,8 @@ map_uidgid() {
|
||||
local -r usermap_new_gid=${USERMAP_GID:-${usermap_original_gid:-$usermap_new_uid}}
|
||||
if [[ ${usermap_new_uid} != "${usermap_original_uid}" || ${usermap_new_gid} != "${usermap_original_gid}" ]]; then
|
||||
echo "Mapping UID and GID for paperless:paperless to $usermap_new_uid:$usermap_new_gid"
|
||||
usermod -o -u "${usermap_new_uid}" paperless
|
||||
groupmod -o -g "${usermap_new_gid}" paperless
|
||||
usermod --non-unique --uid "${usermap_new_uid}" paperless
|
||||
groupmod --non-unique --gid "${usermap_new_gid}" paperless
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ custom_container_init() {
|
||||
fi
|
||||
|
||||
# Make sure custom init directory has files in it
|
||||
if [ -n "$(/bin/ls -A "${custom_script_dir}" 2>/dev/null)" ]; then
|
||||
if [ -n "$(/bin/ls --almost-all "${custom_script_dir}" 2>/dev/null)" ]; then
|
||||
echo "[custom-init] files found in ${custom_script_dir} executing"
|
||||
# Loop over files in the directory
|
||||
for SCRIPT in "${custom_script_dir}"/*; do
|
||||
@@ -86,23 +86,23 @@ initialize() {
|
||||
"${CONSUME_DIR}"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
echo "Creating directory ${dir}"
|
||||
mkdir "${dir}"
|
||||
mkdir --parents --verbose "${dir}"
|
||||
fi
|
||||
done
|
||||
|
||||
local -r tmp_dir="/tmp/paperless"
|
||||
echo "Creating directory ${tmp_dir}"
|
||||
mkdir -p "${tmp_dir}"
|
||||
local -r tmp_dir="${PAPERLESS_SCRATCH_DIR:=/tmp/paperless}"
|
||||
echo "Creating directory scratch directory ${tmp_dir}"
|
||||
mkdir --parents --verbose "${tmp_dir}"
|
||||
|
||||
set +e
|
||||
echo "Adjusting permissions of paperless files. This may take a while."
|
||||
chown -R paperless:paperless ${tmp_dir}
|
||||
chown -R paperless:paperless "${tmp_dir}"
|
||||
for dir in \
|
||||
"${export_dir}" \
|
||||
"${DATA_DIR}" \
|
||||
"${MEDIA_ROOT_DIR}" \
|
||||
"${CONSUME_DIR}"; do
|
||||
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown paperless:paperless {} +
|
||||
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
|
||||
done
|
||||
set -e
|
||||
|
||||
@@ -127,7 +127,7 @@ install_languages() {
|
||||
for lang in "${langs[@]}"; do
|
||||
pkg="tesseract-ocr-$lang"
|
||||
|
||||
if dpkg -s "$pkg" &>/dev/null; then
|
||||
if dpkg --status "$pkg" &>/dev/null; then
|
||||
echo "Package $pkg already installed!"
|
||||
continue
|
||||
fi
|
||||
@@ -138,7 +138,7 @@ install_languages() {
|
||||
fi
|
||||
|
||||
echo "Installing package $pkg..."
|
||||
if ! apt-get -y install "$pkg" &>/dev/null; then
|
||||
if ! apt-get --assume-yes install "$pkg" &>/dev/null; then
|
||||
echo "Could not install $pkg"
|
||||
exit 1
|
||||
fi
|
||||
@@ -148,7 +148,7 @@ install_languages() {
|
||||
echo "Paperless-ngx docker container starting..."
|
||||
|
||||
gosu_cmd=(gosu paperless)
|
||||
if [ "$(id -u)" == "$(id -u paperless)" ]; then
|
||||
if [ "$(id --user)" == "$(id --user paperless)" ]; then
|
||||
gosu_cmd=()
|
||||
fi
|
||||
|
||||
|
@@ -13,7 +13,7 @@ wait_for_postgres() {
|
||||
|
||||
# Disable warning, host and port can't have spaces
|
||||
# shellcheck disable=SC2086
|
||||
while [ ! "$(pg_isready -h ${host} -p ${port})" ]; do
|
||||
while [ ! "$(pg_isready --host ${host} --port ${port})" ]; do
|
||||
|
||||
if [ $attempt_num -eq $max_attempts ]; then
|
||||
echo "Unable to connect to database."
|
||||
@@ -25,6 +25,7 @@ wait_for_postgres() {
|
||||
attempt_num=$(("$attempt_num" + 1))
|
||||
sleep 5
|
||||
done
|
||||
echo "Connected to PostgreSQL"
|
||||
}
|
||||
|
||||
wait_for_mariadb() {
|
||||
@@ -51,6 +52,7 @@ wait_for_mariadb() {
|
||||
attempt_num=$(("$attempt_num" + 1))
|
||||
sleep 5
|
||||
done
|
||||
echo "Connected to MariaDB"
|
||||
}
|
||||
|
||||
wait_for_redis() {
|
||||
@@ -80,7 +82,7 @@ django_checks() {
|
||||
|
||||
search_index() {
|
||||
|
||||
local -r index_version=4
|
||||
local -r index_version=9
|
||||
local -r index_version_file=${DATA_DIR}/.index_version
|
||||
|
||||
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
|
||||
|
@@ -15,6 +15,10 @@ do
|
||||
env_name=${line%%=*}
|
||||
# Check if it starts with "PAPERLESS_" and ends in "_FILE"
|
||||
if [[ ${env_name} == PAPERLESS_*_FILE ]]; then
|
||||
# This should have been named different..
|
||||
if [[ ${env_name} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" ]]; then
|
||||
continue
|
||||
fi
|
||||
# Extract the value of the environment
|
||||
env_value=${line#*=}
|
||||
|
||||
|
@@ -13,6 +13,7 @@ for command in decrypt_documents \
|
||||
document_retagger \
|
||||
document_thumbnails \
|
||||
document_sanity_checker \
|
||||
document_fuzzy_match \
|
||||
manage_superuser;
|
||||
do
|
||||
echo "installing $command..."
|
||||
|
@@ -1,14 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SUPERVISORD_WORKING_DIR="${PAPERLESS_SUPERVISORD_WORKING_DIR:-$PWD}"
|
||||
rootless_args=()
|
||||
if [ "$(id -u)" == "$(id -u paperless)" ]; then
|
||||
rootless_args=(
|
||||
--user
|
||||
paperless
|
||||
--logfile
|
||||
supervisord.log
|
||||
"${SUPERVISORD_WORKING_DIR}/supervisord.log"
|
||||
--pidfile
|
||||
supervisord.pid
|
||||
"${SUPERVISORD_WORKING_DIR}/supervisord.pid"
|
||||
)
|
||||
fi
|
||||
|
||||
|
@@ -15,6 +15,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:consumer]
|
||||
command=python3 manage.py document_consumer
|
||||
@@ -25,6 +26,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:celery]
|
||||
|
||||
@@ -37,6 +39,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:celery-beat]
|
||||
|
||||
@@ -48,6 +51,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:celery-flower]
|
||||
command = /usr/local/bin/flower-conditional.sh
|
||||
@@ -58,3 +62,4 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
@@ -4,6 +4,7 @@ Simple script which attempts to ping the Redis broker as set in the environment
|
||||
a certain number of times, waiting a little bit in between
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
@@ -12,7 +13,6 @@ from typing import Final
|
||||
from redis import Redis
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
MAX_RETRY_COUNT: Final[int] = 5
|
||||
RETRY_SLEEP_SECONDS: Final[int] = 5
|
||||
|
||||
@@ -29,7 +29,7 @@ if __name__ == "__main__":
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Redis ping #{attempt} failed.\n"
|
||||
f"Error: {str(e)}.\n"
|
||||
f"Error: {e!s}.\n"
|
||||
f"Waiting {RETRY_SLEEP_SECONDS}s",
|
||||
flush=True,
|
||||
)
|
||||
|
@@ -5,17 +5,19 @@
|
||||
Multiple options exist for making backups of your paperless instance,
|
||||
depending on how you installed paperless.
|
||||
|
||||
Before making backups, make sure that paperless is not running.
|
||||
Before making a backup, it's probably best to make sure that paperless is not actively
|
||||
consuming documents at that time.
|
||||
|
||||
Options available to any installation of paperless:
|
||||
|
||||
- Use the [document exporter](#exporter). The document exporter exports all your documents,
|
||||
thumbnails and metadata to a specific folder. You may import your
|
||||
documents into a fresh instance of paperless again or store your
|
||||
documents in another DMS with this export.
|
||||
- The document exporter is also able to update an already existing
|
||||
export. Therefore, incremental backups with `rsync` are entirely
|
||||
possible.
|
||||
- Use the [document exporter](#exporter). The document exporter exports all your documents,
|
||||
thumbnails, metadata, and database contents to a specific folder. You may import your
|
||||
documents and settings into a fresh instance of paperless again or store your
|
||||
documents in another DMS with this export.
|
||||
|
||||
The document exporter is also able to update an already existing
|
||||
export. Therefore, incremental backups with `rsync` are entirely
|
||||
possible.
|
||||
|
||||
!!! caution
|
||||
|
||||
@@ -25,31 +27,37 @@ Options available to any installation of paperless:
|
||||
|
||||
Options available to docker installations:
|
||||
|
||||
- Backup the docker volumes. These usually reside within
|
||||
`/var/lib/docker/volumes` on the host and you need to be root in
|
||||
order to access them.
|
||||
- Backup the docker volumes. These usually reside within
|
||||
`/var/lib/docker/volumes` on the host and you need to be root in
|
||||
order to access them.
|
||||
|
||||
Paperless uses 4 volumes:
|
||||
Paperless uses 4 volumes:
|
||||
|
||||
- `paperless_media`: This is where your documents are stored.
|
||||
- `paperless_data`: This is where auxillary data is stored. This
|
||||
folder also contains the SQLite database, if you use it.
|
||||
- `paperless_pgdata`: Exists only if you use PostgreSQL and
|
||||
contains the database.
|
||||
- `paperless_dbdata`: Exists only if you use MariaDB and contains
|
||||
the database.
|
||||
- `paperless_media`: This is where your documents are stored.
|
||||
- `paperless_data`: This is where auxiliary data is stored. This
|
||||
folder also contains the SQLite database, if you use it.
|
||||
- `paperless_pgdata`: Exists only if you use PostgreSQL and
|
||||
contains the database.
|
||||
- `paperless_dbdata`: Exists only if you use MariaDB and contains
|
||||
the database.
|
||||
|
||||
Options available to bare-metal and non-docker installations:
|
||||
|
||||
- Backup the entire paperless folder. This ensures that if your
|
||||
paperless instance crashes at some point or your disk fails, you can
|
||||
simply copy the folder back into place and it works.
|
||||
- Backup the entire paperless folder. This ensures that if your
|
||||
paperless instance crashes at some point or your disk fails, you can
|
||||
simply copy the folder back into place and it works.
|
||||
|
||||
When using PostgreSQL or MariaDB, you'll also have to backup the
|
||||
database.
|
||||
When using PostgreSQL or MariaDB, you'll also have to backup the
|
||||
database.
|
||||
|
||||
### Restoring {#migrating-restoring}
|
||||
|
||||
If you've backed-up Paperless-ngx using the [document exporter](#exporter),
|
||||
restoring can simply be done with the [document importer](#importer).
|
||||
|
||||
Of course, other backup strategies require restoring any volumes, folders and database
|
||||
copies you created in the steps above.
|
||||
|
||||
## Updating Paperless {#updating}
|
||||
|
||||
### Docker Route {#docker-updating}
|
||||
@@ -59,34 +67,34 @@ you installed paperless-ngx in the first place. The releases are
|
||||
available at the [release
|
||||
page](https://github.com/paperless-ngx/paperless-ngx/releases).
|
||||
|
||||
First of all, ensure that paperless is stopped.
|
||||
First of all, make sure no active processes (like consumption) are running, then [make a backup](#backup).
|
||||
|
||||
After that, ensure that paperless is stopped:
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless
|
||||
$ docker-compose down
|
||||
$ docker compose down
|
||||
```
|
||||
|
||||
After that, [make a backup](#backup).
|
||||
1. If you pull the image from the docker hub, all you need to do is:
|
||||
|
||||
1. If you pull the image from the docker hub, all you need to do is:
|
||||
```shell-session
|
||||
$ docker compose pull
|
||||
$ docker compose up
|
||||
```
|
||||
|
||||
```shell-session
|
||||
$ docker-compose pull
|
||||
$ docker-compose up
|
||||
```
|
||||
The Docker Compose files refer to the `latest` version, which is
|
||||
always the latest stable release.
|
||||
|
||||
The docker-compose files refer to the `latest` version, which is
|
||||
always the latest stable release.
|
||||
1. If you built the image yourself, do the following:
|
||||
|
||||
2. If you built the image yourself, do the following:
|
||||
```shell-session
|
||||
$ git pull
|
||||
$ docker compose build
|
||||
$ docker compose up
|
||||
```
|
||||
|
||||
```shell-session
|
||||
$ git pull
|
||||
$ docker-compose build
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
Running `docker-compose up` will also apply any new database migrations.
|
||||
Running `docker compose up` will also apply any new database migrations.
|
||||
If you see everything working, press CTRL+C once to gracefully stop
|
||||
paperless. Then you can start paperless-ngx with `-d` to have it run in
|
||||
the background.
|
||||
@@ -94,7 +102,7 @@ the background.
|
||||
!!! note
|
||||
|
||||
In version 0.9.14, the update process was changed. In 0.9.13 and
|
||||
earlier, the docker-compose files specified exact versions and pull
|
||||
earlier, the Docker Compose files specified exact versions and pull
|
||||
won't automatically update to newer versions. In order to enable
|
||||
updates as described above, either get the new `docker-compose.yml`
|
||||
file from
|
||||
@@ -139,7 +147,7 @@ following:
|
||||
1. Update dependencies. New paperless version may require additional
|
||||
dependencies. The dependencies required are listed in the section
|
||||
about
|
||||
[bare metal installations](/setup#bare_metal).
|
||||
[bare metal installations](setup.md#bare_metal).
|
||||
|
||||
2. Update python requirements. Keep in mind to activate your virtual
|
||||
environment before that, if you use one.
|
||||
@@ -148,6 +156,13 @@ following:
|
||||
$ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
At times, some dependencies will be removed from requirements.txt.
|
||||
Comparing the versions and removing no longer needed dependencies
|
||||
will keep your system or virtual environment clean and prevent
|
||||
possible conflicts.
|
||||
|
||||
3. Migrate the database.
|
||||
|
||||
```shell-session
|
||||
@@ -160,34 +175,22 @@ following:
|
||||
This might not actually do anything. Not every new paperless version
|
||||
comes with new database migrations.
|
||||
|
||||
## Downgrading Paperless {#downgrade-paperless}
|
||||
### Database Upgrades
|
||||
|
||||
Downgrades are possible. However, some updates also contain database
|
||||
migrations (these change the layout of the database and may move data).
|
||||
In order to move back from a version that applied database migrations,
|
||||
you'll have to revert the database migration _before_ downgrading, and
|
||||
then downgrade paperless.
|
||||
In general, paperless does not require a specific version of PostgreSQL or MariaDB and it is
|
||||
safe to update them to newer versions. However, you should always take a backup and follow
|
||||
the instructions from your database's documentation for how to upgrade between major versions.
|
||||
|
||||
This table lists the compatible versions for each database migration
|
||||
number.
|
||||
For PostgreSQL, refer to [Upgrading a PostgreSQL Cluster](https://www.postgresql.org/docs/current/upgrading.html).
|
||||
|
||||
| Migration number | Version range |
|
||||
| ---------------- | --------------- |
|
||||
| 1011 | 1.0.0 |
|
||||
| 1012 | 1.1.0 - 1.2.1 |
|
||||
| 1014 | 1.3.0 - 1.3.1 |
|
||||
| 1016 | 1.3.2 - current |
|
||||
For MariaDB, refer to [Upgrading MariaDB](https://mariadb.com/kb/en/upgrading/)
|
||||
|
||||
Execute the following management command to migrate your database:
|
||||
You may also use the exporter and importer with the `--data-only` flag, after creating a new database with the updated version of PostgreSQL or MariaDB.
|
||||
|
||||
```shell-session
|
||||
$ python3 manage.py migrate documents <migration number>
|
||||
```
|
||||
!!! warning
|
||||
|
||||
!!! note
|
||||
|
||||
Some migrations cannot be undone. The command will issue errors if that
|
||||
happens.
|
||||
You should not change any settings, especially paths, when doing this or there is a
|
||||
risk of data loss
|
||||
|
||||
## Management utilities {#management-commands}
|
||||
|
||||
@@ -195,11 +198,11 @@ Paperless comes with some management commands that perform various
|
||||
maintenance tasks on your paperless instance. You can invoke these
|
||||
commands in the following way:
|
||||
|
||||
With docker-compose, while paperless is running:
|
||||
With Docker Compose, while paperless is running:
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless
|
||||
$ docker-compose exec webserver <command> <arguments>
|
||||
$ docker compose exec webserver <command> <arguments>
|
||||
```
|
||||
|
||||
With docker, while paperless is running:
|
||||
@@ -222,26 +225,31 @@ with the argument `--help`.
|
||||
|
||||
### Document exporter {#exporter}
|
||||
|
||||
The document exporter exports all your data from paperless into a folder
|
||||
for backup or migration to another DMS.
|
||||
The document exporter exports all your data (including your settings
|
||||
and database contents) from paperless into a folder for backup or
|
||||
migration to another DMS.
|
||||
|
||||
If you use the document exporter within a cronjob to backup your data
|
||||
you might use the `-T` flag behind exec to suppress "The input device
|
||||
is not a TTY" errors. For example:
|
||||
`docker-compose exec -T webserver document_exporter ../export`
|
||||
`docker compose exec -T webserver document_exporter ../export`
|
||||
|
||||
```
|
||||
document_exporter target [-c] [-d] [-f] [-na] [-nt] [-p] [-sm] [-z]
|
||||
|
||||
optional arguments:
|
||||
-c, --compare-checksums
|
||||
-d, --delete
|
||||
-f, --use-filename-format
|
||||
-c, --compare-checksums
|
||||
-d, --delete
|
||||
-f, --use-filename-format
|
||||
-na, --no-archive
|
||||
-nt, --no-thumbnail
|
||||
-p, --use-folder-prefix
|
||||
-p, --use-folder-prefix
|
||||
-sm, --split-manifest
|
||||
-z --zip
|
||||
-z, --zip
|
||||
-zn, --zip-name
|
||||
--data-only
|
||||
--no-progress-bar
|
||||
--passphrase
|
||||
```
|
||||
|
||||
`target` is a folder to which the data gets written. This includes
|
||||
@@ -269,7 +277,7 @@ other files.
|
||||
|
||||
The filenames generated by this command follow the format
|
||||
`[date created] [correspondent] [title].[extension]`. If you want
|
||||
paperless to use `PAPERLESS_FILENAME_FORMAT` for exported filenames
|
||||
paperless to use [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) for exported filenames
|
||||
instead, specify `-f` or `--use-filename-format`.
|
||||
|
||||
If `-na` or `--no-archive` is provided, no archive files will be exported,
|
||||
@@ -296,8 +304,19 @@ will be placed in individual json files, instead of a single JSON file. The main
|
||||
manifest.json will still contain application wide information (e.g. tags, correspondent,
|
||||
documenttype, etc)
|
||||
|
||||
If `-z` or `--zip` is provided, the export will be a zipfile
|
||||
in the target directory, named according to the current date.
|
||||
If `-z` or `--zip` is provided, the export will be a zip file
|
||||
in the target directory, named according to the current local date or the
|
||||
value set in `-zn` or `--zip-name`.
|
||||
|
||||
If `--data-only` is provided, only the database will be exported. This option is intended
|
||||
to facilitate database upgrades without needing to clean documents and thumbnails from the media directory.
|
||||
|
||||
If `--no-progress-bar` is provided, the progress bar will be hidden, rendering the
|
||||
exporter quiet. This option is useful for scripting scenarios, such as when using the
|
||||
exporter with `crontab`.
|
||||
|
||||
If `--passphrase` is provided, it will be used to encrypt certain fields in the export. This value
|
||||
must be provided to import. If this value is lost, the export cannot be imported.
|
||||
|
||||
!!! warning
|
||||
|
||||
@@ -313,19 +332,34 @@ exporter](#exporter) and imports it into paperless.
|
||||
The importer works just like the exporter. You point it at a directory,
|
||||
and the script does the rest of the work:
|
||||
|
||||
```
|
||||
```shell
|
||||
document_importer source
|
||||
```
|
||||
|
||||
| Option | Required | Default | Description |
|
||||
| ------------------- | -------- | ------- | ------------------------------------------------------------------------- |
|
||||
| source | Yes | N/A | The directory containing an export |
|
||||
| `--no-progress-bar` | No | False | If provided, the progress bar will be hidden |
|
||||
| `--data-only` | No | False | If provided, only import data, do not import document files or thumbnails |
|
||||
| `--passphrase` | No | N/A | If your export was encrypted with a passphrase, must be provided |
|
||||
|
||||
When you use the provided docker compose script, put the export inside
|
||||
the `export` folder in your paperless source directory. Specify
|
||||
`../export` as the `source`.
|
||||
|
||||
Note that .zip files (as can be generated from the exporter) are not supported. You must unzip them into
|
||||
the target directory first.
|
||||
|
||||
!!! note
|
||||
|
||||
Importing from a previous version of Paperless may work, but for best
|
||||
results it is suggested to match the versions.
|
||||
|
||||
!!! warning
|
||||
|
||||
The importer should be run against a completely empty installation (database and directories) of Paperless-ngx.
|
||||
If using a data only import, only the database must be empty.
|
||||
|
||||
### Document retagger {#retagger}
|
||||
|
||||
Say you've imported a few hundred documents and now want to introduce a
|
||||
@@ -334,7 +368,7 @@ currently-imported docs. This problem is common enough that there are
|
||||
tools for it.
|
||||
|
||||
```
|
||||
document_retagger [-h] [-c] [-T] [-t] [-i] [--use-first] [-f]
|
||||
document_retagger [-h] [-c] [-T] [-t] [-i] [--id-range] [--use-first] [-f]
|
||||
|
||||
optional arguments:
|
||||
-c, --correspondent
|
||||
@@ -342,6 +376,7 @@ optional arguments:
|
||||
-t, --document_type
|
||||
-s, --storage_path
|
||||
-i, --inbox-only
|
||||
--id-range
|
||||
--use-first
|
||||
-f, --overwrite
|
||||
```
|
||||
@@ -358,6 +393,11 @@ Specify `-i` to have the document retagger work on documents tagged with
|
||||
inbox tags only. This is useful when you don't want to mess with your
|
||||
already processed documents.
|
||||
|
||||
Specify `--id-range 1 100` to have the document retagger work only on a
|
||||
specific range of document id´s. This can be useful if you have a lot of
|
||||
documents and want to test the matching rules only on a subset of
|
||||
documents.
|
||||
|
||||
When multiple document types or correspondents match a single document,
|
||||
the retagger won't assign these to the document. Specify `--use-first`
|
||||
to override this behavior and just use the first correspondent or type
|
||||
@@ -374,7 +414,7 @@ that don't match a document anymore get removed as well.
|
||||
### Managing the Automatic matching algorithm
|
||||
|
||||
The _Auto_ matching algorithm requires a trained neural network to work.
|
||||
This network needs to be updated whenever somethings in your data
|
||||
This network needs to be updated whenever something in your data
|
||||
changes. The docker image takes care of that automatically with the task
|
||||
scheduler. You can manually renew the classifier by invoking the
|
||||
following management command:
|
||||
@@ -389,6 +429,9 @@ This command takes no arguments.
|
||||
|
||||
Use this command to re-create document thumbnails. Optionally include the ` --document {id}` option to generate thumbnails for a specific document only.
|
||||
|
||||
You may also specify `--processes` to control the number of processes used to generate new thumbnails. The default is to utilize
|
||||
a quarter of the available processors.
|
||||
|
||||
```
|
||||
document_thumbnails
|
||||
```
|
||||
@@ -416,7 +459,7 @@ task scheduler.
|
||||
### Managing filenames {#renamer}
|
||||
|
||||
If you use paperless' feature to
|
||||
[assign custom filenames to your documents](/advanced_usage#file-name-handling), you can use this command to move all your files after
|
||||
[assign custom filenames to your documents](advanced_usage.md#file-name-handling), you can use this command to move all your files after
|
||||
changing the naming scheme.
|
||||
|
||||
!!! warning
|
||||
@@ -441,19 +484,19 @@ collection for issues.
|
||||
|
||||
The issues detected by the sanity checker are as follows:
|
||||
|
||||
- Missing original files.
|
||||
- Missing archive files.
|
||||
- Inaccessible original files due to improper permissions.
|
||||
- Inaccessible archive files due to improper permissions.
|
||||
- Corrupted original documents by comparing their checksum against
|
||||
what is stored in the database.
|
||||
- Corrupted archive documents by comparing their checksum against what
|
||||
is stored in the database.
|
||||
- Missing thumbnails.
|
||||
- Inaccessible thumbnails due to improper permissions.
|
||||
- Documents without any content (warning).
|
||||
- Orphaned files in the media directory (warning). These are files
|
||||
that are not referenced by any document im paperless.
|
||||
- Missing original files.
|
||||
- Missing archive files.
|
||||
- Inaccessible original files due to improper permissions.
|
||||
- Inaccessible archive files due to improper permissions.
|
||||
- Corrupted original documents by comparing their checksum against
|
||||
what is stored in the database.
|
||||
- Corrupted archive documents by comparing their checksum against what
|
||||
is stored in the database.
|
||||
- Missing thumbnails.
|
||||
- Inaccessible thumbnails due to improper permissions.
|
||||
- Documents without any content (warning).
|
||||
- Orphaned files in the media directory (warning). These are files
|
||||
that are not referenced by any document in paperless.
|
||||
|
||||
```
|
||||
document_sanity_checker
|
||||
@@ -522,7 +565,7 @@ Documents can be stored in Paperless using GnuPG encryption.
|
||||
|
||||
!!! warning
|
||||
|
||||
Encryption is deprecated since [paperless-ng 0.9](/changelog#paperless-ng-090) and doesn't really
|
||||
Encryption is deprecated since [paperless-ng 0.9](changelog.md#paperless-ng-090) and doesn't really
|
||||
provide any additional security, since you have to store the passphrase
|
||||
in a configuration file on the same system as the encrypted documents
|
||||
for paperless to work. Furthermore, the entire text content of the
|
||||
@@ -549,3 +592,31 @@ it here)
|
||||
```
|
||||
decrypt_documents [--passphrase SECR3TP4SSPHRA$E]
|
||||
```
|
||||
|
||||
### Detecting duplicates {#fuzzy_duplicate}
|
||||
|
||||
Paperless already catches and prevents upload of exactly matching documents,
|
||||
however a new scan of an existing document may not produce an exact bit for bit
|
||||
duplicate. But the content should be exact or close, allowing detection.
|
||||
|
||||
This tool does a fuzzy match over document content, looking for
|
||||
those which look close according to a given ratio.
|
||||
|
||||
At this time, other metadata (such as correspondent or type) is not
|
||||
taken into account by the detection.
|
||||
|
||||
```
|
||||
document_fuzzy_match [--ratio] [--processes N]
|
||||
```
|
||||
|
||||
| Option | Required | Default | Description |
|
||||
| ----------- | -------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| --ratio | No | 85.0 | a number between 0 and 100, setting how similar a document must be for it to be reported. Higher numbers mean more similarity. |
|
||||
| --processes | No | 1/4 of system cores | Number of processes to use for matching. Setting 1 disables multiple processes |
|
||||
| --delete | No | False | If provided, one document of a matched pair above the ratio will be deleted. |
|
||||
|
||||
!!! warning
|
||||
|
||||
If providing the `--delete` option, it is highly recommended to have a backup.
|
||||
While every effort has been taken to ensure proper operation, there is always the
|
||||
chance of deletion of a file you want to keep.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Advanced Topics
|
||||
|
||||
Paperless offers a couple features that automate certain tasks and make
|
||||
Paperless offers a couple of features that automate certain tasks and make
|
||||
your life easier.
|
||||
|
||||
## Matching tags, correspondents, document types, and storage paths {#matching}
|
||||
@@ -35,9 +35,10 @@ The following algorithms are available:
|
||||
(i.e. preserve ordering) in the PDF.
|
||||
- **Regular expression:** Parses the match as a regular expression and
|
||||
tries to find a match within the document.
|
||||
- **Fuzzy match:** I don't know. Look at the source.
|
||||
- **Fuzzy match:** Uses a partial matching based on locating the tag text
|
||||
inside the document, using a [partial ratio](https://rapidfuzz.github.io/RapidFuzz/Usage/fuzz.html#partial-ratio)
|
||||
- **Auto:** Tries to automatically match new documents. This does not
|
||||
require you to set a match. See the notes below.
|
||||
require you to set a match. See the [notes below](#automatic-matching).
|
||||
|
||||
When using the _any_ or _all_ matching algorithms, you can search for
|
||||
terms that consist of multiple words by enclosing them in double quotes.
|
||||
@@ -92,7 +93,7 @@ when using this feature:
|
||||
decide when not to assign a certain tag, correspondent, document
|
||||
type, or storage path. This will usually be the case as you start
|
||||
filling up paperless with documents. Example: If all your documents
|
||||
are either from "Webshop" and "Bank", paperless will assign one
|
||||
are either from "Webshop" or "Bank", paperless will assign one
|
||||
of these correspondents to ANY new document, if both are set to
|
||||
automatic matching.
|
||||
|
||||
@@ -101,12 +102,12 @@ when using this feature:
|
||||
Sometimes you may want to do something arbitrary whenever a document is
|
||||
consumed. Rather than try to predict what you may want to do, Paperless
|
||||
lets you execute scripts of your own choosing just before or after a
|
||||
document is consumed using a couple simple hooks.
|
||||
document is consumed using a couple of simple hooks.
|
||||
|
||||
Just write a script, put it somewhere that Paperless can read & execute,
|
||||
and then put the path to that script in `paperless.conf` or
|
||||
`docker-compose.env` with the variable name of either
|
||||
`PAPERLESS_PRE_CONSUME_SCRIPT` or `PAPERLESS_POST_CONSUME_SCRIPT`.
|
||||
[`PAPERLESS_PRE_CONSUME_SCRIPT`](configuration.md#PAPERLESS_PRE_CONSUME_SCRIPT) or [`PAPERLESS_POST_CONSUME_SCRIPT`](configuration.md#PAPERLESS_POST_CONSUME_SCRIPT).
|
||||
|
||||
!!! info
|
||||
|
||||
@@ -126,6 +127,7 @@ script can access the following relevant environment variables set:
|
||||
| ----------------------- | ------------------------------------------------------------ |
|
||||
| `DOCUMENT_SOURCE_PATH` | Original path of the consumed document |
|
||||
| `DOCUMENT_WORKING_PATH` | Path to a copy of the original that consumption will work on |
|
||||
| `TASK_ID` | UUID of the task used to process the new document (if any) |
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -134,6 +136,11 @@ script can access the following relevant environment variables set:
|
||||
be triggered, leading to failures as two tasks work on the
|
||||
same document path
|
||||
|
||||
!!! warning
|
||||
|
||||
If your script modifies `DOCUMENT_WORKING_PATH` in a non-deterministic
|
||||
way, this may allow duplicate documents to be stored
|
||||
|
||||
A simple but common example for this would be creating a simple script
|
||||
like this:
|
||||
|
||||
@@ -168,21 +175,23 @@ Executed after the consumer has successfully processed a document and
|
||||
has moved it into paperless. It receives the following environment
|
||||
variables:
|
||||
|
||||
| Environment Variable | Description |
|
||||
| ---------------------------- | --------------------------------------------- |
|
||||
| `DOCUMENT_ID` | Database primary key of the document |
|
||||
| `DOCUMENT_FILE_NAME` | Formatted filename, not including paths |
|
||||
| `DOCUMENT_CREATED` | Date & time when document created |
|
||||
| `DOCUMENT_MODIFIED` | Date & time when document was last modified |
|
||||
| `DOCUMENT_ADDED` | Date & time when document was added |
|
||||
| `DOCUMENT_SOURCE_PATH` | Path to the original document file |
|
||||
| `DOCUMENT_ARCHIVE_PATH` | Path to the generate archive file (if any) |
|
||||
| `DOCUMENT_THUMBNAIL_PATH` | Path to the generated thumbnail |
|
||||
| `DOCUMENT_DOWNLOAD_URL` | URL for document download |
|
||||
| `DOCUMENT_THUMBNAIL_URL` | URL for the document thumbnail |
|
||||
| `DOCUMENT_CORRESPONDENT` | Assigned correspondent (if any) |
|
||||
| `DOCUMENT_TAGS` | Comma separated list of tags applied (if any) |
|
||||
| `DOCUMENT_ORIGINAL_FILENAME` | Filename of original document |
|
||||
| Environment Variable | Description |
|
||||
| ---------------------------- | ---------------------------------------------- |
|
||||
| `DOCUMENT_ID` | Database primary key of the document |
|
||||
| `DOCUMENT_FILE_NAME` | Formatted filename, not including paths |
|
||||
| `DOCUMENT_CREATED` | Date & time when document created |
|
||||
| `DOCUMENT_MODIFIED` | Date & time when document was last modified |
|
||||
| `DOCUMENT_ADDED` | Date & time when document was added |
|
||||
| `DOCUMENT_SOURCE_PATH` | Path to the original document file |
|
||||
| `DOCUMENT_ARCHIVE_PATH` | Path to the generate archive file (if any) |
|
||||
| `DOCUMENT_THUMBNAIL_PATH` | Path to the generated thumbnail |
|
||||
| `DOCUMENT_DOWNLOAD_URL` | URL for document download |
|
||||
| `DOCUMENT_THUMBNAIL_URL` | URL for the document thumbnail |
|
||||
| `DOCUMENT_OWNER` | Username of the document owner (if any) |
|
||||
| `DOCUMENT_CORRESPONDENT` | Assigned correspondent (if any) |
|
||||
| `DOCUMENT_TAGS` | Comma separated list of tags applied (if any) |
|
||||
| `DOCUMENT_ORIGINAL_FILENAME` | Filename of original document |
|
||||
| `TASK_ID` | Task UUID used to import the document (if any) |
|
||||
|
||||
The script can be in any language, A simple shell script example:
|
||||
|
||||
@@ -197,7 +206,7 @@ The script can be in any language, A simple shell script example:
|
||||
!!! warning
|
||||
|
||||
The post consumption script should not modify the document files
|
||||
directly
|
||||
directly.
|
||||
|
||||
The script's stdout and stderr will be logged line by line to the
|
||||
webserver log, along with the exit code of the script.
|
||||
@@ -233,8 +242,8 @@ webserver:
|
||||
|
||||
Troubleshooting:
|
||||
|
||||
- Monitor the docker-compose log
|
||||
`cd ~/paperless-ngx; docker-compose logs -f`
|
||||
- Monitor the Docker Compose log
|
||||
`cd ~/paperless-ngx; docker compose logs -f`
|
||||
- Check your script's permission e.g. in case of permission error
|
||||
`sudo chmod 755 post-consumption-example.sh`
|
||||
- Pipe your scripts's output to a log file e.g.
|
||||
@@ -248,7 +257,8 @@ document. You will end up getting files like `0000123.pdf` in your media
|
||||
directory. This isn't necessarily a bad thing, because you normally
|
||||
don't have to access these files manually. However, if you wish to name
|
||||
your files differently, you can do that by adjusting the
|
||||
`PAPERLESS_FILENAME_FORMAT` configuration option. Paperless adds the
|
||||
[`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) configuration option
|
||||
or using [storage paths (see below)](#storage-paths). Paperless adds the
|
||||
correct file extension e.g. `.pdf`, `.jpg` automatically.
|
||||
|
||||
This variable allows you to configure the filename (folders are allowed)
|
||||
@@ -281,6 +291,15 @@ will create a directory structure as follows:
|
||||
paperless will report your files as missing and won't be able to find
|
||||
them.
|
||||
|
||||
!!! tip
|
||||
|
||||
Paperless checks the filename of a document whenever it is saved. Changing (or deleting)
|
||||
a [storage path](#storage-paths) will automatically be reflected in the file system. However,
|
||||
when changing `PAPERLESS_FILENAME_FORMAT` you will need to manually run the
|
||||
[`document renamer`](administration.md#renamer) to move any existing documents.
|
||||
|
||||
#### Placeholders
|
||||
|
||||
Paperless provides the following placeholders within filenames:
|
||||
|
||||
- `{asn}`: The archive serial number of the document, or "none".
|
||||
@@ -311,6 +330,13 @@ Paperless provides the following placeholders within filenames:
|
||||
- `{added_day}`: Day added only (number 01-31).
|
||||
- `{owner_username}`: Username of document owner, if any, or "none"
|
||||
- `{original_name}`: Document original filename, minus the extension, if any, or "none"
|
||||
- `{doc_pk}`: The paperless identifier (primary key) for the document.
|
||||
|
||||
!!! warning
|
||||
|
||||
When using file name placeholders, in particular when using `{tag_list}`,
|
||||
you may run into the limits of your operating system's maximum path lengths.
|
||||
In that case, files will retain the previous path instead and the issue logged.
|
||||
|
||||
Paperless will try to conserve the information from your database as
|
||||
much as possible. However, some characters that you can use in document
|
||||
@@ -322,34 +348,12 @@ paperless will automatically append `_01`, `_02`, etc to the filename.
|
||||
This happens if all the placeholders in a filename evaluate to the same
|
||||
value.
|
||||
|
||||
!!! tip
|
||||
|
||||
You can affect how empty placeholders are treated by changing the
|
||||
following setting to `true`.
|
||||
|
||||
```
|
||||
PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=True
|
||||
```
|
||||
|
||||
Doing this results in all empty placeholders resolving to "" instead
|
||||
of "none" as stated above. Spaces before empty placeholders are
|
||||
removed as well, empty directories are omitted.
|
||||
|
||||
!!! tip
|
||||
|
||||
Paperless checks the filename of a document whenever it is saved.
|
||||
Therefore, you need to update the filenames of your documents and move
|
||||
them after altering this setting by invoking the
|
||||
[`document renamer`](/administration#renamer).
|
||||
|
||||
!!! warning
|
||||
|
||||
Make absolutely sure you get the spelling of the placeholders right, or
|
||||
else paperless will use the default naming scheme instead.
|
||||
If there are any errors in the placeholders included in `PAPERLESS_FILENAME_FORMAT`,
|
||||
paperless will fall back to using the default naming scheme instead.
|
||||
|
||||
!!! caution
|
||||
|
||||
As of now, you could totally tell paperless to store your files anywhere
|
||||
As of now, you could potentially tell paperless to store your files anywhere
|
||||
outside the media directory by setting
|
||||
|
||||
```
|
||||
@@ -357,28 +361,25 @@ value.
|
||||
```
|
||||
|
||||
However, keep in mind that inside docker, if files get stored outside of
|
||||
the predefined volumes, they will be lost after a restart of paperless.
|
||||
the predefined volumes, they will be lost after a restart.
|
||||
|
||||
!!! warning
|
||||
##### Empty placeholders
|
||||
|
||||
When file naming handling, in particular when using `{tag_list}`,
|
||||
you may run into the limits of your operating system's maximum
|
||||
path lengths. Files will retain the previous path instead and
|
||||
the issue logged.
|
||||
You can affect how empty placeholders are treated by changing the
|
||||
[`PAPERLESS_FILENAME_FORMAT_REMOVE_NONE`](configuration.md#PAPERLESS_FILENAME_FORMAT_REMOVE_NONE) setting.
|
||||
|
||||
## Storage paths
|
||||
Enabling this results in all empty placeholders resolving to "" instead of "none" as stated above. Spaces
|
||||
before empty placeholders are removed as well, empty directories are omitted.
|
||||
|
||||
One of the best things in Paperless is that you can not only access the
|
||||
documents via the web interface, but also via the file system.
|
||||
### Storage paths
|
||||
|
||||
When a single storage layout is not sufficient for your use case,
|
||||
storage paths come to the rescue. Storage paths allow you to configure
|
||||
more precisely where each document is stored in the file system.
|
||||
When a single storage layout is not sufficient for your use case, storage paths allow for more complex
|
||||
structure to set precisely where each document is stored in the file system.
|
||||
|
||||
- Each storage path is a `PAPERLESS_FILENAME_FORMAT` and
|
||||
- Each storage path is a [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) and
|
||||
follows the rules described above
|
||||
- Each document is assigned a storage path using the matching
|
||||
algorithms described above, but can be overwritten at any time
|
||||
- Each document is assigned a storage path using the matching algorithms described above, but can be
|
||||
overwritten at any time
|
||||
|
||||
For example, you could define the following two storage paths:
|
||||
|
||||
@@ -403,7 +404,7 @@ structure as in the previous example above.
|
||||
Statement January.pdf
|
||||
Statement February.pdf
|
||||
|
||||
Insurances/ # Insurances
|
||||
Insurances/ # Insurances
|
||||
Healthcare 123/
|
||||
2022-01-01 Statement January.pdf
|
||||
2022-02-02 Letter.pdf
|
||||
@@ -415,7 +416,7 @@ structure as in the previous example above.
|
||||
!!! tip
|
||||
|
||||
Defining a storage path is optional. If no storage path is defined for a
|
||||
document, the global `PAPERLESS_FILENAME_FORMAT` is applied.
|
||||
document, the global [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) is applied.
|
||||
|
||||
## Celery Monitoring {#celery-monitoring}
|
||||
|
||||
@@ -425,8 +426,10 @@ to view more detailed information about the health of the celery workers
|
||||
used for asynchronous tasks. This includes details on currently running,
|
||||
queued and completed tasks, timing and more. Flower can also be used
|
||||
with Prometheus, as it exports metrics. For details on its capabilities,
|
||||
refer to the Flower documentation.
|
||||
refer to the [Flower](https://flower.readthedocs.io/en/latest/index.html)
|
||||
documentation.
|
||||
|
||||
Flower can be enabled with the setting [PAPERLESS_ENABLE_FLOWER](configuration.md#PAPERLESS_ENABLE_FLOWER).
|
||||
To configure Flower further, create a `flowerconfig.py` and
|
||||
place it into the `src/paperless` directory. For a Docker
|
||||
installation, you can use volumes to accomplish this:
|
||||
@@ -435,6 +438,8 @@ installation, you can use volumes to accomplish this:
|
||||
services:
|
||||
# ...
|
||||
webserver:
|
||||
environment:
|
||||
- PAPERLESS_ENABLE_FLOWER
|
||||
ports:
|
||||
- 5555:5555 # (2)!
|
||||
# ...
|
||||
@@ -443,7 +448,7 @@ services:
|
||||
```
|
||||
|
||||
1. Note the `:ro` tag means the file will be mounted as read only.
|
||||
2. `flower` runs by default on port 5555, but this can be configured
|
||||
2. By default, Flower runs on port 5555, but this can be configured.
|
||||
|
||||
## Custom Container Initialization
|
||||
|
||||
@@ -488,7 +493,7 @@ database to be case sensitive. This would prevent a user from creating a
|
||||
tag `Name` and `NAME` as they are considered the same.
|
||||
|
||||
Per Django documentation, to enable this requires manual intervention.
|
||||
To enable case sensetive tables, you can execute the following command
|
||||
To enable case sensitive tables, you can execute the following command
|
||||
against each table:
|
||||
|
||||
`ALTER TABLE <table_name> CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`
|
||||
@@ -504,11 +509,23 @@ existing tables) with:
|
||||
an older system may fix issues that can arise while setting up Paperless-ngx but
|
||||
`utf8mb3` can cause issues with consumption (where `utf8mb4` does not).
|
||||
|
||||
### Missing timezones
|
||||
|
||||
MySQL as well as MariaDB do not have any timezone information by default (though some
|
||||
docker images such as the official MariaDB image take care of this for you) which will
|
||||
cause unexpected behavior with date-based queries.
|
||||
|
||||
To fix this, execute one of the following commands:
|
||||
|
||||
MySQL: `mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql -p`
|
||||
|
||||
MariaDB: `mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql -p`
|
||||
|
||||
## Barcodes {#barcodes}
|
||||
|
||||
Paperless is able to utilize barcodes for automatically preforming some tasks.
|
||||
Paperless is able to utilize barcodes for automatically performing some tasks.
|
||||
|
||||
At this time, the library utilized for detection of bacodes supports the following types:
|
||||
At this time, the library utilized for detection of barcodes supports the following types:
|
||||
|
||||
- AN-13/UPC-A
|
||||
- UPC-E
|
||||
@@ -524,11 +541,11 @@ At this time, the library utilized for detection of bacodes supports the followi
|
||||
You may check for updates on the [zbar library homepage](https://github.com/mchehab/zbar).
|
||||
For usage in Paperless, the type of barcode does not matter, only the contents of it.
|
||||
|
||||
For how to enable barcode usage, see [the configuration](/configuration#barcodes).
|
||||
For how to enable barcode usage, see [the configuration](configuration.md#barcodes).
|
||||
The two settings may be enabled independently, but do have interactions as explained
|
||||
below.
|
||||
|
||||
### Document Splitting
|
||||
### Document Splitting {#document-splitting}
|
||||
|
||||
When enabled, Paperless will look for a barcode with the configured value and create a new document
|
||||
starting from the next page. The page with the barcode on it will _not_ be retained. It
|
||||
@@ -543,3 +560,187 @@ If document splitting via barcode is also enabled, documents will be split when
|
||||
barcode is located. However, differing from the splitting, the page with the
|
||||
barcode _will_ be retained. This allows application of a barcode to any page, including
|
||||
one which holds data to keep in the document.
|
||||
|
||||
### Tag Assignment
|
||||
|
||||
When enabled, Paperless will parse barcodes and attempt to interpret and assign tags.
|
||||
|
||||
See the relevant settings [`PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE`](configuration.md#PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE)
|
||||
and [`PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING`](configuration.md#PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING)
|
||||
for more information.
|
||||
|
||||
## Automatic collation of double-sided documents {#collate}
|
||||
|
||||
!!! note
|
||||
|
||||
If your scanner supports double-sided scanning natively, you do not need this feature.
|
||||
|
||||
This feature is turned off by default, see [configuration](configuration.md#collate) on how to turn it on.
|
||||
|
||||
### Summary
|
||||
|
||||
If you have a scanner with an automatic document feeder (ADF) that only scans a single side,
|
||||
this feature makes scanning double-sided documents much more convenient by automatically
|
||||
collating two separate scans into one document, reordering the pages as necessary.
|
||||
|
||||
### Usage example
|
||||
|
||||
Suppose you have a double-sided document with 6 pages (3 sheets of paper). First,
|
||||
put the stack into your ADF as normal, ensuring that page 1 is scanned first. Your ADF
|
||||
will now scan pages 1, 3, and 5. Then you (or your scanner, if it supports it) upload
|
||||
the scan into the correct sub-directory of the consume folder (`double-sided` by default;
|
||||
keep in mind that Paperless will _not_ automatically create the directory for you.)
|
||||
Paperless will then process the scan and move it into an internal staging area.
|
||||
|
||||
The next step is to turn your stack upside down (without reordering the sheets of paper),
|
||||
and scan it once again, your ADF will now scan pages 6, 4, and 2, in that order. Once this
|
||||
scan is copied into the sub-directory, Paperless will collate the previous scan with the
|
||||
new one, reversing the order of the pages on the second, "even numbered" scan. The
|
||||
resulting document will have the pages 1-6 in the correct order, and this new file will
|
||||
then be processed as normal.
|
||||
|
||||
!!! tip
|
||||
|
||||
When scanning the even numbered pages, you can omit the last empty pages, if there are
|
||||
any. For example, if page 6 is empty, you only need to scan pages 2 and 4. _Do not_ omit
|
||||
empty pages in the middle of the document.
|
||||
|
||||
### Things that could go wrong
|
||||
|
||||
Paperless will notice when the first, "odd numbered" scan has less pages than the second
|
||||
scan (this can happen when e.g. the ADF skipped a few pages in the first pass). In that
|
||||
case, Paperless will remove the staging copy as well as the scan, and give you an error
|
||||
message asking you to restart the process from scratch, by scanning the odd pages again,
|
||||
followed by the even pages.
|
||||
|
||||
It's important that the scan files get consumed in the correct order, and one at a time.
|
||||
You therefore need to make sure that Paperless is running while you upload the files into
|
||||
the directory; and if you're using [polling](configuration.md#polling), make sure that
|
||||
`CONSUMER_POLLING` is set to a value lower than it takes for the second scan to appear,
|
||||
like 5-10 or even lower.
|
||||
|
||||
Another thing that might happen is that you start a double sided scan, but then forget
|
||||
to upload the second file. To avoid collating the wrong documents if you then come back
|
||||
a day later to scan a new double-sided document, Paperless will only keep an "odd numbered
|
||||
pages" file for up to 30 minutes. If more time passes, it will consider the next incoming
|
||||
scan a completely new "odd numbered pages" one. The old staging file will get discarded.
|
||||
|
||||
### Interaction with "subdirs as tags"
|
||||
|
||||
The collation feature can be used together with the [subdirs as tags](configuration.md#consume_config)
|
||||
feature (but this is not a requirement). Just create a correctly named double-sided subdir
|
||||
in the hierarchy and upload your scans there. For example, both `double-sided/foo/bar` as
|
||||
well as `foo/bar/double-sided` will cause the collated document to be treated as if it
|
||||
were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.
|
||||
|
||||
### Interaction with document splitting
|
||||
|
||||
You can use the [document splitting](#document-splitting) feature, but if you use a normal
|
||||
single-sided split marker page, the split document(s) will have an empty page at the front (or
|
||||
whatever else was on the backside of the split marker page.) You can work around that by having
|
||||
a split marker page that has the split barcode on _both_ sides. This way, the extra page will
|
||||
get automatically removed.
|
||||
|
||||
## SSO and third party authentication with Paperless-ngx
|
||||
|
||||
Paperless-ngx has a built-in authentication system from Django but you can easily integrate an
|
||||
external authentication solution using one of the following methods:
|
||||
|
||||
### Remote User authentication
|
||||
|
||||
This is a simple option that uses remote user authentication made available by certain SSO
|
||||
applications. See the relevant configuration options for more information:
|
||||
[PAPERLESS_ENABLE_HTTP_REMOTE_USER](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER),
|
||||
[PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME](configuration.md#PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME)
|
||||
and [PAPERLESS_LOGOUT_REDIRECT_URL](configuration.md#PAPERLESS_LOGOUT_REDIRECT_URL)
|
||||
|
||||
### OpenID Connect and social authentication
|
||||
|
||||
Version 2.5.0 of Paperless-ngx added support for integrating other authentication systems via
|
||||
the [django-allauth](https://github.com/pennersr/django-allauth) package. Once set up, users
|
||||
can either log in or (optionally) sign up using any third party systems you integrate. See the
|
||||
relevant [configuration settings](configuration.md#PAPERLESS_SOCIALACCOUNT_PROVIDERS) and
|
||||
[django-allauth docs](https://docs.allauth.org/en/latest/socialaccount/configuration.html)
|
||||
for more information.
|
||||
|
||||
To associate an existing Paperless-ngx account with a social account, first login with your
|
||||
regular credentials and then choose "My Profile" from the user dropdown in the app and you
|
||||
will see options to connect social account(s). If enabled, signup options will be available
|
||||
on the login page.
|
||||
|
||||
As an example, to set up login via Github, the following environment variables would need to be
|
||||
set:
|
||||
|
||||
```conf
|
||||
PAPERLESS_APPS="allauth.socialaccount.providers.github"
|
||||
PAPERLESS_SOCIALACCOUNT_PROVIDERS='{"github": {"APPS": [{"provider_id": "github","name": "Github","client_id": "<CLIENT_ID>","secret": "<CLIENT_SECRET>"}]}}'
|
||||
```
|
||||
|
||||
Or, to use OpenID Connect ("OIDC"), via Keycloak in this example:
|
||||
|
||||
```conf
|
||||
PAPERLESS_APPS="allauth.socialaccount.providers.openid_connect"
|
||||
PAPERLESS_SOCIALACCOUNT_PROVIDERS='
|
||||
{"openid_connect": {"APPS": [{"provider_id": "keycloak","name": "Keycloak","client_id": "paperless","secret": "<CLIENT_SECRET>","settings": { "server_url": "https://<KEYCLOAK_SERVER>/realms/<REALM>/.well-known/openid-configuration"}}]}}'
|
||||
```
|
||||
|
||||
More details about configuration option for various providers can be found in the [allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html#provider-specifics).
|
||||
|
||||
### Disabling Regular Login
|
||||
|
||||
Once external auth is set up, 'regular' login can be disabled with the [PAPERLESS_DISABLE_REGULAR_LOGIN](configuration.md#PAPERLESS_DISABLE_REGULAR_LOGIN) setting and / or users can be automatically
|
||||
redirected with the [PAPERLESS_REDIRECT_LOGIN_TO_SSO](configuration.md#PAPERLESS_REDIRECT_LOGIN_TO_SSO) setting.
|
||||
|
||||
## Decryption of encrypted emails before consumption {#gpg-decryptor}
|
||||
|
||||
Paperless-ngx can be configured to decrypt gpg encrypted emails before consumption.
|
||||
|
||||
### Requirements
|
||||
|
||||
You need a recent version of `gpg-agent >= 2.1.1` installed on your host.
|
||||
Your host needs to be setup for decrypting your emails via `gpg-agent`, see this [tutorial](https://www.digitalocean.com/community/tutorials/how-to-use-gpg-to-encrypt-and-sign-messages#encrypt-and-decrypt-messages-with-gpg) for instance.
|
||||
Test your setup and make sure that you can encrypt and decrypt files using your key
|
||||
|
||||
```
|
||||
gpg --encrypt --armor -r person@email.com name_of_file
|
||||
gpg --decrypt name_of_file.asc
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
First, enable the [PAPERLESS_GPG_DECRYPTOR environment variable](configuration.md#PAPERLESS_GPG_DECRYPTOR).
|
||||
|
||||
Then determine your local `gpg-agent.extra` socket by invoking
|
||||
|
||||
```
|
||||
gpgconf --list-dir agent-extra-socket
|
||||
```
|
||||
|
||||
on your host. A possible output is `~/.gnupg/S.gpg-agent.extra`.
|
||||
Also find the location of your public keyring.
|
||||
|
||||
If using docker, you'll need to add the following volume mounts to your `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
webserver:
|
||||
volumes:
|
||||
- /home/user/.gnupg/pubring.gpg:/usr/src/paperless/.gnupg/pubring.gpg
|
||||
- <path to gpg-agent.extra socket>:/usr/src/paperless/.gnupg/S.gpg-agent
|
||||
```
|
||||
|
||||
For a 'bare-metal' installation no further configuration is necessary. If you
|
||||
want to use a separate `GNUPG_HOME`, you can do so by configuring the [PAPERLESS_EMAIL_GNUPG_HOME environment variable](configuration.md#PAPERLESS_EMAIL_GNUPG_HOME).
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- Make sure, that `gpg-agent` is running on your host machine
|
||||
- Make sure, that encryption and decryption works from inside the container using the `gpg` commands from above.
|
||||
- Check that all files in `/usr/src/paperless/.gnupg` have correct permissions
|
||||
|
||||
```shell
|
||||
paperless@9da1865df327:~/.gnupg$ ls -al
|
||||
drwx------ 1 paperless paperless 4096 Aug 18 17:52 .
|
||||
drwxr-xr-x 1 paperless paperless 4096 Aug 18 17:52 ..
|
||||
srw------- 1 paperless paperless 0 Aug 18 17:22 S.gpg-agent
|
||||
-rw------- 1 paperless paperless 147940 Jul 24 10:23 pubring.gpg
|
||||
```
|
||||
|
226
docs/api.md
@@ -6,22 +6,29 @@ provides a browsable API for most of its endpoints, which you can
|
||||
inspect at `http://<paperless-host>:<port>/api/`. This also documents
|
||||
most of the available filters and ordering fields.
|
||||
|
||||
The API provides 7 main endpoints:
|
||||
The API provides the following main endpoints:
|
||||
|
||||
- `/api/documents/`: Full CRUD support, except POSTing new documents.
|
||||
See below.
|
||||
- `/api/correspondents/`: Full CRUD support.
|
||||
- `/api/custom_fields/`: Full CRUD support.
|
||||
- `/api/documents/`: Full CRUD support, except POSTing new documents.
|
||||
See [below](#file-uploads).
|
||||
- `/api/document_types/`: Full CRUD support.
|
||||
- `/api/groups/`: Full CRUD support.
|
||||
- `/api/logs/`: Read-Only.
|
||||
- `/api/tags/`: Full CRUD support.
|
||||
- `/api/mail_accounts/`: Full CRUD support.
|
||||
- `/api/mail_rules/`: Full CRUD support.
|
||||
- `/api/profile/`: GET, PATCH
|
||||
- `/api/share_links/`: Full CRUD support.
|
||||
- `/api/storage_paths/`: Full CRUD support.
|
||||
- `/api/tags/`: Full CRUD support.
|
||||
- `/api/tasks/`: Read-only.
|
||||
- `/api/users/`: Full CRUD support.
|
||||
- `/api/groups/`: Full CRUD support.
|
||||
- `/api/workflows/`: Full CRUD support.
|
||||
- `/api/search/` GET, see [below](#global-search).
|
||||
|
||||
All of these endpoints except for the logging endpoint allow you to
|
||||
fetch, edit and delete individual objects by appending their primary key
|
||||
to the path, for example `/api/documents/454/`.
|
||||
fetch (and edit and delete where appropriate) individual objects by
|
||||
appending their primary key to the path, e.g. `/api/documents/454/`.
|
||||
|
||||
The objects served by the document endpoint contain the following
|
||||
fields:
|
||||
@@ -46,6 +53,15 @@ fields:
|
||||
Read-only.
|
||||
- `archived_file_name`: Verbose filename of the archived document.
|
||||
Read-only. Null if no archived document is available.
|
||||
- `notes`: Array of notes associated with the document.
|
||||
- `set_permissions`: Allows setting document permissions. Optional,
|
||||
write-only. See [below](#permissions).
|
||||
- `custom_fields`: Array of custom fields & values, specified as
|
||||
`{ field: CUSTOM_FIELD_ID, value: VALUE }`
|
||||
|
||||
!!! note
|
||||
|
||||
Note that all endpoint URLs must end with a `/`slash.
|
||||
|
||||
## Downloading documents
|
||||
|
||||
@@ -121,9 +137,15 @@ File metadata is reported as a list of objects in the following form:
|
||||
depends on the file type and the metadata available in that specific
|
||||
document. Paperless only reports PDF metadata at this point.
|
||||
|
||||
## Documents additional endpoints
|
||||
|
||||
- `/api/documents/<id>/notes/`: Retrieve notes for a document.
|
||||
- `/api/documents/<id>/share_links/`: Retrieve share links for a document.
|
||||
- `/api/documents/<id>/history/`: Retrieve history of changes for a document.
|
||||
|
||||
## Authorization
|
||||
|
||||
The REST api provides three different forms of authentication.
|
||||
The REST api provides four different forms of authentication.
|
||||
|
||||
1. Basic authentication
|
||||
|
||||
@@ -144,6 +166,10 @@ The REST api provides three different forms of authentication.
|
||||
|
||||
3. Token authentication
|
||||
|
||||
You can create (or re-create) an API token by opening the "My Profile"
|
||||
link in the user dropdown found in the web UI and clicking the circular
|
||||
arrow button.
|
||||
|
||||
Paperless also offers an endpoint to acquire authentication tokens.
|
||||
|
||||
POST a username and password as a form or json string to
|
||||
@@ -155,7 +181,45 @@ The REST api provides three different forms of authentication.
|
||||
Authorization: Token <token>
|
||||
```
|
||||
|
||||
Tokens can be managed and revoked in the paperless admin.
|
||||
Tokens can also be managed in the Django admin.
|
||||
|
||||
4. Remote User authentication
|
||||
|
||||
If enabled (see
|
||||
[configuration](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER_API)),
|
||||
you can authenticate against the API using Remote User auth.
|
||||
|
||||
## Global search
|
||||
|
||||
A global search endpoint is available at `/api/search/` and requires a search term
|
||||
of > 2 characters e.g. `?query=foo`. This endpoint returns a maximum of 3 results
|
||||
across nearly all objects, e.g. documents, tags, saved views, mail rules, etc.
|
||||
Results are only included if the requesting user has the appropriate permissions.
|
||||
|
||||
Results are returned in the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
total: number
|
||||
documents: []
|
||||
saved_views: []
|
||||
correspondents: []
|
||||
document_types: []
|
||||
storage_paths: []
|
||||
tags: []
|
||||
users: []
|
||||
groups: []
|
||||
mail_accounts: []
|
||||
mail_rules: []
|
||||
custom_fields: []
|
||||
workflows: []
|
||||
}
|
||||
```
|
||||
|
||||
Global search first searches objects by name (or title for documents) matching the query.
|
||||
If the optional `db_only` parameter is set, only document titles will be searched. Otherwise,
|
||||
if the amount of documents returned by a simple title string search is < 3, results from the
|
||||
search index will also be included.
|
||||
|
||||
## Searching for documents
|
||||
|
||||
@@ -164,8 +228,8 @@ specific query parameters cause the API to return full text search
|
||||
results:
|
||||
|
||||
- `/api/documents/?query=your%20search%20query`: Search for a document
|
||||
using a full text query. For details on the syntax, see [Basic Usage - Searching](/usage#basic-usage_searching).
|
||||
- `/api/documents/?more_like=1234`: Search for documents similar to
|
||||
using a full text query. For details on the syntax, see [Basic Usage - Searching](usage.md#basic-usage_searching).
|
||||
- `/api/documents/?more_like_id=1234`: Search for documents similar to
|
||||
the document with id 1234.
|
||||
|
||||
Pagination works exactly the same as it does for normal requests on this
|
||||
@@ -254,17 +318,137 @@ The endpoint supports the following optional form fields:
|
||||
- `correspondent`: Specify the ID of a correspondent that the consumer
|
||||
should use for the document.
|
||||
- `document_type`: Similar to correspondent.
|
||||
- `storage_path`: Similar to correspondent.
|
||||
- `tags`: Similar to correspondent. Specify this multiple times to
|
||||
have multiple tags added to the document.
|
||||
- `owner`: An optional user ID to set as the owner.
|
||||
- `archive_serial_number`: An optional archive serial number to set.
|
||||
- `custom_fields`: An array of custom field ids to assign (with an empty
|
||||
value) to the document.
|
||||
|
||||
The endpoint will immediately return HTTP 200 if the document consumption
|
||||
process was started successfully, with the UUID of the consumption task
|
||||
as the data. No additional status information about
|
||||
the consumption process itself is available immediately, since that happens in a
|
||||
different process. Querying the tasks endpoint with the returned UUID will
|
||||
provide information on the state of the consumption.
|
||||
as the data. No additional status information about the consumption process
|
||||
itself is available immediately, since that happens in a different process.
|
||||
However, querying the tasks endpoint with the returned UUID e.g.
|
||||
`/api/tasks/?task_id={uuid}` will provide information on the state of the
|
||||
consumption including the ID of a created document if consumption succeeded.
|
||||
|
||||
## Permissions
|
||||
|
||||
All objects (documents, tags, etc.) allow setting object-level permissions
|
||||
with optional `owner` and / or a `set_permissions` parameters which are of
|
||||
the form:
|
||||
|
||||
```
|
||||
"owner": ...,
|
||||
"set_permissions": {
|
||||
"view": {
|
||||
"users": [...],
|
||||
"groups": [...],
|
||||
},
|
||||
"change": {
|
||||
"users": [...],
|
||||
"groups": [...],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
Arrays should contain user or group ID numbers.
|
||||
|
||||
If these parameters are supplied the object's permissions will be overwritten,
|
||||
assuming the authenticated user has permission to do so (the user must be
|
||||
the object owner or a superuser).
|
||||
|
||||
### Retrieving full permissions
|
||||
|
||||
By default, the API will return a truncated version of object-level
|
||||
permissions, returning `user_can_change` indicating whether the current user
|
||||
can edit the object (either because they are the object owner or have permissions
|
||||
granted). You can pass the parameter `full_perms=true` to API calls to view the
|
||||
full permissions of objects in a format that mirrors the `set_permissions`
|
||||
parameter above.
|
||||
|
||||
## Bulk Editing
|
||||
|
||||
The API supports various bulk-editing operations which are executed asynchronously.
|
||||
|
||||
### Documents
|
||||
|
||||
For bulk operations on documents, use the endpoint `/api/documents/bulk_edit/` which accepts
|
||||
a json payload of the format:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [LIST_OF_DOCUMENT_IDS],
|
||||
"method": METHOD, // see below
|
||||
"parameters": args // see below
|
||||
}
|
||||
```
|
||||
|
||||
The following methods are supported:
|
||||
|
||||
- `set_correspondent`
|
||||
- Requires `parameters`: `{ "correspondent": CORRESPONDENT_ID }`
|
||||
- `set_document_type`
|
||||
- Requires `parameters`: `{ "document_type": DOCUMENT_TYPE_ID }`
|
||||
- `set_storage_path`
|
||||
- Requires `parameters`: `{ "storage_path": STORAGE_PATH_ID }`
|
||||
- `add_tag`
|
||||
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||
- `remove_tag`
|
||||
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||
- `modify_tags`
|
||||
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and / or `{ "remove_tags": [LIST_OF_TAG_IDS] }`
|
||||
- `delete`
|
||||
- No `parameters` required
|
||||
- `reprocess`
|
||||
- No `parameters` required
|
||||
- `set_permissions`
|
||||
- Requires `parameters`:
|
||||
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
|
||||
- `"owner": OWNER_ID or null`
|
||||
- `"merge": true or false` (defaults to false)
|
||||
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
||||
removing them) or be merged with existing permissions.
|
||||
- `merge`
|
||||
- No additional `parameters` required.
|
||||
- The ordering of the merged document is determined by the list of IDs.
|
||||
- Optional `parameters`:
|
||||
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
|
||||
- `"delete_originals": true` to delete the original documents. This requires the calling user being the owner of
|
||||
all documents that are merged.
|
||||
- `split`
|
||||
- Requires `parameters`:
|
||||
- `"pages": [..]` The list should be a list of pages and/or a ranges, separated by commas e.g. `"[1,2-3,4,5-7]"`
|
||||
- Optional `parameters`:
|
||||
- `"delete_originals": true` to delete the original document after consumption. This requires the calling user being the owner of
|
||||
the document.
|
||||
- The split operation only accepts a single document.
|
||||
- `rotate`
|
||||
- Requires `parameters`:
|
||||
- `"degrees": DEGREES`. Must be an integer i.e. 90, 180, 270
|
||||
- `delete_pages`
|
||||
- Requires `parameters`:
|
||||
- `"pages": [..]` The list should be a list of integers e.g. `"[2,3,4]"`
|
||||
- The delete_pages operation only accepts a single document.
|
||||
|
||||
### Objects
|
||||
|
||||
Bulk editing for objects (tags, document types etc.) currently supports set permissions or delete
|
||||
operations, using the endpoint: `/api/bulk_edit_objects/`, which requires a json payload of the format:
|
||||
|
||||
```json
|
||||
{
|
||||
"objects": [LIST_OF_OBJECT_IDS],
|
||||
"object_type": "tags", "correspondents", "document_types" or "storage_paths",
|
||||
"operation": "set_permissions" or "delete",
|
||||
"owner": OWNER_ID, // optional
|
||||
"permissions": { "view": { "users": [] ... }, "change": { ... } }, // (see 'set_permissions' format above)
|
||||
"merge": true / false // defaults to false, see above
|
||||
}
|
||||
```
|
||||
|
||||
## API Versioning
|
||||
|
||||
@@ -322,3 +506,13 @@ Initial API version.
|
||||
color to use for a specific tag, which is either black or white
|
||||
depending on the brightness of `Tag.color`.
|
||||
- Removed field `Tag.colour`.
|
||||
|
||||
#### Version 3
|
||||
|
||||
- Permissions endpoints have been added.
|
||||
- The format of the `/api/ui_settings/` has changed.
|
||||
|
||||
#### Version 4
|
||||
|
||||
- Consumption templates were refactored to workflows and API endpoints
|
||||
changed as such.
|
||||
|
@@ -20,6 +20,28 @@
|
||||
margin-left: 4%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.grid-flipped-left {
|
||||
width: 66%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.grid-flipped-right {
|
||||
width: 29%;
|
||||
margin-left: 4%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.grid-half-left {
|
||||
width: 48%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.grid-half-right {
|
||||
width: 48%;
|
||||
margin-left: 4%;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-left > p {
|
||||
@@ -31,6 +53,48 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.index-callout {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
/* make code in headers not bold */
|
||||
h4 code {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Hide config vars from sidebar, toc and move the border on mobile case their hidden */
|
||||
.md-nav.md-nav--secondary .md-nav__item .md-nav__link[href*="PAPERLESS_"],
|
||||
.md-nav.md-nav--secondary .md-nav__item .md-nav__link[href*="USERMAP_"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 76.1875em) {
|
||||
.md-nav--primary .md-nav__item {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.md-nav--primary .md-nav__link {
|
||||
border-top: .05rem solid var(--md-default-fg-color--lightest);
|
||||
}
|
||||
}
|
||||
|
||||
/* Show search shortcut key */
|
||||
[data-md-toggle="search"]:not(:checked) ~ .md-header .md-search__form::after {
|
||||
position: absolute;
|
||||
top: .3rem;
|
||||
right: .3rem;
|
||||
display: block;
|
||||
padding: .1rem .4rem;
|
||||
color: var(--md-default-fg-color--lighter);
|
||||
font-weight: bold;
|
||||
font-size: .8rem;
|
||||
border: .05rem solid var(--md-default-fg-color--lighter);
|
||||
border-radius: .1rem;
|
||||
content: "/";
|
||||
}
|
||||
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 457 KiB After Width: | Height: | Size: 501 KiB |
BIN
docs/assets/screenshots/custom_field1.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
docs/assets/screenshots/custom_field2.png
Normal file
After Width: | Height: | Size: 2.2 MiB |
Before Width: | Height: | Size: 890 KiB After Width: | Height: | Size: 644 KiB |
Before Width: | Height: | Size: 462 KiB After Width: | Height: | Size: 667 KiB |
Before Width: | Height: | Size: 608 KiB After Width: | Height: | Size: 1003 KiB |
Before Width: | Height: | Size: 698 KiB After Width: | Height: | Size: 1.7 MiB |
BIN
docs/assets/screenshots/documents-smallcards-slimsidebar.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 706 KiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 480 KiB After Width: | Height: | Size: 925 KiB |
Before Width: | Height: | Size: 680 KiB After Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 686 KiB After Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 848 KiB After Width: | Height: | Size: 2.3 MiB |
Before Width: | Height: | Size: 703 KiB After Width: | Height: | Size: 726 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 169 KiB |
Before Width: | Height: | Size: 388 KiB |
BIN
docs/assets/screenshots/mobile1.png
Normal file
After Width: | Height: | Size: 432 KiB |
BIN
docs/assets/screenshots/mobile2.png
Normal file
After Width: | Height: | Size: 280 KiB |
BIN
docs/assets/screenshots/mobile3.png
Normal file
After Width: | Height: | Size: 246 KiB |
BIN
docs/assets/screenshots/new-correspondent.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
docs/assets/screenshots/new-document_type.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
docs/assets/screenshots/new-storage_path.png
Normal file
After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 45 KiB |
BIN
docs/assets/screenshots/permissions_document.png
Normal file
After Width: | Height: | Size: 559 KiB |
BIN
docs/assets/screenshots/permissions_global.png
Normal file
After Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 517 KiB After Width: | Height: | Size: 792 KiB |
BIN
docs/assets/screenshots/workflow.png
Normal file
After Width: | Height: | Size: 137 KiB |
2752
docs/changelog.md
@@ -9,7 +9,7 @@ following way:
|
||||
- `main` always represents the latest release and will only see
|
||||
changes when a new release is made.
|
||||
- `dev` contains the code that will be in the next release.
|
||||
- `feature-X` contain bigger changes that will be in some release, but
|
||||
- `feature-X` contains bigger changes that will be in some release, but
|
||||
not necessarily the next one.
|
||||
|
||||
When making functional changes to Paperless-ngx, _always_ make your changes
|
||||
@@ -47,7 +47,7 @@ early on.
|
||||
Once installed, hooks will run when you commit. If the formatting isn't
|
||||
quite right or a linter catches something, the commit will be rejected.
|
||||
You'll need to look at the output and fix the issue. Some hooks, such
|
||||
as the Python formatting tool `black`, will format failing
|
||||
as the Python linting and formatting tool `ruff`, will format failing
|
||||
files, so all you need to do is `git add` those files again
|
||||
and retry your commit.
|
||||
|
||||
@@ -58,10 +58,10 @@ first-time setup.
|
||||
|
||||
!!! note
|
||||
|
||||
Every command is executed directly from the root folder of the project unless specified otherwise.
|
||||
Every command is executed directly from the root folder of the project unless specified otherwise.
|
||||
|
||||
1. Install prerequisites + pipenv as mentioned in
|
||||
[Bare metal route](/setup#bare_metal).
|
||||
[Bare metal route](setup.md#bare_metal).
|
||||
|
||||
2. Copy `paperless.conf.example` to `paperless.conf` and enable debug
|
||||
mode within the file via `PAPERLESS_DEBUG=true`.
|
||||
@@ -81,10 +81,6 @@ first-time setup.
|
||||
!!! note
|
||||
|
||||
Using a virtual environment is highly recommended. You can spawn one via `pipenv shell`.
|
||||
Make sure you're using Python 3.10.x or lower. Otherwise you might
|
||||
get issues with building dependencies. You can use
|
||||
[pyenv](https://github.com/pyenv/pyenv) to install a specific
|
||||
Python version.
|
||||
|
||||
5. Install pre-commit hooks:
|
||||
|
||||
@@ -177,69 +173,69 @@ The front end is built using AngularJS. In order to get started, you need Node.j
|
||||
|
||||
The following commands are all performed in the `src-ui`-directory. You will need a running back end (including an active session) to connect to the back end API. To spin it up refer to the commands under the section [above](#back-end-development).
|
||||
|
||||
1. Install the Angular CLI. You might need sudo privileges
|
||||
to perform this command:
|
||||
1. Install the Angular CLI. You might need sudo privileges to perform this command:
|
||||
|
||||
```bash
|
||||
$ npm install -g @angular/cli
|
||||
```
|
||||
```bash
|
||||
$ npm install -g @angular/cli
|
||||
```
|
||||
|
||||
2. Make sure that it's on your path.
|
||||
2. Make sure that it's on your path.
|
||||
|
||||
3. Install all neccessary modules:
|
||||
3. Install all necessary modules:
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
4. You can launch a development server by running:
|
||||
4. You can launch a development server by running:
|
||||
|
||||
```bash
|
||||
$ ng serve
|
||||
```
|
||||
```bash
|
||||
$ ng serve
|
||||
```
|
||||
|
||||
This will automatically update whenever you save. However, in-place
|
||||
compilation might fail on syntax errors, in which case you need to
|
||||
restart it.
|
||||
This will automatically update whenever you save. However, in-place
|
||||
compilation might fail on syntax errors, in which case you need to
|
||||
restart it.
|
||||
|
||||
By default, the development server is available on `http://localhost:4200/` and is configured to access the API at
|
||||
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts, CORS and X-Frame-Options are in place so that the front end behaves exactly as in production.
|
||||
By default, the development server is available on `http://localhost:4200/` and is configured to access the API at
|
||||
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts, CORS and X-Frame-Options are in place so that the front end behaves exactly as in production.
|
||||
|
||||
### Testing and code style
|
||||
|
||||
- The front end code (.ts, .html, .scss) use `prettier` for code
|
||||
formatting via the Git `pre-commit` hooks which run automatically on
|
||||
commit. See [above](#code-formatting-with-pre-commit-hooks) for installation instructions. You can also run this via the CLI with a
|
||||
command such as
|
||||
The front end code (.ts, .html, .scss) use `prettier` for code
|
||||
formatting via the Git `pre-commit` hooks which run automatically on
|
||||
commit. See [above](#code-formatting-with-pre-commit-hooks) for installation instructions. You can also run this via the CLI with a
|
||||
command such as
|
||||
|
||||
```bash
|
||||
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
|
||||
```
|
||||
```bash
|
||||
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
|
||||
```
|
||||
|
||||
- Front end testing uses jest and cypress. There is currently a need
|
||||
for significantly more front end tests. Unit tests and e2e tests,
|
||||
respectively, can be run non-interactively with:
|
||||
Front end testing uses Jest and Playwright. Unit tests and e2e tests,
|
||||
respectively, can be run non-interactively with:
|
||||
|
||||
```bash
|
||||
$ ng test
|
||||
$ npm run e2e:ci
|
||||
```
|
||||
```bash
|
||||
$ ng test
|
||||
$ npx playwright test
|
||||
```
|
||||
|
||||
- Cypress also includes a UI which can be run with:
|
||||
Playwright also includes a UI which can be run with:
|
||||
|
||||
```bash
|
||||
$ ./node_modules/.bin/cypress open
|
||||
```
|
||||
```bash
|
||||
$ npx playwright test --ui
|
||||
```
|
||||
|
||||
- In order to build the front end and serve it as part of Django, execute:
|
||||
### Building the frontend
|
||||
|
||||
```bash
|
||||
$ ng build --configuration production
|
||||
```
|
||||
In order to build the front end and serve it as part of Django, execute:
|
||||
|
||||
This will build the front end and put it in a location from which the
|
||||
Django server will serve it as static content. This way, you can verify
|
||||
that authentication is working.
|
||||
```bash
|
||||
$ ng build --configuration production
|
||||
```
|
||||
|
||||
This will build the front end and put it in a location from which the
|
||||
Django server will serve it as static content. This way, you can verify
|
||||
that authentication is working.
|
||||
|
||||
## Localization
|
||||
|
||||
@@ -277,27 +273,17 @@ Adding new languages requires adding the translated files in the
|
||||
}
|
||||
```
|
||||
|
||||
2. Add the language to the available options in
|
||||
2. Add the language to the `LANGUAGE_OPTIONS` array in
|
||||
`src-ui/src/app/services/settings.service.ts`:
|
||||
|
||||
```typescript
|
||||
getLanguageOptions(): LanguageOption[] {
|
||||
return [
|
||||
{code: "en-us", name: $localize`English (US)`, englishName: "English (US)", dateInputFormat: "mm/dd/yyyy"},
|
||||
{code: "en-gb", name: $localize`English (GB)`, englishName: "English (GB)", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "de", name: $localize`German`, englishName: "German", dateInputFormat: "dd.mm.yyyy"},
|
||||
{code: "nl", name: $localize`Dutch`, englishName: "Dutch", dateInputFormat: "dd-mm-yyyy"},
|
||||
{code: "fr", name: $localize`French`, englishName: "French", dateInputFormat: "dd/mm/yyyy"},
|
||||
{code: "pt-br", name: $localize`Portuguese (Brazil)`, englishName: "Portuguese (Brazil)", dateInputFormat: "dd/mm/yyyy"}
|
||||
// Add your new language here
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`dateInputFormat` is a special string that defines the behavior of
|
||||
the date input fields and absolutely needs to contain "dd", "mm"
|
||||
and "yyyy".
|
||||
|
||||
```
|
||||
|
||||
3. Import and register the Angular data for this locale in
|
||||
`src-ui/src/app/app.module.ts`:
|
||||
|
||||
@@ -362,7 +348,7 @@ If you want to build the documentation locally, this is how you do it:
|
||||
|
||||
3. Serve the documentation. This will spin up a
|
||||
copy of the documentation at http://127.0.0.1:8000
|
||||
that will automatically refresh everytime you change
|
||||
that will automatically refresh every time you change
|
||||
something.
|
||||
|
||||
```bash
|
||||
@@ -374,13 +360,10 @@ If you want to build the documentation locally, this is how you do it:
|
||||
The docker image is primarily built by the GitHub actions workflow, but
|
||||
it can be faster when developing to build and tag an image locally.
|
||||
|
||||
To provide the build arguments automatically, build the image using the
|
||||
helper script `build-docker-image.sh`.
|
||||
Building the image works as with any image:
|
||||
|
||||
Building the docker image from source:
|
||||
|
||||
```bash
|
||||
./build-docker-image.sh Dockerfile -t <your-tag>
|
||||
```
|
||||
docker build --file Dockerfile --tag paperless:local --progress simple .
|
||||
```
|
||||
|
||||
## Extending Paperless-ngx
|
||||
@@ -398,7 +381,7 @@ responsible for:
|
||||
- Retrieving the content from the original
|
||||
- Creating a thumbnail
|
||||
- _optional:_ Retrieving a created date from the original
|
||||
- _optional:_ Creainge an archived document from the original
|
||||
- _optional:_ Creating an archived document from the original
|
||||
|
||||
Custom parsers can be added to Paperless-ngx to support more file types. In
|
||||
order to do that, you need to write the parser itself and announce its
|
||||
|
45
docs/faq.md
@@ -3,15 +3,16 @@
|
||||
## _What's the general plan for Paperless-ngx?_
|
||||
|
||||
**A:** While Paperless-ngx is already considered largely
|
||||
"feature-complete" it is a community-driven project and development
|
||||
will be guided in this way. New features can be submitted via GitHub
|
||||
discussions and "up-voted" by the community but this is not a
|
||||
guarantee the feature will be implemented. This project will always be
|
||||
"feature-complete", it is a community-driven project and development
|
||||
will be guided in this way. New features can be submitted via
|
||||
[GitHub discussions](https://github.com/paperless-ngx/paperless-ngx/discussions)
|
||||
and "up-voted" by the community, but this is not a
|
||||
guarantee that the feature will be implemented. This project will always be
|
||||
open to collaboration in the form of PRs, ideas etc.
|
||||
|
||||
## _I'm using docker. Where are my documents?_
|
||||
|
||||
**A:** Your documents are stored inside the docker volume
|
||||
**A:** By default, your documents are stored inside the docker volume
|
||||
`paperless_media`. Docker manages this volume automatically for you. It
|
||||
is a persistent storage and will persist as long as you don't
|
||||
explicitly delete it. The actual location depends on your host operating
|
||||
@@ -27,6 +28,12 @@ system. On Linux, chances are high that this location is
|
||||
files around manually. This folder is meant to be entirely managed by
|
||||
docker and paperless.
|
||||
|
||||
!!! note
|
||||
|
||||
Files consumed from the consumption directory are re-created inside
|
||||
this media directory and are removed from the consumption directory
|
||||
itself.
|
||||
|
||||
## Let's say I want to switch tools in a year. Can I easily move to other systems?
|
||||
|
||||
**A:** Your documents are stored as plain files inside the media folder.
|
||||
@@ -39,8 +46,8 @@ elsewhere. Here are a couple notes about that.
|
||||
- By default, paperless uses the internal ID of each document as its
|
||||
filename. This might not be very convenient for export. However, you
|
||||
can adjust the way files are stored in paperless by
|
||||
[configuring the filename format](/advanced_usage#file-name-handling).
|
||||
- [The exporter](/administration#exporter) is
|
||||
[configuring the filename format](advanced_usage.md#file-name-handling).
|
||||
- [The exporter](administration.md#exporter) is
|
||||
another easy way to get your files out of paperless with reasonable
|
||||
file names.
|
||||
|
||||
@@ -52,7 +59,7 @@ elsewhere. Here are a couple notes about that.
|
||||
WebP images are processed with OCR and converted into PDF documents.
|
||||
- Plain text documents are supported as well and are added verbatim to
|
||||
paperless.
|
||||
- With the optional Tika integration enabled (see [Tika configuration](/configuration#tika),
|
||||
- With the optional Tika integration enabled (see [Tika configuration](https://docs.paperless-ngx.com/configuration#tika)),
|
||||
Paperless also supports various Office documents (.docx, .doc, odt,
|
||||
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
||||
|
||||
@@ -71,20 +78,27 @@ has to do much less work to serve the data.
|
||||
!!! note
|
||||
|
||||
You can adjust some of the settings so that paperless uses less
|
||||
processing power. See [setup](/setup#less-powerful-devices) for details.
|
||||
processing power. See [setup](setup.md#less-powerful-devices) for details.
|
||||
|
||||
## _How do I install paperless-ngx on Raspberry Pi?_
|
||||
|
||||
**A:** Docker images are available for armv7 and arm64 hardware, so just
|
||||
follow the docker-compose instructions. Apart from more required disk
|
||||
**A:** Docker images are available for arm64 hardware, so just
|
||||
follow the [Docker Compose instructions](https://docs.paperless-ngx.com/setup/#installation). Apart from more required disk
|
||||
space compared to a bare metal installation, docker comes with close to
|
||||
zero overhead, even on Raspberry Pi.
|
||||
|
||||
If you decide to got with the bare metal route, be aware that some of
|
||||
If you decide to go with the bare metal route, be aware that some of
|
||||
the python requirements do not have precompiled packages for ARM /
|
||||
ARM64. Installation of these will require additional development
|
||||
libraries and compilation will take a long time.
|
||||
|
||||
!!! note
|
||||
|
||||
For ARMv7 (32-bit) systems, paperless may still function, but it could require
|
||||
modifications to the Dockerfile (if using Docker) or additional
|
||||
tools for installing bare metal. It is suggested to upgrade to arm64
|
||||
instead.
|
||||
|
||||
## _How do I run this on Unraid?_
|
||||
|
||||
**A:** Paperless-ngx is available as [community
|
||||
@@ -96,14 +110,11 @@ Fahrer](https://github.com/Tooa) created a container template for that.
|
||||
**A:** I honestly don't know! As for all other devices that might be
|
||||
able to run paperless, you're a bit on your own. If you can't run the
|
||||
docker image, the documentation has instructions for bare metal
|
||||
installs. I'm running paperless on an i3 processor from 2015 or so.
|
||||
This is also what I use to test new releases with. Apart from that, I
|
||||
also have a Raspberry Pi, which I occasionally build the image on and
|
||||
see if it works.
|
||||
installs.
|
||||
|
||||
## _How do I proxy this with NGINX?_
|
||||
|
||||
**A:** See [here](/setup#nginx).
|
||||
**A:** See [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#nginx).
|
||||
|
||||
## _How do I get WebSocket support with Apache mod_wsgi_?
|
||||
|
||||
|